From fb55abf73052510bcb8d8d1bef1eb6fafbfa8f6b Mon Sep 17 00:00:00 2001 From: George Psarakis Date: Wed, 2 Oct 2024 09:01:16 +0300 Subject: [PATCH] Increase test coverage - Establish testing for httpassert testing.T helpers. - Allow response header setting in mocked requests. --- go.mod | 1 + go.sum | 2 ++ httpassert/debug_test.go | 23 ++++++++++++ httpassert/response.go | 26 ++++++++------ httpassert/response_test.go | 54 ++++++++++++++++++++++++++++ httptesting/client.go | 9 +++++ httptesting/client_test.go | 6 ++-- integration/client_get_test.go | 10 ++---- request.go | 6 ++-- request_test.go | 43 +++++++++++++++++++++++ response_test.go | 64 ++++++++++++++++++++++++++++++++++ 11 files changed, 222 insertions(+), 22 deletions(-) create mode 100644 httpassert/debug_test.go create mode 100644 httpassert/response_test.go create mode 100644 response_test.go diff --git a/go.mod b/go.mod index 11cb683..87b0ba7 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sync v0.8.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ae1d287..1b9649b 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/httpassert/debug_test.go b/httpassert/debug_test.go new file mode 100644 index 0000000..b9be082 --- /dev/null +++ b/httpassert/debug_test.go @@ -0,0 +1,23 @@ +package httpassert + +import ( + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" +) + +func TestPrintJSON(t *testing.T) { + t.Parallel() + + t.Run("fails if the input cannot be marshalled to JSON", func(t *testing.T) { + grp := errgroup.Group{} + stubTest := &testing.T{} + grp.Go(func() error { + PrintJSON(stubTest, func() {}) + return nil + }) + require.NoError(t, grp.Wait()) + require.True(t, stubTest.Failed()) + }) +} diff --git a/httpassert/response.go b/httpassert/response.go index 982a6bc..38e63c6 100644 --- a/httpassert/response.go +++ b/httpassert/response.go @@ -4,6 +4,7 @@ import ( "bytes" "io" "net/http" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -17,16 +18,21 @@ func ResponseEqual(t *testing.T, actual, expected *http.Response) { if expected.Header != nil { assert.Equal(t, expected.Header, actual.Header) } - expectedBody, err := io.ReadAll(expected.Body) - require.NoError(t, err) - expected.Body.Close() - actualBody, err := io.ReadAll(actual.Body) - require.NoError(t, err) - actual.Body.Close() - // Restore the body stream in order to allow multiple assertions - actual.Body = io.NopCloser(bytes.NewBuffer(actualBody)) - assert.JSONEq(t, string(expectedBody), string(actualBody)) - + if expected.Body != nil { + expectedBody, err := io.ReadAll(expected.Body) + require.NoError(t, err) + expected.Body.Close() + actualBody, err := io.ReadAll(actual.Body) + require.NoError(t, err) + actual.Body.Close() + // Restore the body stream in order to allow multiple assertions + actual.Body = io.NopCloser(bytes.NewBuffer(actualBody)) + if strings.HasPrefix(actual.Header.Get("Content-Type"), "application/json") { + assert.JSONEq(t, string(expectedBody), string(actualBody)) + } else { + assert.Equal(t, string(expectedBody), string(actualBody)) + } + } if expected.Request != nil { assert.Equal(t, expected.Request.URL, actual.Request.URL) assert.Equal(t, expected.Request.Method, actual.Request.Method) diff --git a/httpassert/response_test.go b/httpassert/response_test.go new file mode 100644 index 0000000..5cc8bb3 --- /dev/null +++ b/httpassert/response_test.go @@ -0,0 +1,54 @@ +package httpassert + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResponseEqual(t *testing.T) { + type args struct { + t *testing.T + actual *http.Response + expected *http.Response + } + tests := []struct { + name string + args args + want assert.BoolAssertionFunc + }{ + { + name: "status code does not match", + args: args{ + t: &testing.T{}, + actual: &http.Response{ + StatusCode: http.StatusBadRequest, + }, + expected: &http.Response{ + StatusCode: http.StatusOK, + }, + }, + want: assert.True, + }, + { + name: "status code matches", + args: args{ + t: &testing.T{}, + actual: &http.Response{ + StatusCode: http.StatusBadRequest, + }, + expected: &http.Response{ + StatusCode: http.StatusBadRequest, + }, + }, + want: assert.False, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ResponseEqual(tt.args.t, tt.args.actual, tt.args.expected) + tt.want(t, tt.args.t.Failed()) + }) + } +} diff --git a/httptesting/client.go b/httptesting/client.go index fce7557..4bc6eb7 100644 --- a/httptesting/client.go +++ b/httptesting/client.go @@ -126,6 +126,15 @@ func (r *MockRequest) RespondWithJSON(statusCode int, body string) *MockRequest return r } +func (r *MockRequest) RespondWithHeaders(respHeaders map[string]string) *MockRequest { + h := http.Header{} + for k, v := range respHeaders { + h.Set(k, v) + } + r.responder.HeaderSet(h) + return r +} + func (c *Client) NewJSONBodyMatcher(body string) httpmock.MatcherFunc { c.t.Helper() diff --git a/httptesting/client_test.go b/httptesting/client_test.go index 89ea45b..dd4513d 100644 --- a/httptesting/client_test.go +++ b/httptesting/client_test.go @@ -44,13 +44,15 @@ func TestClient_Head(t *testing.T) { c.Client.WithDefaultHeaders(map[string]string{"Content-Type": "application/json"}) c.NewMockRequest(http.MethodHead, requestURL+"?test=1", httpclient.WithHeaders(map[string]string{"Content-Type": "application/json"})). - RespondWithJSON(http.StatusOK, `{"name": "hello", "surname": "world"}`).Register() + RespondWithJSON(http.StatusOK, `{"name": "hello", "surname": "world"}`). + RespondWithHeaders(map[string]string{"Content-Type": "application/json"}). + Register() resp, err := c.Head(context.Background(), requestURL, httpclient.WithQueryParameters(map[string]string{"test": "1"})) require.NoError(t, err) reqHeaders := http.Header{} - reqHeaders.Set("Content-Type", "application/json") + reqHeaders.Set("Accept", "application/json") httpassert.ResponseEqual(t, resp, &http.Response{ StatusCode: http.StatusOK, Request: &http.Request{ diff --git a/integration/client_get_test.go b/integration/client_get_test.go index 7b99fdf..6b26923 100644 --- a/integration/client_get_test.go +++ b/integration/client_get_test.go @@ -2,12 +2,12 @@ package integration import ( "context" - "encoding/json" "testing" "github.com/stretchr/testify/require" "github.com/georgepsarakis/go-httpclient" + "github.com/georgepsarakis/go-httpclient/httpassert" ) func TestClient_Get_JSON(t *testing.T) { @@ -16,7 +16,7 @@ func TestClient_Get_JSON(t *testing.T) { require.NoError(t, err) v := map[string]interface{}{} require.NoError(t, httpclient.DeserializeJSON(resp, &v)) - printJSON(t, v) + httpassert.PrintJSON(t, v) } func githubClient(t *testing.T) *httpclient.Client { @@ -28,12 +28,6 @@ func githubClient(t *testing.T) *httpclient.Client { return c } -func printJSON(t *testing.T, v any) { - b, err := json.MarshalIndent(v, "", " ") - require.NoError(t, err) - t.Log(string(b)) -} - func TestClient_Get_JSON_ContextDeadline(t *testing.T) { c := githubClient(t) diff --git a/request.go b/request.go index 41d456d..09774cf 100644 --- a/request.go +++ b/request.go @@ -70,12 +70,14 @@ func InterceptRequestBody(r *http.Request) ([]byte, error) { if err != nil { return nil, err } - r.Body.Close() + if err := r.Body.Close(); err != nil { + return nil, err + } r.Body = io.NopCloser(bytes.NewReader(body)) return body, nil } -func MustInterceptRequestBody(r *http.Request, body []byte) []byte { +func MustInterceptRequestBody(r *http.Request) []byte { b, err := InterceptRequestBody(r) if err != nil { panic(err) diff --git a/request_test.go b/request_test.go index a90bec4..05b70b9 100644 --- a/request_test.go +++ b/request_test.go @@ -1,10 +1,14 @@ package httpclient import ( + "io" + "net/http" "net/url" + "strings" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestWithQueryParameters(t *testing.T) { @@ -57,3 +61,42 @@ func TestWithQueryParameters(t *testing.T) { }) } } + +func TestMustInterceptRequestBody(t *testing.T) { + require.Panics(t, func() { + MustInterceptRequestBody(&http.Request{Body: failureOnReadReader{}}) + }) + require.Panics(t, func() { + MustInterceptRequestBody(&http.Request{Body: failureOnCloseReader{}}) + }) + + req := &http.Request{Body: io.NopCloser(strings.NewReader("test"))} + require.Equal(t, []byte("test"), MustInterceptRequestBody(req)) + b, err := io.ReadAll(req.Body) + require.NoError(t, err) + require.Equal(t, []byte("test"), b) +} + +type failureOnReadReader struct { + io.ReadCloser +} + +func (f failureOnReadReader) Read(_ []byte) (n int, err error) { + return 0, io.ErrUnexpectedEOF +} + +func (f failureOnReadReader) Close() error { + return nil +} + +type failureOnCloseReader struct { + io.ReadCloser +} + +func (f failureOnCloseReader) Read(_ []byte) (n int, err error) { + return 0, io.EOF +} + +func (f failureOnCloseReader) Close() error { + return io.ErrClosedPipe +} diff --git a/response_test.go b/response_test.go new file mode 100644 index 0000000..2cba782 --- /dev/null +++ b/response_test.go @@ -0,0 +1,64 @@ +package httpclient + +import ( + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDeserializeJSON(t *testing.T) { + type args struct { + resp *http.Response + target any + } + tests := []struct { + name string + args args + wantErrMessage string + want map[string]any + }{ + { + name: "returns error from JSON marshalling", + args: args{ + resp: &http.Response{ + Body: io.NopCloser(strings.NewReader("{")), + }, + target: &map[string]any{}, + }, + wantErrMessage: "unexpected end of JSON input", + }, + { + name: "returns error when not passing a pointer", + args: args{ + resp: &http.Response{ + Body: io.NopCloser(strings.NewReader("{}")), + }, + target: map[string]any{}, + }, + wantErrMessage: "pointer required, got map[string]interface {}", + }, + { + name: "unmarshals the JSON payload to the passed pointer", + args: args{ + resp: &http.Response{ + Body: io.NopCloser(strings.NewReader(`{"hello": "world"}`)), + }, + target: &map[string]any{}, + }, + want: map[string]any{"hello": "world"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := DeserializeJSON(tt.args.resp, tt.args.target) + if tt.wantErrMessage != "" { + assert.ErrorContains(t, err, tt.wantErrMessage) + } else { + assert.Equal(t, tt.want, *tt.args.target.(*map[string]any)) + } + }) + } +}