From a39a55aeca5ac68299c88539ec84d63128f96792 Mon Sep 17 00:00:00 2001 From: Eric Rutherford Date: Tue, 9 Jun 2020 19:26:15 -0500 Subject: [PATCH 1/4] adding assertions for httpfake requests --- assertions.go | 170 +++++++ assertions_test.go | 416 ++++++++++++++++++ .../simple_get_with_testing_test.go | 43 ++ .../simple_put_with_testing_test.go | 50 +++ httpfake.go | 53 ++- request.go | 50 +++ 6 files changed, 781 insertions(+), 1 deletion(-) create mode 100644 assertions.go create mode 100644 assertions_test.go create mode 100644 functional_tests/simple_get_with_testing_test.go create mode 100644 functional_tests/simple_put_with_testing_test.go diff --git a/assertions.go b/assertions.go new file mode 100644 index 0000000..a2a1efe --- /dev/null +++ b/assertions.go @@ -0,0 +1,170 @@ +package httpfake + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" +) + +const assertErrorTemplate = "assertion error: %s" + +// Assertor provides an interface for setting assertions for http requests +type Assertor interface { + Assert(r *http.Request) error + Log(t testing.TB) + Error(t testing.TB, err error) +} + +// RequiredHeaders provides an Assertor for the presence of the provided http header keys +type RequiredHeaders struct { + Keys []string +} + +// Assert runs the required headers assertion against the provided request +func (h *RequiredHeaders) Assert(r *http.Request) error { + var missingHeaders []string + + for _, key := range h.Keys { + if value := r.Header.Get(key); len(value) == 0 { + missingHeaders = append(missingHeaders, key) + } + } + + if len(missingHeaders) > 0 { + return fmt.Errorf("missing required header(s): %s", strings.Join(missingHeaders, ", ")) + } + + return nil +} + +// Log prints a testing info log for the RequiredHeaders Assertor +func (h *RequiredHeaders) Log(t testing.TB) { + t.Log("Testing request for required headers") +} + +// Error prints a testing error for the RequiredHeaders Assertor +func (h *RequiredHeaders) Error(t testing.TB, err error) { + t.Errorf(assertErrorTemplate, err) +} + +// RequiredHeaderValue provides an Assertor for a header and its expected value +type RequiredHeaderValue struct { + Key string + ExpectedValue string +} + +// Assert runs the required header value assertion against the provided request +func (h *RequiredHeaderValue) Assert(r *http.Request) error { + if value := r.Header.Get(h.Key); value != h.ExpectedValue { + return fmt.Errorf("header %s does not have the expected value; expected %s to equal %s", + h.Key, + value, + h.ExpectedValue) + } + + return nil +} + +// Log prints a testing info log for the RequiredHeaderValue Assertor +func (h *RequiredHeaderValue) Log(t testing.TB) { + t.Logf("Testing request for required header value [%s: %s]", h.Key, h.ExpectedValue) +} + +// Error prints a testing error for the RequiredHeaderValue Assertor +func (h *RequiredHeaderValue) Error(t testing.TB, err error) { + t.Errorf(assertErrorTemplate, err) +} + +// RequiredQueries provides an Assertor for the presence of the provided query parameter keys +type RequiredQueries struct { + Keys []string +} + +// Assert runs the required queries assertion against the provided request +func (q *RequiredQueries) Assert(r *http.Request) error { + queryVals := r.URL.Query() + var missingParams []string + + for _, key := range q.Keys { + if value := queryVals.Get(key); len(value) == 0 { + missingParams = append(missingParams, key) + } + } + if len(missingParams) > 0 { + return fmt.Errorf("missing required query parameter(s): %s", strings.Join(missingParams, ", ")) + } + + return nil +} + +// Log prints a testing info log for the RequiredQueries Assertor +func (q *RequiredQueries) Log(t testing.TB) { + t.Log("Testing request for required query parameters") +} + +// Error prints a testing error for the RequiredQueries Assertor +func (q *RequiredQueries) Error(t testing.TB, err error) { + t.Errorf(assertErrorTemplate, err) +} + +// RequiredQueryValue provides an Assertor for a query parameter and its expected value +type RequiredQueryValue struct { + Key string + ExpectedValue string +} + +// Assert runs the required query value assertion against the provided request +func (q *RequiredQueryValue) Assert(r *http.Request) error { + if value := r.URL.Query().Get(q.Key); value != q.ExpectedValue { + return fmt.Errorf("query %s does not have the expected value; expected %s to equal %s", q.Key, value, q.ExpectedValue) + } + return nil +} + +// Log prints a testing info log for the RequiredQueryValue Assertor +func (q *RequiredQueryValue) Log(t testing.TB) { + t.Logf("Testing request for required query parameter value [%s: %s]", q.Key, q.ExpectedValue) +} + +// Error prints a testing error for the RequiredQueryValue Assertor +func (q *RequiredQueryValue) Error(t testing.TB, err error) { + t.Errorf(assertErrorTemplate, err) +} + +// RequiredBody provides an Assertor for the expected value of the request body +type RequiredBody struct { + ExpectedBody []byte +} + +// Assert runs the required body assertion against the provided request +func (b *RequiredBody) Assert(r *http.Request) error { + if r.Body == nil { + return fmt.Errorf("error reading the request body; the request body is nil") + } + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return fmt.Errorf("error reading the request body: %w", err) + } + + if !bytes.EqualFold(b.ExpectedBody, body) { + return fmt.Errorf("request body does not have the expected value; expected %s to equal %s", + string(body[:]), + string(b.ExpectedBody[:])) + } + + return nil +} + +// Log prints a testing info log for the RequiredBody Assertor +func (b *RequiredBody) Log(t testing.TB) { + t.Log("Testing request for required a required body") +} + +// Error prints a testing error for the RequiredBody Assertor +func (b *RequiredBody) Error(t testing.TB, err error) { + t.Errorf(assertErrorTemplate, err) +} diff --git a/assertions_test.go b/assertions_test.go new file mode 100644 index 0000000..6e12842 --- /dev/null +++ b/assertions_test.go @@ -0,0 +1,416 @@ +// nolint dupl gocyclo +package httpfake + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "net/url" + "testing" +) + +type mockTester struct { + testing.TB + buf *bytes.Buffer +} + +func (t *mockTester) Log(args ...interface{}) { + t.buf.WriteString(fmt.Sprintln(args...)) +} + +func (t *mockTester) Logf(format string, args ...interface{}) { + t.buf.WriteString(fmt.Sprintf(format, args...)) +} + +func (t *mockTester) Errorf(format string, args ...interface{}) { + t.buf.WriteString(fmt.Sprintf(format, args...)) +} + +func TestAssertors_Assert(t *testing.T) { + tests := []struct { + name string + assertor Assertor + requestBuilder func() (*http.Request, error) + expectedErr string + }{ + { + name: "RequiredHeaders should return no error with a proper request", + assertor: &RequiredHeaders{ + Keys: []string{"test-header-1", "test-header-2"}, + }, + requestBuilder: func() (*http.Request, error) { + testReq, err := http.NewRequest(http.MethodPost, "http://fake.url", nil) + if err != nil { + return nil, err + } + + testReq.Header.Set("test-header-1", "mock-value-1") + testReq.Header.Set("test-header-2", "mock-value-2") + + return testReq, nil + }, + expectedErr: "", + }, + { + name: "RequiredHeaders should return an error if a request is missing a required header", + assertor: &RequiredHeaders{ + Keys: []string{"test-header-1", "test-header-2"}, + }, + requestBuilder: func() (*http.Request, error) { + testReq, err := http.NewRequest(http.MethodPost, "http://fake.url", nil) + if err != nil { + return nil, err + } + + testReq.Header.Set("test-header-2", "mock-value-2") + + return testReq, nil + }, + expectedErr: "missing required header(s): test-header-1", + }, + { + name: "RequiredHeaderValue should return no error with a proper request", + assertor: &RequiredHeaderValue{ + Key: "test-header-1", + ExpectedValue: "mock-value-1", + }, + requestBuilder: func() (*http.Request, error) { + testReq, err := http.NewRequest(http.MethodPost, "http://fake.url", nil) + if err != nil { + return nil, err + } + + testReq.Header.Set("test-header-1", "mock-value-1") + + return testReq, nil + }, + expectedErr: "", + }, + { + name: "RequiredHeaderValue should return an error if a request is missing a required header", + assertor: &RequiredHeaderValue{ + Key: "test-header-1", + ExpectedValue: "mock-value-1", + }, + requestBuilder: func() (*http.Request, error) { + testReq, err := http.NewRequest(http.MethodPost, "http://fake.url", nil) + if err != nil { + return nil, err + } + + return testReq, nil + }, + expectedErr: "header test-header-1 does not have the expected value; expected to equal mock-value-1", + }, + { + name: "RequiredQueries should return no error with a proper request", + assertor: &RequiredQueries{ + Keys: []string{"query-1", "query-2"}, + }, + requestBuilder: func() (*http.Request, error) { + u := "http://fake.url?query-1=apples&query-2=oranges" + mockURL, err := url.Parse(u) + if err != nil { + return nil, err + } + + testReq, err := http.NewRequest(http.MethodPost, mockURL.Host, nil) + if err != nil { + return nil, err + } + testReq.URL = mockURL + + return testReq, nil + }, + expectedErr: "", + }, + { + name: "RequiredQueries should return an error if a request is missing the a required query params", + assertor: &RequiredQueries{ + Keys: []string{"query-1", "query-3"}, + }, + requestBuilder: func() (*http.Request, error) { + u := "http://fake.url?query-2=oranges" + mockURL, err := url.Parse(u) + if err != nil { + return nil, err + } + + testReq, err := http.NewRequest(http.MethodPost, mockURL.Host, nil) + if err != nil { + return nil, err + } + testReq.URL = mockURL + + return testReq, nil + }, + expectedErr: "missing required query parameter(s): query-1, query-3", + }, + { + name: "RequiredQueryValue should return no error with a proper request", + assertor: &RequiredQueryValue{ + Key: "query-1", + ExpectedValue: "value-1", + }, + requestBuilder: func() (*http.Request, error) { + u := "http://fake.url?query-1=value-1" + mockURL, err := url.Parse(u) + if err != nil { + return nil, err + } + + testReq, err := http.NewRequest(http.MethodPost, mockURL.Host, nil) + if err != nil { + return nil, err + } + testReq.URL = mockURL + + return testReq, nil + }, + expectedErr: "", + }, + { + name: "RequiredQueryValue should return an error if a request is missing the a required query param", + assertor: &RequiredQueryValue{ + Key: "query-1", + ExpectedValue: "apples", + }, + requestBuilder: func() (*http.Request, error) { + u := "http://fake.url" + mockURL, err := url.Parse(u) + if err != nil { + return nil, err + } + + testReq, err := http.NewRequest(http.MethodPost, mockURL.Host, nil) + if err != nil { + return nil, err + } + testReq.URL = mockURL + + return testReq, nil + }, + expectedErr: "query query-1 does not have the expected value; expected to equal apples", + }, + { + name: "RequiredQueryValue should return an error if a request has an incorrect query param value", + assertor: &RequiredQueryValue{ + Key: "query-1", + ExpectedValue: "apples", + }, + requestBuilder: func() (*http.Request, error) { + u := "http://fake.url?query-1=oranges" + mockURL, err := url.Parse(u) + if err != nil { + return nil, err + } + + testReq, err := http.NewRequest(http.MethodPost, mockURL.Host, nil) + if err != nil { + return nil, err + } + testReq.URL = mockURL + + return testReq, nil + }, + expectedErr: "query query-1 does not have the expected value; expected oranges to equal apples", + }, + { + name: "RequiredBody should return no error with a proper request", + assertor: &RequiredBody{ + ExpectedBody: []byte(`{"testObj": {"data": {"fakeData": "testdata"}}}`), + }, + requestBuilder: func() (*http.Request, error) { + reader := bytes.NewBuffer([]byte(`{"testObj": {"data": {"fakeData": "testdata"}}}`)) + + testReq, err := http.NewRequest(http.MethodPost, "http://fake.url", reader) + if err != nil { + return nil, err + } + + return testReq, nil + }, + expectedErr: "", + }, + { + name: "RequiredBody should return an error if the body is not what's expected", + assertor: &RequiredBody{ + ExpectedBody: []byte(`{"testObj": {"data": {"fakeData": "testdata"}}}`), + }, + requestBuilder: func() (*http.Request, error) { + reader := bytes.NewBuffer([]byte(`{"testObj": {"data": {"badData": "bad"}}}`)) + + testReq, err := http.NewRequest(http.MethodPost, "http://fake.url", reader) + if err != nil { + return nil, err + } + + return testReq, nil + }, + expectedErr: "request body does not have the expected value; expected {\"testObj\": {\"data\": {\"badData\": \"bad\"}}} to equal {\"testObj\": {\"data\": {\"fakeData\": \"testdata\"}}}", + }, + { + name: "RequiredBody should handle a nil body without panic", + assertor: &RequiredBody{ + ExpectedBody: []byte(`{"testObj": {"data": {"fakeData": "testdata"}}}`), + }, + requestBuilder: func() (*http.Request, error) { + + testReq, err := http.NewRequest(http.MethodPost, "http://fake.url", nil) + if err != nil { + return nil, err + } + + return testReq, nil + }, + expectedErr: "error reading the request body; the request body is nil", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testReq, err := tt.requestBuilder() + if err != nil { + t.Fatalf("error setting up fake request: %#v", err) + } + + err = tt.assertor.Assert(testReq) + if len(tt.expectedErr) > 0 { + if err == nil { + t.Errorf("Expected error %s but err was nil", tt.expectedErr) + return + } + + if err.Error() != tt.expectedErr { + t.Errorf("Assert() error = %v, expected error %s", err, tt.expectedErr) + } + return + } + + if err != nil { + t.Errorf("Unexpected error = %v", err) + } + }) + } +} + +func TestAssertors_Log(t *testing.T) { + tests := []struct { + name string + mockTester *mockTester + assertor Assertor + expected string + }{ + { + name: "RequiredHeaders Log should log the expected output when called", + mockTester: &mockTester{ + buf: &bytes.Buffer{}, + }, + assertor: &RequiredHeaders{}, + expected: "Testing request for required headers\n", + }, + { + name: "RequiredHeaderValue Log should log the expected output when called", + mockTester: &mockTester{ + buf: &bytes.Buffer{}, + }, + assertor: &RequiredHeaderValue{Key: "test-key", ExpectedValue: "test-value"}, + expected: "Testing request for required header value [test-key: test-value]", + }, + { + name: "RequiredQueries Log should log the expected output when called", + mockTester: &mockTester{ + buf: &bytes.Buffer{}, + }, + assertor: &RequiredQueries{}, + expected: "Testing request for required query parameters\n", + }, + { + name: "RequiredQueryValue Log should log the expected output when called", + mockTester: &mockTester{ + buf: &bytes.Buffer{}, + }, + assertor: &RequiredQueryValue{Key: "test-key", ExpectedValue: "test-value"}, + expected: "Testing request for required query parameter value [test-key: test-value]", + }, + { + name: "RequiredBody Log should log the expected output when called", + mockTester: &mockTester{ + buf: &bytes.Buffer{}, + }, + assertor: &RequiredBody{}, + expected: "Testing request for required a required body\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertor.Log(tt.mockTester) + + result := tt.mockTester.buf.String() + if result != tt.expected { + t.Errorf("Expected Log %s, actual %#v", tt.expected, result) + } + }) + } +} + +func TestAssertors_Error(t *testing.T) { + tests := []struct { + name string + mockTester *mockTester + assertor Assertor + expected string + }{ + { + name: "RequiredHeaders Log should log the expected output when called", + mockTester: &mockTester{ + buf: &bytes.Buffer{}, + }, + assertor: &RequiredHeaders{}, + expected: "assertion error: test error", + }, + { + name: "RequiredHeaderValue Log should log the expected output when called", + mockTester: &mockTester{ + buf: &bytes.Buffer{}, + }, + assertor: &RequiredHeaderValue{Key: "test-key", ExpectedValue: "test-value"}, + expected: "assertion error: test error", + }, + { + name: "RequiredQueries Log should log the expected output when called", + mockTester: &mockTester{ + buf: &bytes.Buffer{}, + }, + assertor: &RequiredQueries{}, + expected: "assertion error: test error", + }, + { + name: "RequiredQueryValue Log should log the expected output when called", + mockTester: &mockTester{ + buf: &bytes.Buffer{}, + }, + assertor: &RequiredQueryValue{Key: "test-key", ExpectedValue: "test-value"}, + expected: "assertion error: test error", + }, + { + name: "RequiredBody Log should log the expected output when called", + mockTester: &mockTester{ + buf: &bytes.Buffer{}, + }, + assertor: &RequiredBody{}, + expected: "assertion error: test error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testErr := errors.New("test error") + tt.assertor.Error(tt.mockTester, testErr) + + result := tt.mockTester.buf.String() + if result != tt.expected { + t.Errorf("Expected Error %s, actual %#v", tt.expected, result) + } + }) + } +} diff --git a/functional_tests/simple_get_with_testing_test.go b/functional_tests/simple_get_with_testing_test.go new file mode 100644 index 0000000..155b1fb --- /dev/null +++ b/functional_tests/simple_get_with_testing_test.go @@ -0,0 +1,43 @@ +// nolint dupl +package functional_tests + +import ( + "io/ioutil" + "net/http" + "testing" + + "github.com/maxcnunes/httpfake" +) + +// TestSimpleGet tests a fake server handling a GET request +func TestSimpleGetWithTesting(t *testing.T) { + fakeService := httpfake.New(httpfake.WithTesting(t)) + defer fakeService.Close() + + // register a handler for our fake service + fakeService.NewHandler(). + Get("/users?movie=dreamers"). + AssertQueryValue("movie", "dreamers"). + Reply(200). + BodyString(`[{"username": "dreamer","movie": "dreamers"}]`) + + res, err := http.Get(fakeService.ResolveURL("/users?movie=dreamers")) + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() // nolint errcheck + + // Check the status code is what we expect + if status := res.StatusCode; status != 200 { + t.Errorf("request returned wrong status code: got %v want %v", + status, 200) + } + + // Check the response body is what we expect + expected := `[{"username": "dreamer","movie": "dreamers"}]` + body, _ := ioutil.ReadAll(res.Body) + if bodyString := string(body); bodyString != expected { + t.Errorf("request returned unexpected body: got %v want %v", + bodyString, expected) + } +} diff --git a/functional_tests/simple_put_with_testing_test.go b/functional_tests/simple_put_with_testing_test.go new file mode 100644 index 0000000..2b970ea --- /dev/null +++ b/functional_tests/simple_put_with_testing_test.go @@ -0,0 +1,50 @@ +// nolint dupl +package functional_tests + +import ( + "bytes" + "io/ioutil" + "net/http" + "testing" + + "github.com/maxcnunes/httpfake" +) + +// TestSimplePut tests a fake server handling a PUT request +func TestSimplePutWithTesting(t *testing.T) { + fakeService := httpfake.New(httpfake.WithTesting(t)) + defer fakeService.Close() + + // register a handler for our fake service + fakeService.NewHandler(). + Put("/users/1&"). + AssertBody([]byte(`{"username": "dreamer"}`)). + Reply(200). + BodyString(`{"id": 1,"username": "dreamer"}`) + + sendBody := bytes.NewBuffer([]byte(`{"username": "dreamer"}`)) + req, err := http.NewRequest("PUT", fakeService.ResolveURL("/users/1"), sendBody) + if err != nil { + t.Fatal(err) + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() // nolint errcheck + + // Check the status code is what we expect + if status := res.StatusCode; status != 200 { + t.Errorf("request returned wrong status code: got %v want %v", + status, 200) + } + + // Check the response body is what we expect + expected := `{"id": 1,"username": "dreamer"}` + body, _ := ioutil.ReadAll(res.Body) + if bodyString := string(body); bodyString != expected { + t.Errorf("request returned unexpected body: got %v want %v", + bodyString, expected) + } +} diff --git a/httpfake.go b/httpfake.go index eef9a29..fbbd648 100644 --- a/httpfake.go +++ b/httpfake.go @@ -11,21 +11,46 @@ import ( "net/http/httptest" netURL "net/url" "strings" + "testing" ) // HTTPFake is the root struct for the fake server type HTTPFake struct { Server *httptest.Server RequestHandlers []*Request + t *testing.T +} + +// ServerOption provides a functional signature for providing configuration options to the fake server +type ServerOption func(opts *ServerOptions) + +// ServerOptions a configuration object for the fake test server +type ServerOptions struct { + t *testing.T +} + +// WithTesting returns a configuration function that allows you to configure the testing object on the fake server. +// The testing object is utilized for assertions set on the request object and will throw a testing error if an +// endpoint is not called. +func WithTesting(t *testing.T) ServerOption { + return func(opts *ServerOptions) { + opts.t = t + } } // New starts a httptest.Server as the fake server // and sets up the initial configuration to this server's request handlers -func New() *HTTPFake { +func New(opts ...ServerOption) *HTTPFake { fake := &HTTPFake{ RequestHandlers: []*Request{}, } + var serverOpts ServerOptions + for _, opt := range opts { + opt(&serverOpts) + } + + fake.t = serverOpts.t fake.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rh, err := fake.findHandler(r) if err != nil { @@ -47,6 +72,17 @@ func New() *HTTPFake { return } + rh.called++ + + if rh.assertions != nil { + if fake.t == nil { + errMsg := fmt.Sprintf("setup error: \"WithTesting\" is required when assertions are set") + panic(errMsg) + } + + rh.runAssertions(fake.t, r) + } + if rh.CustomHandle != nil { rh.CustomHandle(w, r, rh) return @@ -77,6 +113,21 @@ func (f *HTTPFake) Reset() *HTTPFake { return f } +// Close shuts down the HTTP Test server, this will block until all outstanding requests on the server have completed. +// If the WithTesting option was specified when setting up the server Close will assert that each http handler +// specified for this server was called +func (f *HTTPFake) Close() { + defer f.Server.Close() + + if f.t != nil { + for _, reqHandler := range f.RequestHandlers { + if reqHandler.called == 0 { + f.t.Errorf("httpfake: request handler was specified but not called %s", reqHandler.URL.Path) + } + } + } +} + func (f *HTTPFake) findHandler(r *http.Request) (*Request, error) { founds := []*Request{} url := r.URL.String() diff --git a/request.go b/request.go index 91e88d2..3bf64f5 100644 --- a/request.go +++ b/request.go @@ -1,8 +1,10 @@ package httpfake import ( + "net/http" "net/url" "strings" + "testing" ) // Request stores the settings for a request handler @@ -13,6 +15,8 @@ type Request struct { URL *url.URL Response *Response CustomHandle Responder + assertions []Assertor + called int } // NewRequest creates a new Request @@ -20,6 +24,7 @@ func NewRequest() *Request { return &Request{ URL: &url.URL{}, Response: NewResponse(), + called: 0, } } @@ -72,3 +77,48 @@ func (r *Request) method(method, path string) *Request { r.Method = strings.ToUpper(method) return r } + +func (r *Request) runAssertions(t *testing.T, testReq *http.Request) { + for _, assertor := range r.assertions { + assertor.Log(t) + if err := assertor.Assert(testReq); err != nil { + assertor.Error(t, err) + } + } +} + +// AssertQueries will assert that the provided query parameters are present in the requests to this handler +func (r *Request) AssertQueries(key ...string) *Request { + r.assertions = append(r.assertions, &RequiredQueries{Keys: key}) + return r +} + +// AssertQueryValue will assert that the provided query parameter and value are present in the requests to this handler +func (r *Request) AssertQueryValue(key, value string) *Request { + r.assertions = append(r.assertions, &RequiredQueryValue{Key: key, ExpectedValue: value}) + return r +} + +// AssertHeaders will assert that the provided header keys are present in the requests to this handler +func (r *Request) AssertHeaders(keys ...string) *Request { + r.assertions = append(r.assertions, &RequiredHeaders{Keys: keys}) + return r +} + +// AssertHeaderValue will assert that the provided header key and value are present in the requests to this handler +func (r *Request) AssertHeaderValue(key, value string) *Request { + r.assertions = append(r.assertions, &RequiredHeaderValue{Key: key, ExpectedValue: value}) + return r +} + +// AssertBody will assert that that the provided body matches in the requests to this handler +func (r *Request) AssertBody(body []byte) *Request { + r.assertions = append(r.assertions, &RequiredBody{ExpectedBody: body}) + return r +} + +// AssertCustom will run the provided assertor against requests to this handler +func (r *Request) AssertCustom(assertor Assertor) *Request { + r.assertions = append(r.assertions, assertor) + return r +} From d69a1541ceeaa2d7e65ed0f670f143a7cae59321 Mon Sep 17 00:00:00 2001 From: Eric Rutherford Date: Tue, 9 Jun 2020 21:09:19 -0500 Subject: [PATCH 2/4] do not export internal assertions they're exposed via the assert calls, fixing a broken test --- assertions.go | 70 +++++++-------- assertions_test.go | 88 +++++++++---------- .../simple_put_with_testing_test.go | 2 +- request.go | 10 +-- 4 files changed, 85 insertions(+), 85 deletions(-) diff --git a/assertions.go b/assertions.go index a2a1efe..94bbbfd 100644 --- a/assertions.go +++ b/assertions.go @@ -18,13 +18,13 @@ type Assertor interface { Error(t testing.TB, err error) } -// RequiredHeaders provides an Assertor for the presence of the provided http header keys -type RequiredHeaders struct { +// requiredHeaders provides an Assertor for the presence of the provided http header keys +type requiredHeaders struct { Keys []string } // Assert runs the required headers assertion against the provided request -func (h *RequiredHeaders) Assert(r *http.Request) error { +func (h *requiredHeaders) Assert(r *http.Request) error { var missingHeaders []string for _, key := range h.Keys { @@ -40,24 +40,24 @@ func (h *RequiredHeaders) Assert(r *http.Request) error { return nil } -// Log prints a testing info log for the RequiredHeaders Assertor -func (h *RequiredHeaders) Log(t testing.TB) { +// Log prints a testing info log for the requiredHeaders Assertor +func (h *requiredHeaders) Log(t testing.TB) { t.Log("Testing request for required headers") } -// Error prints a testing error for the RequiredHeaders Assertor -func (h *RequiredHeaders) Error(t testing.TB, err error) { +// Error prints a testing error for the requiredHeaders Assertor +func (h *requiredHeaders) Error(t testing.TB, err error) { t.Errorf(assertErrorTemplate, err) } -// RequiredHeaderValue provides an Assertor for a header and its expected value -type RequiredHeaderValue struct { +// requiredHeaderValue provides an Assertor for a header and its expected value +type requiredHeaderValue struct { Key string ExpectedValue string } // Assert runs the required header value assertion against the provided request -func (h *RequiredHeaderValue) Assert(r *http.Request) error { +func (h *requiredHeaderValue) Assert(r *http.Request) error { if value := r.Header.Get(h.Key); value != h.ExpectedValue { return fmt.Errorf("header %s does not have the expected value; expected %s to equal %s", h.Key, @@ -68,23 +68,23 @@ func (h *RequiredHeaderValue) Assert(r *http.Request) error { return nil } -// Log prints a testing info log for the RequiredHeaderValue Assertor -func (h *RequiredHeaderValue) Log(t testing.TB) { +// Log prints a testing info log for the requiredHeaderValue Assertor +func (h *requiredHeaderValue) Log(t testing.TB) { t.Logf("Testing request for required header value [%s: %s]", h.Key, h.ExpectedValue) } -// Error prints a testing error for the RequiredHeaderValue Assertor -func (h *RequiredHeaderValue) Error(t testing.TB, err error) { +// Error prints a testing error for the requiredHeaderValue Assertor +func (h *requiredHeaderValue) Error(t testing.TB, err error) { t.Errorf(assertErrorTemplate, err) } -// RequiredQueries provides an Assertor for the presence of the provided query parameter keys -type RequiredQueries struct { +// requiredQueries provides an Assertor for the presence of the provided query parameter keys +type requiredQueries struct { Keys []string } // Assert runs the required queries assertion against the provided request -func (q *RequiredQueries) Assert(r *http.Request) error { +func (q *requiredQueries) Assert(r *http.Request) error { queryVals := r.URL.Query() var missingParams []string @@ -100,47 +100,47 @@ func (q *RequiredQueries) Assert(r *http.Request) error { return nil } -// Log prints a testing info log for the RequiredQueries Assertor -func (q *RequiredQueries) Log(t testing.TB) { +// Log prints a testing info log for the requiredQueries Assertor +func (q *requiredQueries) Log(t testing.TB) { t.Log("Testing request for required query parameters") } -// Error prints a testing error for the RequiredQueries Assertor -func (q *RequiredQueries) Error(t testing.TB, err error) { +// Error prints a testing error for the requiredQueries Assertor +func (q *requiredQueries) Error(t testing.TB, err error) { t.Errorf(assertErrorTemplate, err) } -// RequiredQueryValue provides an Assertor for a query parameter and its expected value -type RequiredQueryValue struct { +// requiredQueryValue provides an Assertor for a query parameter and its expected value +type requiredQueryValue struct { Key string ExpectedValue string } // Assert runs the required query value assertion against the provided request -func (q *RequiredQueryValue) Assert(r *http.Request) error { +func (q *requiredQueryValue) Assert(r *http.Request) error { if value := r.URL.Query().Get(q.Key); value != q.ExpectedValue { return fmt.Errorf("query %s does not have the expected value; expected %s to equal %s", q.Key, value, q.ExpectedValue) } return nil } -// Log prints a testing info log for the RequiredQueryValue Assertor -func (q *RequiredQueryValue) Log(t testing.TB) { +// Log prints a testing info log for the requiredQueryValue Assertor +func (q *requiredQueryValue) Log(t testing.TB) { t.Logf("Testing request for required query parameter value [%s: %s]", q.Key, q.ExpectedValue) } -// Error prints a testing error for the RequiredQueryValue Assertor -func (q *RequiredQueryValue) Error(t testing.TB, err error) { +// Error prints a testing error for the requiredQueryValue Assertor +func (q *requiredQueryValue) Error(t testing.TB, err error) { t.Errorf(assertErrorTemplate, err) } -// RequiredBody provides an Assertor for the expected value of the request body -type RequiredBody struct { +// requiredBody provides an Assertor for the expected value of the request body +type requiredBody struct { ExpectedBody []byte } // Assert runs the required body assertion against the provided request -func (b *RequiredBody) Assert(r *http.Request) error { +func (b *requiredBody) Assert(r *http.Request) error { if r.Body == nil { return fmt.Errorf("error reading the request body; the request body is nil") } @@ -159,12 +159,12 @@ func (b *RequiredBody) Assert(r *http.Request) error { return nil } -// Log prints a testing info log for the RequiredBody Assertor -func (b *RequiredBody) Log(t testing.TB) { +// Log prints a testing info log for the requiredBody Assertor +func (b *requiredBody) Log(t testing.TB) { t.Log("Testing request for required a required body") } -// Error prints a testing error for the RequiredBody Assertor -func (b *RequiredBody) Error(t testing.TB, err error) { +// Error prints a testing error for the requiredBody Assertor +func (b *requiredBody) Error(t testing.TB, err error) { t.Errorf(assertErrorTemplate, err) } diff --git a/assertions_test.go b/assertions_test.go index 6e12842..7a23e84 100644 --- a/assertions_test.go +++ b/assertions_test.go @@ -35,8 +35,8 @@ func TestAssertors_Assert(t *testing.T) { expectedErr string }{ { - name: "RequiredHeaders should return no error with a proper request", - assertor: &RequiredHeaders{ + name: "requiredHeaders should return no error with a proper request", + assertor: &requiredHeaders{ Keys: []string{"test-header-1", "test-header-2"}, }, requestBuilder: func() (*http.Request, error) { @@ -53,8 +53,8 @@ func TestAssertors_Assert(t *testing.T) { expectedErr: "", }, { - name: "RequiredHeaders should return an error if a request is missing a required header", - assertor: &RequiredHeaders{ + name: "requiredHeaders should return an error if a request is missing a required header", + assertor: &requiredHeaders{ Keys: []string{"test-header-1", "test-header-2"}, }, requestBuilder: func() (*http.Request, error) { @@ -70,8 +70,8 @@ func TestAssertors_Assert(t *testing.T) { expectedErr: "missing required header(s): test-header-1", }, { - name: "RequiredHeaderValue should return no error with a proper request", - assertor: &RequiredHeaderValue{ + name: "requiredHeaderValue should return no error with a proper request", + assertor: &requiredHeaderValue{ Key: "test-header-1", ExpectedValue: "mock-value-1", }, @@ -88,8 +88,8 @@ func TestAssertors_Assert(t *testing.T) { expectedErr: "", }, { - name: "RequiredHeaderValue should return an error if a request is missing a required header", - assertor: &RequiredHeaderValue{ + name: "requiredHeaderValue should return an error if a request is missing a required header", + assertor: &requiredHeaderValue{ Key: "test-header-1", ExpectedValue: "mock-value-1", }, @@ -104,8 +104,8 @@ func TestAssertors_Assert(t *testing.T) { expectedErr: "header test-header-1 does not have the expected value; expected to equal mock-value-1", }, { - name: "RequiredQueries should return no error with a proper request", - assertor: &RequiredQueries{ + name: "requiredQueries should return no error with a proper request", + assertor: &requiredQueries{ Keys: []string{"query-1", "query-2"}, }, requestBuilder: func() (*http.Request, error) { @@ -126,8 +126,8 @@ func TestAssertors_Assert(t *testing.T) { expectedErr: "", }, { - name: "RequiredQueries should return an error if a request is missing the a required query params", - assertor: &RequiredQueries{ + name: "requiredQueries should return an error if a request is missing the a required query params", + assertor: &requiredQueries{ Keys: []string{"query-1", "query-3"}, }, requestBuilder: func() (*http.Request, error) { @@ -148,8 +148,8 @@ func TestAssertors_Assert(t *testing.T) { expectedErr: "missing required query parameter(s): query-1, query-3", }, { - name: "RequiredQueryValue should return no error with a proper request", - assertor: &RequiredQueryValue{ + name: "requiredQueryValue should return no error with a proper request", + assertor: &requiredQueryValue{ Key: "query-1", ExpectedValue: "value-1", }, @@ -171,8 +171,8 @@ func TestAssertors_Assert(t *testing.T) { expectedErr: "", }, { - name: "RequiredQueryValue should return an error if a request is missing the a required query param", - assertor: &RequiredQueryValue{ + name: "requiredQueryValue should return an error if a request is missing the a required query param", + assertor: &requiredQueryValue{ Key: "query-1", ExpectedValue: "apples", }, @@ -194,8 +194,8 @@ func TestAssertors_Assert(t *testing.T) { expectedErr: "query query-1 does not have the expected value; expected to equal apples", }, { - name: "RequiredQueryValue should return an error if a request has an incorrect query param value", - assertor: &RequiredQueryValue{ + name: "requiredQueryValue should return an error if a request has an incorrect query param value", + assertor: &requiredQueryValue{ Key: "query-1", ExpectedValue: "apples", }, @@ -217,8 +217,8 @@ func TestAssertors_Assert(t *testing.T) { expectedErr: "query query-1 does not have the expected value; expected oranges to equal apples", }, { - name: "RequiredBody should return no error with a proper request", - assertor: &RequiredBody{ + name: "requiredBody should return no error with a proper request", + assertor: &requiredBody{ ExpectedBody: []byte(`{"testObj": {"data": {"fakeData": "testdata"}}}`), }, requestBuilder: func() (*http.Request, error) { @@ -234,8 +234,8 @@ func TestAssertors_Assert(t *testing.T) { expectedErr: "", }, { - name: "RequiredBody should return an error if the body is not what's expected", - assertor: &RequiredBody{ + name: "requiredBody should return an error if the body is not what's expected", + assertor: &requiredBody{ ExpectedBody: []byte(`{"testObj": {"data": {"fakeData": "testdata"}}}`), }, requestBuilder: func() (*http.Request, error) { @@ -251,8 +251,8 @@ func TestAssertors_Assert(t *testing.T) { expectedErr: "request body does not have the expected value; expected {\"testObj\": {\"data\": {\"badData\": \"bad\"}}} to equal {\"testObj\": {\"data\": {\"fakeData\": \"testdata\"}}}", }, { - name: "RequiredBody should handle a nil body without panic", - assertor: &RequiredBody{ + name: "requiredBody should handle a nil body without panic", + assertor: &requiredBody{ ExpectedBody: []byte(`{"testObj": {"data": {"fakeData": "testdata"}}}`), }, requestBuilder: func() (*http.Request, error) { @@ -302,43 +302,43 @@ func TestAssertors_Log(t *testing.T) { expected string }{ { - name: "RequiredHeaders Log should log the expected output when called", + name: "requiredHeaders Log should log the expected output when called", mockTester: &mockTester{ buf: &bytes.Buffer{}, }, - assertor: &RequiredHeaders{}, + assertor: &requiredHeaders{}, expected: "Testing request for required headers\n", }, { - name: "RequiredHeaderValue Log should log the expected output when called", + name: "requiredHeaderValue Log should log the expected output when called", mockTester: &mockTester{ buf: &bytes.Buffer{}, }, - assertor: &RequiredHeaderValue{Key: "test-key", ExpectedValue: "test-value"}, + assertor: &requiredHeaderValue{Key: "test-key", ExpectedValue: "test-value"}, expected: "Testing request for required header value [test-key: test-value]", }, { - name: "RequiredQueries Log should log the expected output when called", + name: "requiredQueries Log should log the expected output when called", mockTester: &mockTester{ buf: &bytes.Buffer{}, }, - assertor: &RequiredQueries{}, + assertor: &requiredQueries{}, expected: "Testing request for required query parameters\n", }, { - name: "RequiredQueryValue Log should log the expected output when called", + name: "requiredQueryValue Log should log the expected output when called", mockTester: &mockTester{ buf: &bytes.Buffer{}, }, - assertor: &RequiredQueryValue{Key: "test-key", ExpectedValue: "test-value"}, + assertor: &requiredQueryValue{Key: "test-key", ExpectedValue: "test-value"}, expected: "Testing request for required query parameter value [test-key: test-value]", }, { - name: "RequiredBody Log should log the expected output when called", + name: "requiredBody Log should log the expected output when called", mockTester: &mockTester{ buf: &bytes.Buffer{}, }, - assertor: &RequiredBody{}, + assertor: &requiredBody{}, expected: "Testing request for required a required body\n", }, } @@ -362,43 +362,43 @@ func TestAssertors_Error(t *testing.T) { expected string }{ { - name: "RequiredHeaders Log should log the expected output when called", + name: "requiredHeaders Log should log the expected output when called", mockTester: &mockTester{ buf: &bytes.Buffer{}, }, - assertor: &RequiredHeaders{}, + assertor: &requiredHeaders{}, expected: "assertion error: test error", }, { - name: "RequiredHeaderValue Log should log the expected output when called", + name: "requiredHeaderValue Log should log the expected output when called", mockTester: &mockTester{ buf: &bytes.Buffer{}, }, - assertor: &RequiredHeaderValue{Key: "test-key", ExpectedValue: "test-value"}, + assertor: &requiredHeaderValue{Key: "test-key", ExpectedValue: "test-value"}, expected: "assertion error: test error", }, { - name: "RequiredQueries Log should log the expected output when called", + name: "requiredQueries Log should log the expected output when called", mockTester: &mockTester{ buf: &bytes.Buffer{}, }, - assertor: &RequiredQueries{}, + assertor: &requiredQueries{}, expected: "assertion error: test error", }, { - name: "RequiredQueryValue Log should log the expected output when called", + name: "requiredQueryValue Log should log the expected output when called", mockTester: &mockTester{ buf: &bytes.Buffer{}, }, - assertor: &RequiredQueryValue{Key: "test-key", ExpectedValue: "test-value"}, + assertor: &requiredQueryValue{Key: "test-key", ExpectedValue: "test-value"}, expected: "assertion error: test error", }, { - name: "RequiredBody Log should log the expected output when called", + name: "requiredBody Log should log the expected output when called", mockTester: &mockTester{ buf: &bytes.Buffer{}, }, - assertor: &RequiredBody{}, + assertor: &requiredBody{}, expected: "assertion error: test error", }, } diff --git a/functional_tests/simple_put_with_testing_test.go b/functional_tests/simple_put_with_testing_test.go index 2b970ea..1003b8b 100644 --- a/functional_tests/simple_put_with_testing_test.go +++ b/functional_tests/simple_put_with_testing_test.go @@ -17,7 +17,7 @@ func TestSimplePutWithTesting(t *testing.T) { // register a handler for our fake service fakeService.NewHandler(). - Put("/users/1&"). + Put("/users/1"). AssertBody([]byte(`{"username": "dreamer"}`)). Reply(200). BodyString(`{"id": 1,"username": "dreamer"}`) diff --git a/request.go b/request.go index 3bf64f5..3856948 100644 --- a/request.go +++ b/request.go @@ -89,31 +89,31 @@ func (r *Request) runAssertions(t *testing.T, testReq *http.Request) { // AssertQueries will assert that the provided query parameters are present in the requests to this handler func (r *Request) AssertQueries(key ...string) *Request { - r.assertions = append(r.assertions, &RequiredQueries{Keys: key}) + r.assertions = append(r.assertions, &requiredQueries{Keys: key}) return r } // AssertQueryValue will assert that the provided query parameter and value are present in the requests to this handler func (r *Request) AssertQueryValue(key, value string) *Request { - r.assertions = append(r.assertions, &RequiredQueryValue{Key: key, ExpectedValue: value}) + r.assertions = append(r.assertions, &requiredQueryValue{Key: key, ExpectedValue: value}) return r } // AssertHeaders will assert that the provided header keys are present in the requests to this handler func (r *Request) AssertHeaders(keys ...string) *Request { - r.assertions = append(r.assertions, &RequiredHeaders{Keys: keys}) + r.assertions = append(r.assertions, &requiredHeaders{Keys: keys}) return r } // AssertHeaderValue will assert that the provided header key and value are present in the requests to this handler func (r *Request) AssertHeaderValue(key, value string) *Request { - r.assertions = append(r.assertions, &RequiredHeaderValue{Key: key, ExpectedValue: value}) + r.assertions = append(r.assertions, &requiredHeaderValue{Key: key, ExpectedValue: value}) return r } // AssertBody will assert that that the provided body matches in the requests to this handler func (r *Request) AssertBody(body []byte) *Request { - r.assertions = append(r.assertions, &RequiredBody{ExpectedBody: body}) + r.assertions = append(r.assertions, &requiredBody{ExpectedBody: body}) return r } From 5d39e5ab02a99955629cc85b089f42b188dee75d Mon Sep 17 00:00:00 2001 From: Eric Rutherford Date: Wed, 10 Jun 2020 09:32:38 -0500 Subject: [PATCH 3/4] adding an example test for asserting on headers, fixing comments for some of the existing tests --- .../simple_get_with_testing_test.go | 2 +- .../simple_post_with_testing_test.go | 58 +++++++++++++++++++ .../simple_put_with_testing_test.go | 2 +- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 functional_tests/simple_post_with_testing_test.go diff --git a/functional_tests/simple_get_with_testing_test.go b/functional_tests/simple_get_with_testing_test.go index 155b1fb..996382e 100644 --- a/functional_tests/simple_get_with_testing_test.go +++ b/functional_tests/simple_get_with_testing_test.go @@ -9,7 +9,7 @@ import ( "github.com/maxcnunes/httpfake" ) -// TestSimpleGet tests a fake server handling a GET request +// TestSimpleGetWithTesting tests a fake server handling a GET request func TestSimpleGetWithTesting(t *testing.T) { fakeService := httpfake.New(httpfake.WithTesting(t)) defer fakeService.Close() diff --git a/functional_tests/simple_post_with_testing_test.go b/functional_tests/simple_post_with_testing_test.go new file mode 100644 index 0000000..b8a91fa --- /dev/null +++ b/functional_tests/simple_post_with_testing_test.go @@ -0,0 +1,58 @@ +// nolint dupl +package functional_tests + +import ( + "bytes" + "io/ioutil" + "net/http" + "testing" + "time" + + "github.com/maxcnunes/httpfake" +) + +// TestSimplePostWithTesting tests a fake server handling a POST request +func TestSimplePostWithTesting(t *testing.T) { + fakeService := httpfake.New(httpfake.WithTesting(t)) + defer fakeService.Server.Close() + + // register a handler for our fake service + fakeService.NewHandler(). + Post("/users"). + AssertHeaders("Authorization"). + AssertHeaderValue("Content-Type", "application/json"). + Reply(201). + BodyString(`{"id": 1, "username": "dreamer"}`) + + client := &http.Client{ + Timeout: 5 * time.Second, + } + + sendBody := bytes.NewBuffer([]byte(`{"username": "dreamer"}`)) + req, err := http.NewRequest(http.MethodPost, fakeService.ResolveURL("/users"), sendBody) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Authorization", "Bearer some-token") + req.Header.Set("Content-Type", "application/json") + + res, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() // nolint errcheck + + // Check the status code is what we expect + if status := res.StatusCode; status != 201 { + t.Errorf("request returned wrong status code: got %v want %v", + status, 201) + } + + // Check the response body is what we expect + expected := `{"id": 1, "username": "dreamer"}` + body, _ := ioutil.ReadAll(res.Body) + if bodyString := string(body); bodyString != expected { + t.Errorf("request returned unexpected body: got %v want %v", + bodyString, expected) + } +} diff --git a/functional_tests/simple_put_with_testing_test.go b/functional_tests/simple_put_with_testing_test.go index 1003b8b..79baacd 100644 --- a/functional_tests/simple_put_with_testing_test.go +++ b/functional_tests/simple_put_with_testing_test.go @@ -10,7 +10,7 @@ import ( "github.com/maxcnunes/httpfake" ) -// TestSimplePut tests a fake server handling a PUT request +// TestSimplePutWithTesting tests a fake server handling a PUT request func TestSimplePutWithTesting(t *testing.T) { fakeService := httpfake.New(httpfake.WithTesting(t)) defer fakeService.Close() From 77b01114d421e124f944ae3bfa98cbcf3298d3b8 Mon Sep 17 00:00:00 2001 From: Eric Rutherford Date: Wed, 10 Jun 2020 15:59:19 -0500 Subject: [PATCH 4/4] adding a section to the README file with a brief overview of the supported assertions --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 7d603c3..bb51abc 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,28 @@ See [Releases](https://github.com/maxcnunes/httpfake/releases) for detailed hist See [godoc reference](https://godoc.org/github.com/maxcnunes/httpfake) for detailed API documentation. +## Assertions + +There are built-in methods you can use to make assertions about requests to your HTTP handlers. The currently +supported assertions are: + +* Presence of query parameters +* Query parameter and its expected value +* Presence of HTTP headers +* HTTP header and its expected value +* The expected body of your request + +[WithTesting](https://godoc.org/github.com/maxcnunes/httpfake#WithTesting) **must** be provided as a server +option when creating the test server if intend to set request assertions. Failing to set the option +when using request assertions will result in a panic. + +### Custom Assertions + +You can also provide your own request assertions by creating a type that implements the +[Assertor interface](https://godoc.org/github.com/maxcnunes/httpfake#Assertor). The `Assertor.Log` method will be +called for each assertion before it's processed. The `Assertor.Error` method will only be called if the +`Assertor.Assert` method returns an error. + ## Examples For a full list of examples please check out the [functional_tests folder](/functional_tests).