diff --git a/apitest/apitest.go b/apitest/apitest.go new file mode 100644 index 0000000..45f52a0 --- /dev/null +++ b/apitest/apitest.go @@ -0,0 +1,41 @@ +package apitest + +import ( + "context" + "fmt" + + "github.com/riverqueue/apiframe/apierror" + "github.com/riverqueue/apiframe/internal/validate" +) + +// InvokeHandler invokes a service handler and returns its results. +// +// Service handlers are normal functions and can be invoked directly, but it's +// preferable to invoke them with this function because a few extra niceties are +// observed that are normally only available from the API framework: +// +// - Incoming request structs are validated and an API error is emitted in case +// they're invalid (any `validate` tags are checked). +// - Outgoing response structs are validated. +// +// Sample invocation: +// +// endpoint := &testEndpoint{} +// resp, err := apitest.InvokeHandler(ctx, endpoint.Execute, &testRequest{ReqField: "string"}) +// require.NoError(t, err) +func InvokeHandler[TReq any, TResp any](ctx context.Context, handler func(context.Context, *TReq) (*TResp, error), req *TReq) (*TResp, error) { + if err := validate.StructCtx(ctx, req); err != nil { + return nil, apierror.NewBadRequest(validate.PublicFacingMessage(err)) + } + + resp, err := handler(ctx, req) + if err != nil { + return nil, err + } + + if err := validate.StructCtx(ctx, resp); err != nil { + return nil, fmt.Errorf("apitest: error validating response API resource: %w", err) + } + + return resp, nil +} diff --git a/apitest/apitest_test.go b/apitest/apitest_test.go new file mode 100644 index 0000000..58a7714 --- /dev/null +++ b/apitest/apitest_test.go @@ -0,0 +1,53 @@ +package apitest + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/riverqueue/apiframe/apierror" +) + +func TestInvokeHandler(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + type testRequest struct { + RequiredReqField string `json:"req_field" validate:"required"` + } + type testResponse struct { + RequiredRespField string `json:"resp_field" validate:"required"` + } + + handler := func(_ context.Context, req *testRequest) (*testResponse, error) { + return &testResponse{RequiredRespField: "response value"}, nil + } + + t.Run("Success", func(t *testing.T) { + t.Parallel() + + resp, err := InvokeHandler(ctx, handler, &testRequest{RequiredReqField: "string"}) + require.NoError(t, err) + require.Equal(t, &testResponse{RequiredRespField: "response value"}, resp) + }) + + t.Run("ValidatesRequest", func(t *testing.T) { + t.Parallel() + + _, err := InvokeHandler(ctx, handler, &testRequest{RequiredReqField: ""}) + require.Equal(t, apierror.NewBadRequestf("Field `req_field` is required."), err) + }) + + t.Run("ValidatesResponse", func(t *testing.T) { + t.Parallel() + + handler := func(_ context.Context, _ *testRequest) (*testResponse, error) { + return &testResponse{RequiredRespField: ""}, nil + } + + _, err := InvokeHandler(ctx, handler, &testRequest{RequiredReqField: "string"}) + 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") + }) +}