Skip to content

Commit 28a3f81

Browse files
committed
Add apitest package for testing service handlers
This is a small augmentation to the API framework intended to provide a package for testing service handlers. Service handlers are normal functions and can be invoked as normal functions, but `apitest` provides some extra niceties: * Request structs are validated and an API error is emitted in case they're invalid. * Response structs are validated and an error returned in case they're invalid. We may add more things down the road as they're needed as well. For example, `apitest` might inject a valid looking IP address into context to simulate a more realistic request for purposes that need an IP like rate limiting. (Nothing we're doing needs this yet, so I didn't bother.) Sample usage: endpoint := &testEndpoint{} resp, err := apitest.InvokeHandler(ctx, endpoint.Execute, &testRequest{ReqField: "string"}) require.NoError(t, err)
1 parent 203f3fd commit 28a3f81

2 files changed

Lines changed: 94 additions & 0 deletions

File tree

apitest/apitest.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package apitest
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/riverqueue/apiframe/apierror"
8+
"github.com/riverqueue/apiframe/internal/validate"
9+
)
10+
11+
// InvokeHandler invokes a service handler and returns its results.
12+
//
13+
// Service handlers are normal functions and can be invoked directly, but it's
14+
// preferable to invoke them with this function because a few extra niceties are
15+
// observed that are normally only available from the API framework:
16+
//
17+
// - Incoming request structs are validated and an API error is emitted in case
18+
// they're invalid (any `validate` tags are checked).
19+
// - Outgoing response structs are validated.
20+
//
21+
// Sample invocation:
22+
//
23+
// endpoint := &testEndpoint{}
24+
// resp, err := apitest.InvokeHandler(ctx, endpoint.Execute, &testRequest{ReqField: "string"})
25+
// require.NoError(t, err)
26+
func InvokeHandler[TReq any, TResp any](ctx context.Context, handler func(context.Context, *TReq) (*TResp, error), req *TReq) (*TResp, error) {
27+
if err := validate.StructCtx(ctx, req); err != nil {
28+
return nil, apierror.NewBadRequest(validate.PublicFacingMessage(err))
29+
}
30+
31+
resp, err := handler(ctx, req)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
if err := validate.StructCtx(ctx, resp); err != nil {
37+
return nil, fmt.Errorf("apitest: error validating response API resource: %w", err)
38+
}
39+
40+
return resp, nil
41+
}

apitest/apitest_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package apitest
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/riverqueue/apiframe/apierror"
10+
)
11+
12+
func TestInvokeHandler(t *testing.T) {
13+
t.Parallel()
14+
15+
ctx := context.Background()
16+
17+
type testRequest struct {
18+
RequiredReqField string `json:"req_field" validate:"required"`
19+
}
20+
type testResponse struct {
21+
RequiredRespField string `json:"resp_field" validate:"required"`
22+
}
23+
24+
handler := func(_ context.Context, req *testRequest) (*testResponse, error) {
25+
return &testResponse{RequiredRespField: "response value"}, nil
26+
}
27+
28+
t.Run("Success", func(t *testing.T) {
29+
t.Parallel()
30+
31+
resp, err := InvokeHandler(ctx, handler, &testRequest{RequiredReqField: "string"})
32+
require.NoError(t, err)
33+
require.Equal(t, &testResponse{RequiredRespField: "response value"}, resp)
34+
})
35+
36+
t.Run("ValidatesRequest", func(t *testing.T) {
37+
t.Parallel()
38+
39+
_, err := InvokeHandler(ctx, handler, &testRequest{RequiredReqField: ""})
40+
require.Equal(t, apierror.NewBadRequestf("Field `req_field` is required."), err)
41+
})
42+
43+
t.Run("ValidatesResponse", func(t *testing.T) {
44+
t.Parallel()
45+
46+
handler := func(_ context.Context, _ *testRequest) (*testResponse, error) {
47+
return &testResponse{RequiredRespField: ""}, nil
48+
}
49+
50+
_, err := InvokeHandler(ctx, handler, &testRequest{RequiredReqField: "string"})
51+
require.EqualError(t, err, "apitest: error validating response API resource: Key: 'testResponse.resp_field' Error:Field validation for 'resp_field' failed on the 'required' tag")
52+
})
53+
}

0 commit comments

Comments
 (0)