From 39061990193ad3c541682da220f50975336b8af2 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sat, 18 Oct 2025 22:15:31 +0200 Subject: [PATCH] internal/testutil/request: add ReadJSONResponse utility Signed-off-by: Sebastiaan van Stijn --- internal/testutil/request/helpers.go | 33 ++++++++++ internal/testutil/request/helpers_test.go | 77 +++++++++++++++++++++++ internal/testutil/request/request.go | 6 -- 3 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 internal/testutil/request/helpers.go create mode 100644 internal/testutil/request/helpers_test.go diff --git a/internal/testutil/request/helpers.go b/internal/testutil/request/helpers.go new file mode 100644 index 0000000000..7ffca5744d --- /dev/null +++ b/internal/testutil/request/helpers.go @@ -0,0 +1,33 @@ +package request + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "mime" + "net/http" +) + +// ReadBody read the specified ReadCloser content and returns it. +func ReadBody(b io.ReadCloser) ([]byte, error) { + defer func() { _ = b.Close() }() + return io.ReadAll(b) +} + +// ReadJSONResponse reads a JSON response body into the given variable. it +// returns an error for non-jSON responses, or when failing to unmarshal. +func ReadJSONResponse[T any](resp *http.Response, v *T) error { + if resp == nil { + return errors.New("nil *http.Response") + } + defer func() { _ = resp.Body.Close() }() + + mt, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if mt != "application/json" { + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<10)) + return fmt.Errorf("unexpected Content-Type: '%s' (body: %s)", mt, string(raw)) + } + + return json.NewDecoder(resp.Body).Decode(v) +} diff --git a/internal/testutil/request/helpers_test.go b/internal/testutil/request/helpers_test.go new file mode 100644 index 0000000000..1db3705a86 --- /dev/null +++ b/internal/testutil/request/helpers_test.go @@ -0,0 +1,77 @@ +package request_test + +import ( + "bytes" + "io" + "net/http" + "testing" + + "github.com/moby/moby/v2/internal/testutil/request" + "gotest.tools/v3/assert" +) + +func TestReadJSONResponse(t *testing.T) { + type someResponse struct { + Hello string `json:"Hello"` + } + + tests := []struct { + name string + body string + contentType string + expected someResponse + expErr string + }{ + { + name: "valid JSON", + body: `{"hello": "world"}`, + contentType: "application/json", + expected: someResponse{Hello: "world"}, + }, + { + name: "valid JSON, utf-8", + body: `{"hello": "world"}`, + contentType: "application/json; charset=utf-8", + expected: someResponse{Hello: "world"}, + }, + { + name: "malformed JSON", + body: `{"hello": "world"`, + contentType: "application/json", + expErr: "unexpected EOF", + }, + { + name: "non-JSON", + body: `Page not found`, + contentType: "text/html", + expErr: "unexpected Content-Type: 'text/html'", + }, + { + name: "nil response", + expErr: "nil *http.Response", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var v someResponse + + var resp *http.Response + if tc.name != "nil response" { + resp = &http.Response{ + Header: make(http.Header), + Body: io.NopCloser(bytes.NewBufferString(tc.body)), + } + resp.Header.Set("Content-Type", tc.contentType) + } + + err := request.ReadJSONResponse(resp, &v) + if tc.expErr != "" { + assert.ErrorContains(t, err, tc.expErr) + } else { + assert.NilError(t, err) + assert.DeepEqual(t, tc.expected, v) + } + }) + } +} diff --git a/internal/testutil/request/request.go b/internal/testutil/request/request.go index d4af61eba8..dfd225905f 100644 --- a/internal/testutil/request/request.go +++ b/internal/testutil/request/request.go @@ -105,12 +105,6 @@ func Do(ctx context.Context, endpoint string, modifiers ...func(*Options)) (*htt return resp, body, err } -// ReadBody read the specified ReadCloser content and returns it -func ReadBody(b io.ReadCloser) ([]byte, error) { - defer b.Close() - return io.ReadAll(b) -} - // newRequest creates a new http Request to the specified host and endpoint, with the specified request modifiers func newRequest(endpoint string, opts *Options) (*http.Request, error) { hostURL, err := client.ParseHostURL(opts.host)