Skip to content

Commit 1e54f35

Browse files
committed
✨ [api] added some helpers for more robust API calls
1 parent 0ac88fa commit 1e54f35

File tree

5 files changed

+208
-156
lines changed

5 files changed

+208
-156
lines changed

changes/20251223161900.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: [api] added some helpers for more robust API calls

utils/api/api.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,22 @@ func CallAndCheckSuccess[T any](ctx context.Context, errorContext string, apiCal
3535
return api.CallAndCheckSuccess[T](ctx, errorContext, errors.FetchAPIErrorDescriptionWithContext, apiCallFunc)
3636
}
3737

38+
// CallAndCheckSuccessAndReturnRawResponse is similar to CallAndCheckSuccess but also returns the raw http.Response.
39+
// It is the responsibility of the caller to close the body of such response.
40+
func CallAndCheckSuccessAndReturnRawResponse[T any](ctx context.Context, errorContext string, apiCallFunc func(ctx context.Context) (*T, *http.Response, error)) (*T, *http.Response, error) {
41+
return api.CallAndCheckSuccessAndReturnRawResponse[T](ctx, errorContext, errors.FetchAPIErrorDescriptionWithContext, apiCallFunc)
42+
}
43+
3844
// GenericCallAndCheckSuccess is similar to CallAndCheckSuccess but for function returning interfaces rather than concrete types.
3945
// T must be an interface.
4046
// errorContext corresponds to the description of what led to the error if error there is e.g. `Failed adding a user`.
4147
// apiCallFunc corresponds to a generic function that will be called to make the API call
4248
func GenericCallAndCheckSuccess[T any](ctx context.Context, errorContext string, apiCallFunc func(ctx context.Context) (T, *http.Response, error)) (T, error) {
4349
return api.GenericCallAndCheckSuccess[T](ctx, errorContext, errors.FetchAPIErrorDescriptionWithContext, apiCallFunc)
4450
}
51+
52+
// GenericCallAndCheckSuccessAndReturnRawResponse is similar to GenericCallAndCheckSuccess but also returns the raw http.Response.
53+
// It is the responsibility of the caller to close the body of such response.
54+
func GenericCallAndCheckSuccessAndReturnRawResponse[T any](ctx context.Context, errorContext string, apiCallFunc func(ctx context.Context) (T, *http.Response, error)) (T, *http.Response, error) {
55+
return api.GenericCallAndCheckSuccessAndReturnRawResponse[T](ctx, errorContext, errors.FetchAPIErrorDescriptionWithContext, apiCallFunc)
56+
}

utils/api/api_test.go

Lines changed: 192 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -86,172 +86,211 @@ func TestCheckAPICallSuccess(t *testing.T) {
8686
}
8787

