Skip to content

Commit de100c2

Browse files
authored
Merge pull request #21 from brevdev/devin/1754709828-add-lambdalabs-error-tests
Add comprehensive test coverage for Lambda Labs error handling functions
2 parents 8b0a3c4 + e7f2302 commit de100c2

File tree

1 file changed

+327
-0
lines changed

1 file changed

+327
-0
lines changed
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
package v1
2+
3+
import (
4+
"context"
5+
"errors"
6+
"io"
7+
"net/http"
8+
"net/url"
9+
"strings"
10+
"testing"
11+
12+
"github.com/cenkalti/backoff/v4"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
16+
openapi "github.com/brevdev/cloud/internal/lambdalabs/gen/lambdalabs"
17+
v1 "github.com/brevdev/cloud/pkg/v1"
18+
)
19+
20+
func TestHandleAPIError_InstanceNotFound(t *testing.T) {
21+
body := `{"error": {"message": "instance does not exist"}}`
22+
resp := &http.Response{
23+
StatusCode: 404,
24+
Body: io.NopCloser(strings.NewReader(body)),
25+
Request: &http.Request{URL: &url.URL{Path: "/test"}},
26+
Status: "404 Not Found",
27+
}
28+
29+
err := handleAPIError(context.Background(), resp, errors.New("not found"))
30+
31+
var permanentErr *backoff.PermanentError
32+
require.True(t, errors.As(err, &permanentErr))
33+
assert.Equal(t, v1.ErrInstanceNotFound, permanentErr.Err)
34+
}
35+
36+
func TestHandleAPIError_BannedTemporarily(t *testing.T) {
37+
body := `{"error": {"message": "banned you temporarily"}}`
38+
resp := &http.Response{
39+
StatusCode: 429,
40+
Body: io.NopCloser(strings.NewReader(body)),
41+
Request: &http.Request{URL: &url.URL{Path: "/test"}},
42+
Status: "429 Too Many Requests",
43+
}
44+
45+
err := handleAPIError(context.Background(), resp, errors.New("rate limited"))
46+
47+
var permanentErr *backoff.PermanentError
48+
assert.False(t, errors.As(err, &permanentErr))
49+
assert.Contains(t, err.Error(), "LambdaLabs API error")
50+
assert.Contains(t, err.Error(), "banned you temporarily")
51+
}
52+
53+
func TestHandleAPIError_ClientError(t *testing.T) {
54+
tests := []struct {
55+
name string
56+
statusCode int
57+
status string
58+
}{
59+
{"bad request", 400, "400 Bad Request"},
60+
{"unauthorized", 401, "401 Unauthorized"},
61+
{"forbidden", 403, "403 Forbidden"},
62+
{"not found", 404, "404 Not Found"},
63+
}
64+
65+
for _, tt := range tests {
66+
t.Run(tt.name, func(t *testing.T) {
67+
body := `{"error": {"message": "client error"}}`
68+
resp := &http.Response{
69+
StatusCode: tt.statusCode,
70+
Body: io.NopCloser(strings.NewReader(body)),
71+
Request: &http.Request{URL: &url.URL{Path: "/test"}},
72+
Status: tt.status,
73+
}
74+
75+
err := handleAPIError(context.Background(), resp, errors.New("client error"))
76+
77+
var permanentErr *backoff.PermanentError
78+
require.True(t, errors.As(err, &permanentErr))
79+
assert.Contains(t, permanentErr.Err.Error(), "LambdaLabs API error")
80+
})
81+
}
82+
}
83+
84+
func TestHandleAPIError_TooManyRequests(t *testing.T) {
85+
body := `{"error": {"message": "too many requests"}}`
86+
resp := &http.Response{
87+
StatusCode: 429,
88+
Body: io.NopCloser(strings.NewReader(body)),
89+
Request: &http.Request{URL: &url.URL{Path: "/test"}},
90+
Status: "429 Too Many Requests",
91+
}
92+
93+
err := handleAPIError(context.Background(), resp, errors.New("rate limited"))
94+
95+
var permanentErr *backoff.PermanentError
96+
assert.False(t, errors.As(err, &permanentErr))
97+
assert.Contains(t, err.Error(), "LambdaLabs API error")
98+
}
99+
100+
func TestHandleAPIError_ServerError(t *testing.T) {
101+
tests := []struct {
102+
name string
103+
statusCode int
104+
status string
105+
}{
106+
{"internal server error", 500, "500 Internal Server Error"},
107+
{"bad gateway", 502, "502 Bad Gateway"},
108+
{"service unavailable", 503, "503 Service Unavailable"},
109+
}
110+
111+
for _, tt := range tests {
112+
t.Run(tt.name, func(t *testing.T) {
113+
body := `{"error": {"message": "server error"}}`
114+
resp := &http.Response{
115+
StatusCode: tt.statusCode,
116+
Body: io.NopCloser(strings.NewReader(body)),
117+
Request: &http.Request{URL: &url.URL{Path: "/test"}},
118+
Status: tt.status,
119+
}
120+
121+
err := handleAPIError(context.Background(), resp, errors.New("server error"))
122+
123+
var permanentErr *backoff.PermanentError
124+
assert.False(t, errors.As(err, &permanentErr))
125+
assert.Contains(t, err.Error(), "LambdaLabs API error")
126+
})
127+
}
128+
}
129+
130+
func TestHandleAPIError_OpenAPIError(t *testing.T) {
131+
body := `{"error": {"message": "test error"}}`
132+
133+
openAPIErr := openapi.GenericOpenAPIError{}
134+
135+
resp := &http.Response{
136+
StatusCode: 400,
137+
Body: io.NopCloser(strings.NewReader(body)),
138+
Request: &http.Request{URL: &url.URL{Path: "/test"}},
139+
Status: "400 Bad Request",
140+
}
141+
142+
err := handleAPIError(context.Background(), resp, openAPIErr)
143+
144+
var permanentErr *backoff.PermanentError
145+
require.True(t, errors.As(err, &permanentErr))
146+
assert.Contains(t, permanentErr.Err.Error(), "LambdaLabs API error")
147+
assert.Contains(t, permanentErr.Err.Error(), "/test")
148+
assert.Contains(t, permanentErr.Err.Error(), "400 Bad Request")
149+
}
150+
151+
func TestHandleAPIError_EmptyBody(t *testing.T) {
152+
resp := &http.Response{
153+
StatusCode: 400,
154+
Body: io.NopCloser(strings.NewReader("")),
155+
Request: &http.Request{URL: &url.URL{Path: "/test"}},
156+
Status: "400 Bad Request",
157+
}
158+
159+
err := handleAPIError(context.Background(), resp, errors.New("test error"))
160+
161+
var permanentErr *backoff.PermanentError
162+
require.True(t, errors.As(err, &permanentErr))
163+
assert.Contains(t, permanentErr.Err.Error(), "LambdaLabs API error")
164+
assert.Contains(t, permanentErr.Err.Error(), "test error")
165+
}
166+
167+
func TestHandleAPIError_BodyReadError(t *testing.T) {
168+
resp := &http.Response{
169+
StatusCode: 400,
170+
Body: &errorReader{},
171+
Request: &http.Request{URL: &url.URL{Path: "/test"}},
172+
Status: "400 Bad Request",
173+
}
174+
175+
err := handleAPIError(context.Background(), resp, errors.New("test error"))
176+
177+
var permanentErr *backoff.PermanentError
178+
require.True(t, errors.As(err, &permanentErr))
179+
assert.Contains(t, permanentErr.Err.Error(), "LambdaLabs API error")
180+
}
181+
182+
type errorReader struct{}
183+
184+
func (e *errorReader) Read(_ []byte) (n int, err error) {
185+
return 0, errors.New("read error")
186+
}
187+
188+
func (e *errorReader) Close() error {
189+
return nil
190+
}
191+
192+
func TestHandleErrToCloudErr_NilError(t *testing.T) {
193+
result := handleErrToCloudErr(nil)
194+
assert.Nil(t, result)
195+
}
196+
197+
func TestHandleErrToCloudErr_CapacityErrors(t *testing.T) {
198+
tests := []struct {
199+
name string
200+
errMsg string
201+
expected error
202+
}{
203+
{
204+
name: "not enough capacity",
205+
errMsg: "Not enough capacity in region",
206+
expected: v1.ErrInsufficientResources,
207+
},
208+
{
209+
name: "insufficient capacity",
210+
errMsg: "insufficient-capacity error occurred",
211+
expected: v1.ErrInsufficientResources,
212+
},
213+
{
214+
name: "capacity with mixed case",
215+
errMsg: "Error: Not enough capacity available",
216+
expected: v1.ErrInsufficientResources,
217+
},
218+
}
219+
220+
for _, tt := range tests {
221+
t.Run(tt.name, func(t *testing.T) {
222+
inputErr := errors.New(tt.errMsg)
223+
result := handleErrToCloudErr(inputErr)
224+
assert.Equal(t, tt.expected, result)
225+
})
226+
}
227+
}
228+
229+
func TestHandleErrToCloudErr_RegionErrors(t *testing.T) {
230+
tests := []struct {
231+
name string
232+
errMsg string
233+
expected error
234+
}{
235+
{
236+
name: "region does not exist",
237+
errMsg: "global/invalid-parameters: Region us-invalid-1 does not exist",
238+
expected: v1.ErrInsufficientResources,
239+
},
240+
{
241+
name: "region error with different format",
242+
errMsg: "global/invalid-parameters error: Region eu-central-99 does not exist in this zone",
243+
expected: v1.ErrInsufficientResources,
244+
},
245+
{
246+
name: "invalid parameters without region",
247+
errMsg: "global/invalid-parameters: Invalid instance type",
248+
expected: errors.New("global/invalid-parameters: Invalid instance type"),
249+
},
250+
}
251+
252+
for _, tt := range tests {
253+
t.Run(tt.name, func(t *testing.T) {
254+
inputErr := errors.New(tt.errMsg)
255+
result := handleErrToCloudErr(inputErr)
256+
if tt.expected == v1.ErrInsufficientResources {
257+
assert.Equal(t, tt.expected, result)
258+
} else {
259+
assert.Equal(t, tt.expected.Error(), result.Error())
260+
}
261+
})
262+
}
263+
}
264+
265+
func TestHandleErrToCloudErr_OtherErrors(t *testing.T) {
266+
tests := []struct {
267+
name string
268+
errMsg string
269+
}{
270+
{
271+
name: "authentication error",
272+
errMsg: "invalid API key provided",
273+
},
274+
{
275+
name: "network error",
276+
errMsg: "connection timeout",
277+
},
278+
{
279+
name: "generic error",
280+
errMsg: "something went wrong",
281+
},
282+
}
283+
284+
for _, tt := range tests {
285+
t.Run(tt.name, func(t *testing.T) {
286+
inputErr := errors.New(tt.errMsg)
287+
result := handleErrToCloudErr(inputErr)
288+
assert.Equal(t, inputErr, result)
289+
})
290+
}
291+
}
292+
293+
func TestHandleErrToCloudErr_EdgeCases(t *testing.T) {
294+
tests := []struct {
295+
name string
296+
errMsg string
297+
expected error
298+
}{
299+
{
300+
name: "empty error message",
301+
errMsg: "",
302+
expected: errors.New(""),
303+
},
304+
{
305+
name: "capacity substring in larger message",
306+
errMsg: "The request failed because Not enough capacity is available in the selected region",
307+
expected: v1.ErrInsufficientResources,
308+
},
309+
{
310+
name: "insufficient capacity with prefix",
311+
errMsg: "API Error: insufficient-capacity - please try again later",
312+
expected: v1.ErrInsufficientResources,
313+
},
314+
}
315+
316+
for _, tt := range tests {
317+
t.Run(tt.name, func(t *testing.T) {
318+
inputErr := errors.New(tt.errMsg)
319+
result := handleErrToCloudErr(inputErr)
320+
if tt.expected == v1.ErrInsufficientResources {
321+
assert.Equal(t, tt.expected, result)
322+
} else {
323+
assert.Equal(t, tt.expected.Error(), result.Error())
324+
}
325+
})
326+
}
327+
}

0 commit comments

Comments
 (0)