internal/testutil/request: add ReadJSONResponse utility

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-10-18 22:15:31 +02:00
parent ec83dd46ed
commit 3906199019
3 changed files with 110 additions and 6 deletions

View File

@@ -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)
}

View File

@@ -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: `<html><head><title>Page not found</title></head></html>`,
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)
}
})
}
}

View File

@@ -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)