8888
func TestCallAndCheckSuccess(t *testing.T) {
89-
t.Run("context cancelled", func(t *testing.T) {
90-
errMessage := "context cancelled"
91-
parentCtx := context.Background()
92-
ctx, cancelCtx := context.WithCancel(parentCtx)
93-
cancelCtx()
94-
_, actualErr := CallAndCheckSuccess(ctx, errMessage,
95-
func(ctx context.Context) (*struct{}, *_http.Response, error) {
96-
return nil, &_http.Response{Body: io.NopCloser(bytes.NewReader(nil))}, nil
97-
})
98-
errortest.AssertError(t, actualErr, commonerrors.ErrCancelled)
99-
})
89+
tests := []struct {
90+
Func func(ctx context.Context, errorContext string, apiCallFunc func(ctx context.Context) (*client.ErrorResponse, *_http.Response, error)) (*client.ErrorResponse, error)
91+
}{
92+
{
93+
Func: CallAndCheckSuccess[client.ErrorResponse],
94+
},
95+
{
96+
Func: func(ctx context.Context, errorContext string, apiCallFunc func(ctx context.Context) (*client.ErrorResponse, *_http.Response, error)) (*client.ErrorResponse, error) {
97+
r, resp, err := CallAndCheckSuccessAndReturnRawResponse(ctx, errorContext, apiCallFunc)
98+
if resp != nil && resp.Body != nil {
99+
require.NoError(t, resp.Body.Close())
100+
}
101+
return r, err
102+
},
103+
},
104+
}
105+
for i := range tests {
106+
test := tests[i]
107+
t.Run("context cancelled", func(t *testing.T) {
108+
errMessage := "context cancelled"
109+
parentCtx := context.Background()
110+
ctx, cancelCtx := context.WithCancel(parentCtx)
111+
cancelCtx()
112+
_, actualErr := test.Func(ctx, errMessage,
113+
func(ctx context.Context) (*client.ErrorResponse, *_http.Response, error) {
114+
return nil, &_http.Response{Body: io.NopCloser(bytes.NewReader(nil))}, nil
115+
})
116+
errortest.AssertError(t, actualErr, commonerrors.ErrCancelled)
117+
})
100118

101-
t.Run("api call not successful", func(t *testing.T) {
102-
errMessage := "client error"
103-
parentCtx := context.Background()
104-
_, actualErr := CallAndCheckSuccess(parentCtx, errMessage,
105-
func(ctx context.Context) (*struct{}, *_http.Response, error) {
106-
resp := _http.Response{StatusCode: 400, Body: io.NopCloser(bytes.NewReader([]byte("{\"message\": \"client error\",\"requestId\": \"761761721\"}")))}
107-
return nil, &resp, errors.New(errMessage)
108-
})
109-
expectedErr := "client error (400): API call error [request-id: 761761721] client error; client error"
110-
assert.Contains(t, actualErr.Error(), expectedErr)
111-
errortest.AssertError(t, actualErr, commonerrors.ErrInvalid)
112-
})
119+
t.Run("api call not successful", func(t *testing.T) {
120+
errMessage := "client error"
121+
parentCtx := context.Background()
122+
_, actualErr := test.Func(parentCtx, errMessage,
123+
func(ctx context.Context) (*client.ErrorResponse, *_http.Response, error) {
124+
resp := _http.Response{StatusCode: 400, Body: io.NopCloser(bytes.NewReader([]byte("{\"message\": \"client error\",\"requestId\": \"761761721\"}")))}
125+
return nil, &resp, errors.New(errMessage)
126+
})
127+
expectedErr := "client error (400): API call error [request-id: 761761721] client error; client error"
128+
assert.Contains(t, actualErr.Error(), expectedErr)
129+
errortest.AssertError(t, actualErr, commonerrors.ErrInvalid)
130+
})
113131

114-
t.Run("api call successful, marshalling failed due to missing required field in response", func(t *testing.T) {
115-
expectedErrorMessage := client.ErrorResponse{
116-
Fields: []client.FieldObject{{
117-
FieldName: faker.Name(),
118-
FieldPath: field.ToOptionalString(faker.Name()),
119-
Message: faker.Sentence(),
120-
}},
121-
HttpStatusCode: 200,
122-
Message: faker.Sentence(),
123-
RequestId: faker.UUIDDigit(),
124-
}
125-
response, err := expectedErrorMessage.ToMap()
126-
require.NoError(t, err)
127-
delete(response, "message")
132+
t.Run("api call successful, marshalling failed due to missing required field in response", func(t *testing.T) {
133+
expectedErrorMessage := client.ErrorResponse{
134+
Fields: []client.FieldObject{{
135+
FieldName: faker.Name(),
136+
FieldPath: field.ToOptionalString(faker.Name()),
137+
Message: faker.Sentence(),
138+
}},
139+
HttpStatusCode: 200,
140+
Message: faker.Sentence(),
141+
RequestId: faker.UUIDDigit(),
142+
}
143+
response, err := expectedErrorMessage.ToMap()
144+
require.NoError(t, err)
145+
delete(response, "message")
128146

129-
reducedResponse, err := json.Marshal(response)
130-
require.NoError(t, err)
147+
reducedResponse, err := json.Marshal(response)
148+
require.NoError(t, err)
131149

132-
errorResponse := client.ErrorResponse{}
133-
errM := errorResponse.UnmarshalJSON(reducedResponse)
134-
require.Error(t, errM)
150+
errorResponse := client.ErrorResponse{}
151+
errM := errorResponse.UnmarshalJSON(reducedResponse)
152+
require.Error(t, errM)
135153

136-
parentCtx := context.Background()
137-
_, err = CallAndCheckSuccess[client.ErrorResponse](parentCtx, "test",
138-
func(ctx context.Context) (*client.ErrorResponse, *_http.Response, error) {
139-
return &errorResponse, &_http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(reducedResponse))}, errM
140-
})
141-
require.Error(t, err)
142-
errortest.AssertError(t, err, commonerrors.ErrMarshalling)
143-
})
154+
parentCtx := context.Background()
155+
_, err = test.Func(parentCtx, "test",
156+
func(ctx context.Context) (*client.ErrorResponse, *_http.Response, error) {
157+
return &errorResponse, &_http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(reducedResponse))}, errM
158+
})
159+
require.Error(t, err)
160+
errortest.AssertError(t, err, commonerrors.ErrMarshalling)
161+
})
144162

