diff --git a/docs/docs/tutorial/writing-tests.md b/docs/docs/tutorial/writing-tests.md index eba70d65..80cc56dd 100644 --- a/docs/docs/tutorial/writing-tests.md +++ b/docs/docs/tutorial/writing-tests.md @@ -160,6 +160,23 @@ Now you can run your tests! $ go test -cover ``` +You may also need to send requests with a custom [`context.Context`](https://pkg.go.dev/context#Context). For example, you may need to test an authenticated route, or test using some other request-specific values. + +```go +func TestGetGreeting(t *testing.T) { + _, api := humatest.New(t) + + addRoutes(api) + + ctx := context.Background() // define your necessary context + + resp := api.GetCtx(ctx, "/greeting/world") // provide it using the 'Ctx' suffixed methods + if !strings.Contains(resp.Body.String(), "Hello, world!") { + t.Fatalf("Unexpected response: %s", resp.Body.String()) + } +} +``` + ## Review Congratulations! You just learned: diff --git a/humatest/humatest.go b/humatest/humatest.go index 1e2e0a56..fde3ba27 100644 --- a/humatest/humatest.go +++ b/humatest/humatest.go @@ -4,6 +4,7 @@ package humatest import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -41,16 +42,38 @@ func NewAdapter() huma.Adapter { type TestAPI interface { huma.API - // Do a request against the API. Args, if provided, should be string headers - // like `Content-Type: application/json`, an `io.Reader` for the request - // body, or a slice/map/struct which will be serialized to JSON and sent - // as the request body. Anything else will panic. + // DoCtx a request against the API with a custom [context.Context] in the + // [http.Request]. Args, if provided, should be string headers like + // `Content-Type: application/json`, an `io.Reader` for the request body, or a + // slice/map/struct which will be serialized to JSON and sent as the request + // body. Anything else will panic. + DoCtx(ctx context.Context, method, path string, args ...any) *httptest.ResponseRecorder + + // Do a request against the API using [context.Background] in the [http.Request]. + // Args, if provided, should be string headers like `Content-Type: + // application/json`, an `io.Reader` for the request body, or a slice/map/struct + // which will be serialized to JSON and sent as the request body. Anything else + // will panic. Do(method, path string, args ...any) *httptest.ResponseRecorder - // Get performs a GET request against the API. Args, if provided, should be - // string headers like `Content-Type: application/json`, an `io.Reader` - // for the request body, or a slice/map/struct which will be serialized to - // JSON and sent as the request body. Anything else will panic. + // GetCtx performs a GET request against the API with a custom [context.Context] + // in the [http.Request]. Args, if provided, should be string headers like + // `Content-Type: application/json`, an `io.Reader` for the request body, or a + // slice/map/struct which will be serialized to JSON and sent as the request + // body. Anything else will panic. + // + // // Make a GET request + // api.GetCtx(ctx, "/foo") + // + // // Make a GET request with a custom header. + // api.GetCtx(ctx, "/foo", "X-My-Header: my-value") + GetCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder + + // Get performs a GET request against the API using [context.Background] in the + // [http.Request]. Args, if provided, should be string headers like + // `Content-Type: application/json`, an `io.Reader` for the request body, or a + // slice/map/struct which will be serialized to JSON and sent as the request + // body. Anything else will panic. // // // Make a GET request // api.Get("/foo") @@ -59,22 +82,50 @@ type TestAPI interface { // api.Get("/foo", "X-My-Header: my-value") Get(path string, args ...any) *httptest.ResponseRecorder - // Post performs a POST request against the API. Args, if provided, should be - // string headers like `Content-Type: application/json`, an `io.Reader` - // for the request body, or a slice/map/struct which will be serialized to - // JSON and sent as the request body. Anything else will panic. + // PostCtx performs a POST request against the API with a custom + // [context.Context] in the [http.Request]. Args, if provided, should be string + // headers like `Content-Type: application/json`, an `io.Reader` for the request + // body, or a slice/map/struct which will be serialized to JSON and sent as the + // request body. Anything else will panic. + // + // // Make a POST request + // api.PostCtx(ctx, "/foo", bytes.NewReader(`{"foo": "bar"}`)) + // + // // Make a POST request with a custom header. + // api.PostCtx(ctx, "/foo", "X-My-Header: my-value", MyBody{Foo: "bar"}) + PostCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder + + // Post performs a POST request against the API using [context.Background] in the + // [http.Request]. Args, if provided, should be string headers like + // `Content-Type: application/json`, an `io.Reader` for the request body, or a + // slice/map/struct which will be serialized to JSON and sent as the request + // body. Anything else will panic. // // // Make a POST request - // api.Post("/foo", bytes.NewReader(`{"foo": "bar"}`)) + // api.Post("/foo", bytes.NewReader(`{"foo": "bar"}`)) // // // Make a POST request with a custom header. // api.Post("/foo", "X-My-Header: my-value", MyBody{Foo: "bar"}) Post(path string, args ...any) *httptest.ResponseRecorder - // Put performs a PUT request against the API. Args, if provided, should be - // string headers like `Content-Type: application/json`, an `io.Reader` - // for the request body, or a slice/map/struct which will be serialized to - // JSON and sent as the request body. Anything else will panic. + // PutCtx performs a PUT request against the API with a custom [context.Context] + // in the [http.Request]. Args, if provided, should be string headers like + // `Content-Type: application/json`, an `io.Reader` for the request body, or a + // slice/map/struct which will be serialized to JSON and sent as the request + // body. Anything else will panic. + // + // // Make a PUT request + // api.PutCtx(ctx, "/foo", bytes.NewReader(`{"foo": "bar"}`)) + // + // // Make a PUT request with a custom header. + // api.PutCtx(ctx, "/foo", "X-My-Header: my-value", MyBody{Foo: "bar"}) + PutCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder + + // Put performs a PUT request against the API using [context.Background] in the + // [http.Request]. Args, if provided, should be string headers like + // `Content-Type: application/json`, an `io.Reader` for the request body, or a + // slice/map/struct which will be serialized to JSON and sent as the request + // body. Anything else will panic. // // // Make a PUT request // api.Put("/foo", bytes.NewReader(`{"foo": "bar"}`)) @@ -83,10 +134,24 @@ type TestAPI interface { // api.Put("/foo", "X-My-Header: my-value", MyBody{Foo: "bar"}) Put(path string, args ...any) *httptest.ResponseRecorder - // Patch performs a PATCH request against the API. Args, if provided, should - // be string headers like `Content-Type: application/json`, an `io.Reader` - // for the request body, or a slice/map/struct which will be serialized to - // JSON and sent as the request body. Anything else will panic. + // PatchCtx performs a PATCH request against the API with a custom + // [context.Context] in the [http.Request]. Args, if provided, should be string + // headers like `Content-Type: application/json`, an `io.Reader` for the request + // body, or a slice/map/struct which will be serialized to JSON and sent as the + // request body. Anything else will panic. + // + // // Make a PATCH request + // api.PatchCtx(ctx, "/foo", bytes.NewReader(`{"foo": "bar"}`)) + // + // // Make a PATCH request with a custom header. + // api.PatchCtx(ctx, "/foo", "X-My-Header: my-value", MyBody{Foo: "bar"}) + PatchCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder + + // Patch performs a PATCH request against the API using [context.Background] in + // the [http.Request]. Args, if provided, should be string headers like + // `Content-Type: application/json`, an `io.Reader` for the request body, or a + // slice/map/struct which will be serialized to JSON and sent as the request + // body. Anything else will panic. // // // Make a PATCH request // api.Patch("/foo", bytes.NewReader(`{"foo": "bar"}`)) @@ -95,25 +160,45 @@ type TestAPI interface { // api.Patch("/foo", "X-My-Header: my-value", MyBody{Foo: "bar"}) Patch(path string, args ...any) *httptest.ResponseRecorder - // Delete performs a DELETE request against the API. Args, if provided, should - // be string headers like `Content-Type: application/json`, an `io.Reader` - // for the request body, or a slice/map/struct which will be serialized to - // JSON and sent as the request body. Anything else will panic. + // DeleteCtx performs a DELETE request against the API with a custom + // [context.Context] in the [http.Request]. Args, if provided, should be string + // headers like `Content-Type: application/json`, an `io.Reader` for the request + // body, or a slice/map/struct which will be serialized to JSON and sent as the + // request body. Anything else will panic. + // + // // Make a DELETE request + // api.DeleteCtx(ctx, "/foo") + // + // // Make a DELETE request with a custom header. + // api.DeleteCtx(ctx, "/foo", "X-My-Header: my-value") + DeleteCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder + + // Delete performs a DELETE request against the API using [context.Background] in + // the [http.Request]. Args, if provided, should be string headers like + // `Content-Type: application/json`, an `io.Reader` for the request body, or a + // slice/map/struct which will be serialized to JSON and sent as the request + // body. Anything else will panic. // // // Make a DELETE request - // api.Delete("/foo") + // api.Delete("/foo") // // // Make a DELETE request with a custom header. // api.Delete("/foo", "X-My-Header: my-value") Delete(path string, args ...any) *httptest.ResponseRecorder } +var _ TestAPI = &testAPI{} + type testAPI struct { huma.API tb TB } func (a *testAPI) Do(method, path string, args ...any) *httptest.ResponseRecorder { + return a.DoCtx(context.Background(), method, path, args...) +} + +func (a *testAPI) DoCtx(ctx context.Context, method, path string, args ...any) *httptest.ResponseRecorder { a.tb.Helper() var b io.Reader isJSON := false @@ -136,7 +221,7 @@ func (a *testAPI) Do(method, path string, args ...any) *httptest.ResponseRecorde } } - req, _ := http.NewRequest(method, path, b) + req, _ := http.NewRequestWithContext(ctx, method, path, b) req.RequestURI = path req.RemoteAddr = "127.0.0.1:12345" if isJSON { @@ -167,27 +252,52 @@ func (a *testAPI) Do(method, path string, args ...any) *httptest.ResponseRecorde func (a *testAPI) Get(path string, args ...any) *httptest.ResponseRecorder { a.tb.Helper() - return a.Do(http.MethodGet, path, args...) + return a.GetCtx(context.Background(), path, args...) +} + +func (a *testAPI) GetCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder { + a.tb.Helper() + return a.DoCtx(ctx, http.MethodGet, path, args...) } func (a *testAPI) Post(path string, args ...any) *httptest.ResponseRecorder { a.tb.Helper() - return a.Do(http.MethodPost, path, args...) + return a.PostCtx(context.Background(), path, args...) +} + +func (a *testAPI) PostCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder { + a.tb.Helper() + return a.DoCtx(ctx, http.MethodPost, path, args...) } func (a *testAPI) Put(path string, args ...any) *httptest.ResponseRecorder { a.tb.Helper() - return a.Do(http.MethodPut, path, args...) + return a.PutCtx(context.Background(), path, args...) +} + +func (a *testAPI) PutCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder { + a.tb.Helper() + return a.DoCtx(ctx, http.MethodPut, path, args...) } func (a *testAPI) Patch(path string, args ...any) *httptest.ResponseRecorder { a.tb.Helper() - return a.Do(http.MethodPatch, path, args...) + return a.PatchCtx(context.Background(), path, args...) +} + +func (a *testAPI) PatchCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder { + a.tb.Helper() + return a.DoCtx(ctx, http.MethodPatch, path, args...) } func (a *testAPI) Delete(path string, args ...any) *httptest.ResponseRecorder { a.tb.Helper() - return a.Do(http.MethodDelete, path, args...) + return a.DeleteCtx(context.Background(), path, args...) +} + +func (a *testAPI) DeleteCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder { + a.tb.Helper() + return a.DoCtx(ctx, http.MethodDelete, path, args...) } // Wrap returns a `TestAPI` wrapping the given API.