145-
t.Run("api call successful, strict marshalling failed but recovery", func(t *testing.T) {
146-
expectedErrorMessage := client.ErrorResponse{
147-
Fields: []client.FieldObject{{
148-
FieldName: faker.Name(),
149-
FieldPath: field.ToOptionalString(faker.Name()),
150-
Message: faker.Sentence(),
151-
}},
152-
HttpStatusCode: 200,
153-
Message: faker.Sentence(),
154-
RequestId: faker.UUIDDigit(),
155-
}
156-
response, err := expectedErrorMessage.ToMap()
157-
require.NoError(t, err)
158-
response[faker.Word()] = faker.Name()
159-
response[faker.Word()] = faker.Sentence()
160-
response[faker.Word()] = faker.Paragraph()
161-
response[faker.Word()] = faker.UUIDDigit()
162-
extendedResponse, err := json.Marshal(response)
163-
require.NoError(t, err)
164-
errMessage := "no error"
165-
parentCtx := context.Background()
166-
result, err := CallAndCheckSuccess[client.ErrorResponse](parentCtx, errMessage,
167-
func(ctx context.Context) (*client.ErrorResponse, *_http.Response, error) {
168-
return &client.ErrorResponse{}, &_http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(extendedResponse))}, errors.New(errMessage)
169-
})
170-
require.NoError(t, err)
171-
assert.Equal(t, expectedErrorMessage, *result)
172-
})
163+
t.Run("api call successful, strict marshalling failed but recovery", func(t *testing.T) {
164+
expectedErrorMessage := client.ErrorResponse{
165+
Fields: []client.FieldObject{{
166+
FieldName: faker.Name(),
167+
FieldPath: field.ToOptionalString(faker.Name()),
168+
Message: faker.Sentence(),
169+
}},
170+
HttpStatusCode: 200,
171+
Message: faker.Sentence(),
172+
RequestId: faker.UUIDDigit(),
173+
}
174+
response, err := expectedErrorMessage.ToMap()
175+
require.NoError(t, err)
176+
response[faker.Word()] = faker.Name()
177+
response[faker.Word()] = faker.Sentence()
178+
response[faker.Word()] = faker.Paragraph()
179+
response[faker.Word()] = faker.UUIDDigit()
180+
extendedResponse, err := json.Marshal(response)
181+
require.NoError(t, err)
182+
errMessage := "no error"
183+
parentCtx := context.Background()
184+
result, err := test.Func(parentCtx, errMessage,
185+
func(ctx context.Context) (*client.ErrorResponse, *_http.Response, error) {
186+
return &client.ErrorResponse{}, &_http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(extendedResponse))}, errors.New(errMessage)
187+
})
188+
require.NoError(t, err)
189+
assert.Equal(t, expectedErrorMessage, *result)
190+
})
173191

174-
t.Run("api call successful, empty response", func(t *testing.T) {
175-
errMessage := "no error"
176-
parentCtx := context.Background()
177-
_, err := CallAndCheckSuccess(parentCtx, errMessage,
178-
func(ctx context.Context) (*struct{}, *_http.Response, error) {
179-
return &struct{}{}, &_http.Response{StatusCode: 200}, errors.New(errMessage)
180-
})
181-
errortest.AssertError(t, err, commonerrors.ErrMarshalling)
182-
errortest.AssertErrorDescription(t, err, "API call was successful but an error occurred during response marshalling")
183-
})
192+
t.Run("api call successful, empty response", func(t *testing.T) {
193+
errMessage := "no error"
194+
parentCtx := context.Background()
195+
_, err := test.Func(parentCtx, errMessage,
196+
func(ctx context.Context) (*client.ErrorResponse, *_http.Response, error) {
197+
return client.NewErrorResponseWithDefaults(), &_http.Response{StatusCode: 200}, errors.New(errMessage)
198+
})
199+
errortest.AssertError(t, err, commonerrors.ErrMarshalling)
200+
errortest.AssertErrorDescription(t, err, "API call was successful but an error occurred during response marshalling")
201+
})
202+
203+
t.Run("api call successful, broken response decode", func(t *testing.T) {
204+
errMessage := "no error"
205+
parentCtx := context.Background()
206+
_, err := test.Func(parentCtx, errMessage,
207+
func(ctx context.Context) (*client.ErrorResponse, *_http.Response, error) {
208+
return client.NewErrorResponseWithDefaults(), &_http.Response{StatusCode: 200}, nil
209+
})
210+
errortest.AssertError(t, err, commonerrors.ErrMarshalling)
211+
errortest.AssertErrorDescription(t, err, "unmarshalled response is empty")
212+
})
213+
}
184214

185-
t.Run("api call successful, broken response decode", func(t *testing.T) {
186-
errMessage := "no error"
187-
parentCtx := context.Background()
188-
_, err := CallAndCheckSuccess(parentCtx, errMessage,
189-
func(ctx context.Context) (*struct{}, *_http.Response, error) {
190-
return &struct{}{}, &_http.Response{StatusCode: 200}, nil
191-
})
192-
errortest.AssertError(t, err, commonerrors.ErrMarshalling)
193-
errortest.AssertErrorDescription(t, err, "unmarshalled response is empty")
194-
})
195215
}
196216

197217
func TestGenericCallAndCheckSuccess(t *testing.T) {
198-
t.Run("context cancelled", func(t *testing.T) {
199-
errMessage := "context cancelled"
200-
parentCtx := context.Background()
201-
ctx, cancelCtx := context.WithCancel(parentCtx)
202-
cancelCtx()
203-
_, actualErr := GenericCallAndCheckSuccess(ctx, errMessage,
204-
func(ctx context.Context) (*struct{}, *_http.Response, error) {
205-
return nil, &_http.Response{Body: io.NopCloser(bytes.NewReader(nil))}, errors.New(errMessage)
206-
})
207-
errortest.AssertError(t, actualErr, commonerrors.ErrCancelled)
208-
})
218+
tests := []struct {
219+
Func func(ctx context.Context, errorContext string, apiCallFunc func(ctx context.Context) (any, *_http.Response, error)) (any, error)
220+
}{
221+
{
222+
Func: GenericCallAndCheckSuccess[any],
223+
},
224+
{
225+
Func: func(ctx context.Context, errorContext string, apiCallFunc func(ctx context.Context) (any, *_http.Response, error)) (any, error) {
226+
r, resp, err := GenericCallAndCheckSuccessAndReturnRawResponse[any](ctx, errorContext, apiCallFunc)
227+
if resp != nil && resp.Body != nil {
228+
require.NoError(t, resp.Body.Close())
229+
}
230+
return r, err
231+
},
232+
},
233+
}
234+
for i := range tests {
235+
test := tests[i]
236+
t.Run("context cancelled", func(t *testing.T) {
237+
errMessage := "context cancelled"
238+
parentCtx := context.Background()
239+
ctx, cancelCtx := context.WithCancel(parentCtx)
240+
cancelCtx()
241+
_, actualErr := test.Func(ctx, errMessage,
242+
func(ctx context.Context) (any, *_http.Response, error) {
243+
return nil, &_http.Response{Body: io.NopCloser(bytes.NewReader(nil))}, errors.New(errMessage)
244+
})
245+
errortest.AssertError(t, actualErr, commonerrors.ErrCancelled)
246+
})
209247

210-
t.Run("api call not successful", func(t *testing.T) {
211-
errMessage := "client error"
212-
parentCtx := context.Background()
213-
_, actualErr := GenericCallAndCheckSuccess(parentCtx, errMessage,
214-
func(ctx context.Context) (*struct{}, *_http.Response, error) {
215-
resp := _http.Response{StatusCode: 400, Body: io.NopCloser(bytes.NewReader([]byte("{\"message\": \"client error\",\"requestId\": \"761761721\"}")))}
216-
return nil, &resp, errors.New(errMessage)
217-
})
218-
expectedErr := "client error (400): API call error [request-id: 761761721] client error; client error"
219-
assert.Contains(t, actualErr.Error(), expectedErr)
220-
errortest.AssertError(t, actualErr, commonerrors.ErrInvalid)
221-
})
248+
t.Run("api call not successful", func(t *testing.T) {
249+
errMessage := "client error"
250+
parentCtx := context.Background()
251+
_, actualErr := test.Func(parentCtx, errMessage,
252+
func(ctx context.Context) (any, *_http.Response, error) {
253+
resp := _http.Response{StatusCode: 400, Body: io.NopCloser(bytes.NewReader([]byte("{\"message\": \"client error\",\"requestId\": \"761761721\"}")))}
254+
return nil, &resp, errors.New(errMessage)
255+
})
256+
expectedErr := "client error (400): API call error [request-id: 761761721] client error; client error"
257+
assert.Contains(t, actualErr.Error(), expectedErr)
258+
errortest.AssertError(t, actualErr, commonerrors.ErrInvalid)
259+
})
222260

223-
t.Run("api call successful but error marshalling", func(t *testing.T) {
224-
errMessage := "no error"
225-
parentCtx := context.Background()
226-
_, err := GenericCallAndCheckSuccess(parentCtx, errMessage,
227-
func(ctx context.Context) (any, *_http.Response, error) {
228-
tmp := struct {
229-
test string
230-
}{
231-
test: faker.Word(),
232-
}
233-
return &tmp, &_http.Response{StatusCode: 200}, errors.New(errMessage)
234-
})
235-
require.Error(t, err)
236-
errortest.AssertError(t, err, commonerrors.ErrMarshalling)
237-
})
261+
t.Run("api call successful but error marshalling", func(t *testing.T) {
262+
errMessage := "no error"
263+
parentCtx := context.Background()
264+
_, err := test.Func(parentCtx, errMessage,
265+
func(ctx context.Context) (any, *_http.Response, error) {
266+
tmp := struct {
267+
test string
268+
}{
269+
test: faker.Word(),
270+
}
271+
return &tmp, &_http.Response{StatusCode: 200}, errors.New(errMessage)
272+
})
273+
require.Error(t, err)
274+
errortest.AssertError(t, err, commonerrors.ErrMarshalling)
275+
})
238276

239-
t.Run("api call successful, empty response", func(t *testing.T) {
240-
errMessage := "response error"
241-
parentCtx := context.Background()
242-
_, err := GenericCallAndCheckSuccess(parentCtx, errMessage,
243-
func(ctx context.Context) (*struct{}, *_http.Response, error) {
244-
return &struct{}{}, &_http.Response{StatusCode: 200}, errors.New(errMessage)
245-
})
246-
errortest.AssertError(t, err, commonerrors.ErrMarshalling)
247-
})
277+
t.Run("api call successful, empty response", func(t *testing.T) {
278+
errMessage := "response error"
279+
parentCtx := context.Background()
280+
_, err := test.Func(parentCtx, errMessage,
281+
func(ctx context.Context) (any, *_http.Response, error) {
282+
return &struct{}{}, &_http.Response{StatusCode: 200}, errors.New(errMessage)
283+
})
284+
errortest.AssertError(t, err, commonerrors.ErrMarshalling)
285+
})
248286

249-
t.Run("api call successful, incorrect response", func(t *testing.T) {
250-
parentCtx := context.Background()
251-
_, err := GenericCallAndCheckSuccess(parentCtx, "response error",
252-
func(ctx context.Context) (struct{ Blah string }, *_http.Response, error) {
253-
return struct{ Blah string }{Blah: "fsadsfs"}, &_http.Response{StatusCode: 200}, nil
254-
})
255-
errortest.AssertError(t, err, commonerrors.ErrConflict)
256-
})
287+
t.Run("api call successful, incorrect response", func(t *testing.T) {
288+
parentCtx := context.Background()
289+
_, err := test.Func(parentCtx, "response error",
290+
func(ctx context.Context) (any, *_http.Response, error) {
291+
return struct{ Blah string }{Blah: "fsadsfs"}, &_http.Response{StatusCode: 200}, nil
292+
})
293+
errortest.AssertError(t, err, commonerrors.ErrConflict)
294+
})
295+
}
257296
}

utils/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.25
44

55
require (
66
github.com/ARM-software/embedded-development-services-client/client v1.103.0
7-
github.com/ARM-software/golang-utils/utils v1.142.0
7+
github.com/ARM-software/golang-utils/utils v1.143.0
88
github.com/go-faker/faker/v4 v4.7.0
99
github.com/go-logr/logr v1.4.3
1010
github.com/stretchr/testify v1.11.1

0 commit comments

Comments
 (0)