From c99c5cb92d47189e8c54c0d99016340e539a157f Mon Sep 17 00:00:00 2001 From: Artem Voronkov Date: Tue, 25 Oct 2022 13:48:32 +0700 Subject: [PATCH 1/4] special chars in url path and query --- .../get_path_with_special_chars_test.go | 42 ++++++++++++++++ .../get_query_with_special_chars_test.go | 50 +++++++++++++++++++ httpfake.go | 10 +--- request.go | 7 ++- 4 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 functional_tests/get_path_with_special_chars_test.go create mode 100644 functional_tests/get_query_with_special_chars_test.go diff --git a/functional_tests/get_path_with_special_chars_test.go b/functional_tests/get_path_with_special_chars_test.go new file mode 100644 index 0000000..e4f3076 --- /dev/null +++ b/functional_tests/get_path_with_special_chars_test.go @@ -0,0 +1,42 @@ +// nolint dupl +package functional_tests + +import ( + "io/ioutil" + "net/http" + "testing" + + "github.com/maxcnunes/httpfake" +) + +// TestGetPathWithSpecialChars tests a fake server handling a request with special chars in path and query +func TestGetPathWithSpecialChars(t *testing.T) { + fakeService := httpfake.New() + defer fakeService.Server.Close() + + // register a handler for our fake service + fakeService.NewHandler(). + Get("/user/+79998887766"). + Reply(200). + BodyString(`[{"username": "dreamer"}]`) + + res, err := http.Get(fakeService.ResolveURL("/user/+79998887766")) + 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"}]` + 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/get_query_with_special_chars_test.go b/functional_tests/get_query_with_special_chars_test.go new file mode 100644 index 0000000..3a67483 --- /dev/null +++ b/functional_tests/get_query_with_special_chars_test.go @@ -0,0 +1,50 @@ +// nolint dupl +package functional_tests + +import ( + "io/ioutil" + "net/http" + "net/url" + "testing" + + "github.com/maxcnunes/httpfake" +) + +// TestGetQueryWithSpecialChars tests a fake server handling a request with special chars in path and query +func TestGetQueryWithSpecialChars(t *testing.T) { + fakeService := httpfake.New() + defer fakeService.Server.Close() + + // register a handler for our fake service + fakeService.NewHandler(). + Get("/users?name=" + url.QueryEscape("Tim Burton")). + Reply(200). + BodyString(`[{"username": "dreamer"}]`) + + // register second handler for our fake service + fakeService.NewHandler(). + Get("/users?name=other"). + Reply(201). + BodyString(`[{"username": "other"}]`) + + res, err := http.Get(fakeService.ResolveURL("/users?name=%s", url.QueryEscape("Tim Burton"))) + //res, err := http.Get(fakeService.ResolveURL("/users?name=Tim Burton")) + 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"}]` + 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 a1db898..438c9bf 100644 --- a/httpfake.go +++ b/httpfake.go @@ -9,7 +9,6 @@ import ( "fmt" "net/http" "net/http/httptest" - netURL "net/url" "strings" "testing" ) @@ -139,18 +138,13 @@ func (f *HTTPFake) findHandler(r *http.Request) (*Request, error) { continue } - rhURL, err := netURL.QueryUnescape(rh.URL.String()) - if err != nil { - return nil, err - } - - if rhURL == url { + if rh.URL.String() == url { return rh, nil } // fallback if the income request has query strings // and there is handlers only for the path - if getURLPath(rhURL) == path { + if rh.URL.Path == path { founds = append(founds, rh) } } diff --git a/request.go b/request.go index 1ecf3e5..804c408 100644 --- a/request.go +++ b/request.go @@ -74,7 +74,12 @@ func (r *Request) Reply(status int) *Response { func (r *Request) method(method, path string) *Request { if path != "/" { - r.URL.Path = path + u, err := url.Parse(path) + if err != nil { + panic(err) + } + + r.URL = u } r.Method = strings.ToUpper(method) return r From 52ca0c982ac5c543a04af101657f7aac0c83cdbd Mon Sep 17 00:00:00 2001 From: Artem Voronkov Date: Fri, 8 Sep 2023 14:36:25 +0700 Subject: [PATCH 2/4] Thread safe, go modules and self-sufficient library --- .gitignore | 2 ++ README.md | 18 +++++------ functional_tests/custom_handler_test.go | 2 +- .../get_path_with_special_chars_test.go | 2 +- functional_tests/get_query_string_test.go | 2 +- .../get_query_with_special_chars_test.go | 2 +- .../not_registered_method_test.go | 2 +- functional_tests/not_registered_route_test.go | 2 +- functional_tests/response_add_header_test.go | 2 +- functional_tests/response_set_header_test.go | 2 +- .../response_set_reponse_bodystruct_test.go | 2 +- functional_tests/simple_delete_test.go | 2 +- functional_tests/simple_get_gorutines_test.go | 2 +- functional_tests/simple_get_test.go | 2 +- .../simple_get_with_testing_test.go | 2 +- functional_tests/simple_head_test.go | 2 +- functional_tests/simple_patch_test.go | 2 +- functional_tests/simple_post_test.go | 2 +- .../simple_post_with_testing_test.go | 2 +- functional_tests/simple_put_test.go | 2 +- .../simple_put_with_testing_test.go | 2 +- go.mod | 3 ++ go.sum | 0 httpfake.go | 32 +++++++++++++++---- 24 files changed, 58 insertions(+), 35 deletions(-) create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore index fe8d305..99ba55f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ coverage.out profile.out +.idea +.vscode diff --git a/README.md b/README.md index 186ce12..89f810b 100644 --- a/README.md +++ b/README.md @@ -4,34 +4,34 @@ httpfake [![LICENSE](https://img.shields.io/badge/license-MIT-orange.svg)](LICENSE) -[![Godocs](https://img.shields.io/badge/golang-documentation-blue.svg)](https://godoc.org/github.com/maxcnunes/httpfake) +[![Godocs](https://img.shields.io/badge/golang-documentation-blue.svg)](https://godoc.org/github.com/voronelf/httpfake) [![Build Status](https://travis-ci.org/maxcnunes/httpfake.svg?branch=master)](https://travis-ci.org/maxcnunes/httpfake) [![Coverage Status](https://coveralls.io/repos/github/maxcnunes/httpfake/badge.svg?branch=master)](https://coveralls.io/github/maxcnunes/httpfake?branch=master) -[![Go Report Card](https://goreportcard.com/badge/github.com/maxcnunes/httpfake)](https://goreportcard.com/report/github.com/maxcnunes/httpfake) +[![Go Report Card](https://goreportcard.com/badge/github.com/voronelf/httpfake)](https://goreportcard.com/report/github.com/voronelf/httpfake) httpfake provides is a simple wrapper for [httptest](https://golang.org/pkg/net/http/httptest/) with a handful chainable API for setting up handlers to a fake server. This package is aimed to be used in tests where the original external server must not be reached. Instead is used in its place a fake server which can be configured to handle any request as desired. ## Installation ``` -go get -u github.com/maxcnunes/httpfake +go get -u github.com/voronelf/httpfake ``` or ``` -govendor fetch github.com/maxcnunes/httpfake +govendor fetch github.com/voronelf/httpfake ``` > If possible give preference for using vendor. This way the version is locked up as a dependency in your project. ## Changelog -See [Releases](https://github.com/maxcnunes/httpfake/releases) for detailed history changes. +See [Releases](https://github.com/voronelf/httpfake/releases) for detailed history changes. ## API -See [godoc reference](https://godoc.org/github.com/maxcnunes/httpfake) for detailed API documentation. +See [godoc reference](https://godoc.org/github.com/voronelf/httpfake) for detailed API documentation. ## Assertions @@ -44,15 +44,15 @@ supported assertions are: * 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 +[WithTesting](https://godoc.org/github.com/voronelf/httpfake#WithTesting) **must** be provided as a server option when creating the test server if you 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 assertions by creating a type that implements the -[Assertor interface](https://godoc.org/github.com/maxcnunes/httpfake#Assertor) or utilizing the -[CustomAssertor function type](https://pkg.go.dev/github.com/maxcnunes/httpfake#CustomAssertor). The `Assertor.Log` method will be +[Assertor interface](https://godoc.org/github.com/voronelf/httpfake#Assertor) or utilizing the +[CustomAssertor function type](https://pkg.go.dev/github.com/voronelf/httpfake#CustomAssertor). 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. diff --git a/functional_tests/custom_handler_test.go b/functional_tests/custom_handler_test.go index a67b746..addf71e 100644 --- a/functional_tests/custom_handler_test.go +++ b/functional_tests/custom_handler_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestHandleCustomResponder tests a fake server handling a GET request diff --git a/functional_tests/get_path_with_special_chars_test.go b/functional_tests/get_path_with_special_chars_test.go index e4f3076..45af80d 100644 --- a/functional_tests/get_path_with_special_chars_test.go +++ b/functional_tests/get_path_with_special_chars_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestGetPathWithSpecialChars tests a fake server handling a request with special chars in path and query diff --git a/functional_tests/get_query_string_test.go b/functional_tests/get_query_string_test.go index 96f9d20..5670342 100644 --- a/functional_tests/get_query_string_test.go +++ b/functional_tests/get_query_string_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestGetQueryString tests a fake server handling a GET request with query string diff --git a/functional_tests/get_query_with_special_chars_test.go b/functional_tests/get_query_with_special_chars_test.go index 3a67483..f94733c 100644 --- a/functional_tests/get_query_with_special_chars_test.go +++ b/functional_tests/get_query_with_special_chars_test.go @@ -7,7 +7,7 @@ import ( "net/url" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestGetQueryWithSpecialChars tests a fake server handling a request with special chars in path and query diff --git a/functional_tests/not_registered_method_test.go b/functional_tests/not_registered_method_test.go index b8a001f..186eb7b 100644 --- a/functional_tests/not_registered_method_test.go +++ b/functional_tests/not_registered_method_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestNotRegisteredMethod tests a fake server handling a GET request diff --git a/functional_tests/not_registered_route_test.go b/functional_tests/not_registered_route_test.go index aef3d5a..17456e7 100644 --- a/functional_tests/not_registered_route_test.go +++ b/functional_tests/not_registered_route_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestNotRegisteredRoute tests a fake server handling a GET request diff --git a/functional_tests/response_add_header_test.go b/functional_tests/response_add_header_test.go index 3f652bb..28546b1 100644 --- a/functional_tests/response_add_header_test.go +++ b/functional_tests/response_add_header_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestResponseAddHeader tests a fake server handling a GET request diff --git a/functional_tests/response_set_header_test.go b/functional_tests/response_set_header_test.go index 2382413..087d8af 100644 --- a/functional_tests/response_set_header_test.go +++ b/functional_tests/response_set_header_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestResponseSetHeader tests a fake server handling a GET request diff --git a/functional_tests/response_set_reponse_bodystruct_test.go b/functional_tests/response_set_reponse_bodystruct_test.go index a08faa1..461bebb 100644 --- a/functional_tests/response_set_reponse_bodystruct_test.go +++ b/functional_tests/response_set_reponse_bodystruct_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestResponseBodyStruct tests a fake server handling a GET request diff --git a/functional_tests/simple_delete_test.go b/functional_tests/simple_delete_test.go index d8cf273..460456c 100644 --- a/functional_tests/simple_delete_test.go +++ b/functional_tests/simple_delete_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestSimpleDelete tests a fake server handling a DELETE request diff --git a/functional_tests/simple_get_gorutines_test.go b/functional_tests/simple_get_gorutines_test.go index 272b592..b7e7fbb 100644 --- a/functional_tests/simple_get_gorutines_test.go +++ b/functional_tests/simple_get_gorutines_test.go @@ -7,7 +7,7 @@ import ( "sync" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestSimpleGetWithRutines tests a fake server handling a GET request diff --git a/functional_tests/simple_get_test.go b/functional_tests/simple_get_test.go index c6b0107..1240a53 100644 --- a/functional_tests/simple_get_test.go +++ b/functional_tests/simple_get_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestSimpleGet tests a fake server handling a GET request diff --git a/functional_tests/simple_get_with_testing_test.go b/functional_tests/simple_get_with_testing_test.go index 996382e..8e47a6c 100644 --- a/functional_tests/simple_get_with_testing_test.go +++ b/functional_tests/simple_get_with_testing_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestSimpleGetWithTesting tests a fake server handling a GET request diff --git a/functional_tests/simple_head_test.go b/functional_tests/simple_head_test.go index 73a848f..4d44e1b 100644 --- a/functional_tests/simple_head_test.go +++ b/functional_tests/simple_head_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestSimpleHead tests a fake server handling a HEAD request diff --git a/functional_tests/simple_patch_test.go b/functional_tests/simple_patch_test.go index 7f8e1e0..ae615c2 100644 --- a/functional_tests/simple_patch_test.go +++ b/functional_tests/simple_patch_test.go @@ -7,7 +7,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestSimplePatch tests a fake server handling a PATCH request diff --git a/functional_tests/simple_post_test.go b/functional_tests/simple_post_test.go index 642c49f..e8e44cd 100644 --- a/functional_tests/simple_post_test.go +++ b/functional_tests/simple_post_test.go @@ -7,7 +7,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestSimplePost tests a fake server handling a POST request diff --git a/functional_tests/simple_post_with_testing_test.go b/functional_tests/simple_post_with_testing_test.go index b8a91fa..644f674 100644 --- a/functional_tests/simple_post_with_testing_test.go +++ b/functional_tests/simple_post_with_testing_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestSimplePostWithTesting tests a fake server handling a POST request diff --git a/functional_tests/simple_put_test.go b/functional_tests/simple_put_test.go index 14d35a4..395d453 100644 --- a/functional_tests/simple_put_test.go +++ b/functional_tests/simple_put_test.go @@ -7,7 +7,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestSimplePut tests a fake server handling a PUT request diff --git a/functional_tests/simple_put_with_testing_test.go b/functional_tests/simple_put_with_testing_test.go index 79baacd..5139f2e 100644 --- a/functional_tests/simple_put_with_testing_test.go +++ b/functional_tests/simple_put_with_testing_test.go @@ -7,7 +7,7 @@ import ( "net/http" "testing" - "github.com/maxcnunes/httpfake" + "github.com/voronelf/httpfake" ) // TestSimplePutWithTesting tests a fake server handling a PUT request diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..58d971c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/voronelf/httpfake + +go 1.19 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/httpfake.go b/httpfake.go index 438c9bf..8b40154 100644 --- a/httpfake.go +++ b/httpfake.go @@ -10,13 +10,15 @@ import ( "net/http" "net/http/httptest" "strings" + "sync" "testing" ) // HTTPFake is the root struct for the fake server type HTTPFake struct { Server *httptest.Server - RequestHandlers []*Request + mu sync.RWMutex + requestHandlers []*Request t testing.TB } @@ -41,7 +43,7 @@ func WithTesting(t testing.TB) ServerOption { // and sets up the initial configuration to this server's request handlers func New(opts ...ServerOption) *HTTPFake { fake := &HTTPFake{ - RequestHandlers: []*Request{}, + requestHandlers: []*Request{}, } var serverOpts ServerOptions @@ -63,9 +65,13 @@ func New(opts ...ServerOption) *HTTPFake { "not found request handler for [%s: %s]; registered handlers are:\n", r.Method, r.URL, ) - for _, frh := range fake.RequestHandlers { + + fake.mu.RLock() + for _, frh := range fake.requestHandlers { errMsg += fmt.Sprintf("* [%s: %s]\n", frh.Method, frh.URL.Path) } + fake.mu.RUnlock() + printError(errMsg) w.WriteHeader(http.StatusNotFound) return @@ -97,8 +103,11 @@ func New(opts ...ServerOption) *HTTPFake { // NewHandler initializes the configuration for a new request handler func (f *HTTPFake) NewHandler() *Request { + f.mu.Lock() + defer f.mu.Unlock() + rh := NewRequest() - f.RequestHandlers = append(f.RequestHandlers, rh) + f.requestHandlers = append(f.requestHandlers, rh) return rh } @@ -110,7 +119,10 @@ func (f *HTTPFake) ResolveURL(path string, args ...interface{}) string { // Reset wipes the request handlers definitions func (f *HTTPFake) Reset() *HTTPFake { - f.RequestHandlers = []*Request{} + f.mu.Lock() + defer f.mu.Unlock() + + f.requestHandlers = []*Request{} return f } @@ -118,10 +130,13 @@ func (f *HTTPFake) Reset() *HTTPFake { // 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() { + f.mu.Lock() + defer f.mu.Unlock() + defer f.Server.Close() if f.t != nil { - for _, reqHandler := range f.RequestHandlers { + for _, reqHandler := range f.requestHandlers { if reqHandler.called == 0 { f.t.Errorf("httpfake: request handler was specified but not called %s", reqHandler.URL.Path) } @@ -130,10 +145,13 @@ func (f *HTTPFake) Close() { } func (f *HTTPFake) findHandler(r *http.Request) (*Request, error) { + f.mu.RLock() + defer f.mu.RUnlock() + founds := []*Request{} url := r.URL.String() path := getURLPath(url) - for _, rh := range f.RequestHandlers { + for _, rh := range f.requestHandlers { if rh.Method != r.Method { continue } From 5bbff60427ab444c35f962beda22b9583d34aa7e Mon Sep 17 00:00:00 2001 From: "ma.mikhaylov" Date: Wed, 6 Dec 2023 10:35:20 +0300 Subject: [PATCH 3/4] SubJSON assertor --- assertions.go | 118 ++++++++++++++++++++++++++++++++++----------- assertions_test.go | 72 +++++++++++++++++++++++++++ go.mod | 7 +++ go.sum | 6 +++ request.go | 84 ++++++++++++++++++++++++++------ 5 files changed, 244 insertions(+), 43 deletions(-) diff --git a/assertions.go b/assertions.go index 907b68d..0b54d05 100644 --- a/assertions.go +++ b/assertions.go @@ -3,27 +3,30 @@ package httpfake import ( "bytes" "fmt" - "io/ioutil" + "io" "net/http" + "reflect" "strings" "testing" + + "github.com/tidwall/gjson" ) const assertErrorTemplate = "assertion error: %s" -// Assertor provides an interface for setting assertions for http requests +// 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 +// 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 +// Assert runs the required headers assertion against the provided request. func (h *requiredHeaders) Assert(r *http.Request) error { var missingHeaders []string @@ -40,23 +43,23 @@ func (h *requiredHeaders) Assert(r *http.Request) error { return nil } -// Log prints a testing info log for the requiredHeaders Assertor +// 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 +// 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 +// 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 +// 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", @@ -68,22 +71,22 @@ func (h *requiredHeaderValue) Assert(r *http.Request) error { return nil } -// Log prints a testing info log for the requiredHeaderValue Assertor +// Log prints a testing info log for the requiredHeaderValue Assertor. func (h *requiredHeaderValue) Log(t testing.TB) { t.Logf("Testing request for a required header value [%s: %s]", h.Key, h.ExpectedValue) } -// Error prints a testing error for the requiredHeaderValue Assertor +// 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 +// 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 +// 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 @@ -100,23 +103,23 @@ func (q *requiredQueries) Assert(r *http.Request) error { return nil } -// Log prints a testing info log for the requiredQueries Assertor +// 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 +// 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 +// 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 +// 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) @@ -124,47 +127,47 @@ func (q *requiredQueryValue) Assert(r *http.Request) error { return nil } -// Log prints a testing info log for the requiredQueryValue Assertor +// Log prints a testing info log for the requiredQueryValue Assertor. func (q *requiredQueryValue) Log(t testing.TB) { t.Logf("Testing request for a required query parameter value [%s: %s]", q.Key, q.ExpectedValue) } -// Error prints a testing error for the requiredQueryValue Assertor +// 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 +// 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 +// 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) + body, err := io.ReadAll(r.Body) if err != nil { return fmt.Errorf("error reading the request body: %s", err.Error()) } 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[:])) + string(body), + string(b.ExpectedBody)) } return nil } -// Log prints a testing info log for the requiredBody Assertor +// Log prints a testing info log for the requiredBody Assertor. func (b *requiredBody) Log(t testing.TB) { t.Log("Testing request for a required body value") } -// Error prints a testing error for the requiredBody Assertor +// Error prints a testing error for the requiredBody Assertor. func (b *requiredBody) Error(t testing.TB, err error) { t.Errorf(assertErrorTemplate, err) } @@ -173,17 +176,76 @@ func (b *requiredBody) Error(t testing.TB, err error) { // adhoc creation of a custom assertion for use with the AssertCustom assertor. type CustomAssertor func(r *http.Request) error -// Assert runs the CustomAssertor assertion against the provided request +// Assert runs the CustomAssertor assertion against the provided request. func (c CustomAssertor) Assert(r *http.Request) error { return c(r) } -// Log prints a testing info log for the CustomAssertor +// Log prints a testing info log for the CustomAssertor. func (c CustomAssertor) Log(t testing.TB) { t.Log("Testing request with a custom assertor") } -// Error prints a testing error for the CustomAssertor +// Error prints a testing error for the CustomAssertor. func (c CustomAssertor) Error(t testing.TB, err error) { t.Errorf(assertErrorTemplate, err) } + +type subJSON struct { + necessaryFields map[string]any +} + +type gjsonForEachFunc func(key, value gjson.Result) bool + +func (j *subJSON) fillFunc(keyPrefix string) gjsonForEachFunc { + return func(key, value gjson.Result) bool { + keyString := key.String() + + if keyPrefix != "" { + keyString = fmt.Sprintf("%s.%s", keyPrefix, keyString) + } + + if value.IsArray() || value.IsObject() { + value.ForEach(j.fillFunc(keyString)) + return true + } + + j.necessaryFields[keyString] = value.Value() + + return true + } +} + +func (j *subJSON) Assert(r *http.Request) error { + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + return err + } + + _ = r.Body.Close() + + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + js := gjson.ParseBytes(bodyBytes) + + var assertionError error + + for key, value := range j.necessaryFields { + bodyValue := js.Get(key).Value() + + if !reflect.DeepEqual(bodyValue, value) { + assertionError = fmt.Errorf(`json assertion failed for "%s" field: expected "%v", got "%v"`, key, value, bodyValue) + break + } + } + + return assertionError +} + +func (j *subJSON) Log(t testing.TB) { + t.Log("Testing request for a required json fields") +} + +func (j *subJSON) Error(t testing.TB, err error) { + t.Errorf(assertErrorTemplate, err) +} diff --git a/assertions_test.go b/assertions_test.go index 47ae931..424e6cc 100644 --- a/assertions_test.go +++ b/assertions_test.go @@ -296,6 +296,62 @@ func TestAssertors_Assert(t *testing.T) { }, expectedErr: "custom assertor error", }, + { + name: "subJSON should return an error when the necessary fields are not present", + assertor: &subJSON{necessaryFields: map[string]any{ + "field1": float64(101), + "subfield1.field1": "101", + "subfield1.subfield2.field1": true, + }, + }, + requestBuilder: func() (*http.Request, error) { + const body = `{ + "field1": 101, + "subfield1": { + "field1": "101", + "subfield2": { + "field1": true + } + } + }` + + testReq, err := http.NewRequest(http.MethodGet, "http://fake.url", bytes.NewBuffer([]byte(body))) + if err != nil { + return nil, err + } + + return testReq, nil + }, + expectedErr: "", + }, + { + name: "subJSON should return an error when the necessary fields are not present", + assertor: &subJSON{necessaryFields: map[string]any{ + "field1": float64(101), + "subfield1.field1": "101", + "subfield1.subfield2.field1": true, + }, + }, + requestBuilder: func() (*http.Request, error) { + const body = `{ + "field1": 101, + "subfield1": { + "field1": "505", + "subfield2": { + "field1": true + } + } + }` + + testReq, err := http.NewRequest(http.MethodGet, "http://fake.url", bytes.NewBuffer([]byte(body))) + if err != nil { + return nil, err + } + + return testReq, nil + }, + expectedErr: `json assertion failed for "subfield1.field1" field: expected "101", got "505"`, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -381,6 +437,14 @@ func TestAssertors_Log(t *testing.T) { }), expected: "Testing request with a custom assertor\n", }, + { + name: "SubJSON Log should log the expected output when called", + mockTester: &mockTester{ + buf: &bytes.Buffer{}, + }, + assertor: &subJSON{}, + expected: "Testing request for a required json fields\n", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -451,6 +515,14 @@ func TestAssertors_Error(t *testing.T) { }), expected: "assertion error: test error", }, + { + name: "SubJSON Log should log the expected output when called", + mockTester: &mockTester{ + buf: &bytes.Buffer{}, + }, + assertor: &subJSON{}, + expected: "assertion error: test error", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/go.mod b/go.mod index 58d971c..a450b9b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/voronelf/httpfake go 1.19 + +require github.com/tidwall/gjson v1.17.0 + +require ( + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect +) diff --git a/go.sum b/go.sum index e69de29..2b213a0 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= diff --git a/request.go b/request.go index 804c408..1515f15 100644 --- a/request.go +++ b/request.go @@ -3,14 +3,17 @@ package httpfake import ( "net/http" "net/url" + "reflect" "strings" "sync" "testing" + + "github.com/tidwall/gjson" ) // Request stores the settings for a request handler // Such as how to match this handler for the incoming requests -// And how this request will respond back +// And how this request will respond back. type Request struct { sync.Mutex Method string @@ -21,7 +24,7 @@ type Request struct { called int } -// NewRequest creates a new Request +// NewRequest creates a new Request. func NewRequest() *Request { return &Request{ URL: &url.URL{}, @@ -30,22 +33,22 @@ func NewRequest() *Request { } } -// Get sets a GET request handler for a given path +// Get sets a GET request handler for a given path. func (r *Request) Get(path string) *Request { return r.method("GET", path) } -// Post sets a POST request handler for a given path +// Post sets a POST request handler for a given path. func (r *Request) Post(path string) *Request { return r.method("POST", path) } -// Put sets a PUT request handler for a given path +// Put sets a PUT request handler for a given path. func (r *Request) Put(path string) *Request { return r.method("PUT", path) } -// Patch sets a PATCH request handler for a given path +// Patch sets a PATCH request handler for a given path. func (r *Request) Patch(path string) *Request { return r.method("PATCH", path) } @@ -55,19 +58,19 @@ func (r *Request) Delete(path string) *Request { return r.method("DELETE", path) } -// Head sets a HEAD request handler for a given path +// Head sets a HEAD request handler for a given path. func (r *Request) Head(path string) *Request { return r.method("HEAD", path) } // Handle sets a custom handle -// By setting this responder it gives full control to the user over this request handler +// By setting this responder it gives full control to the user over this request handler. func (r *Request) Handle(handle Responder) { r.CustomHandle = handle } // Reply sets a response status for this request -// And returns the Response struct to allow chaining the response settings +// And returns the Response struct to allow chaining the response settings. func (r *Request) Reply(status int) *Response { return r.Response.Status(status) } @@ -94,38 +97,89 @@ func (r *Request) runAssertions(t testing.TB, testReq *http.Request) { } } -// AssertQueries will assert that the provided query parameters are present in the requests to this handler +// 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 +// 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 +// 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 +// 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 +// 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 +// 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 } + +func (r *Request) AssertSubJSON(body string) *Request { + js := gjson.Parse(body) + + for _, a := range r.assertions { + ja, ok := a.(*subJSON) + if ok { + js.ForEach(ja.fillFunc("")) + return r + } + } + + jsonAssertor := new(subJSON) + + js.ForEach(jsonAssertor.fillFunc("")) + + r.assertions = append(r.assertions, jsonAssertor) + + return r +} + +func (r *Request) AssertSubJSONBytes(body []byte) *Request { + return r.AssertSubJSON(string(body)) +} + +func (r *Request) AssertJsonField(key string, value any) *Request { + // because gjson only supports float64 + switch reflectValue := reflect.ValueOf(value); { + case reflectValue.CanInt(): + value = float64(reflectValue.Int()) + case reflectValue.CanUint(): + value = float64(reflectValue.Uint()) + case reflectValue.CanFloat(): + value = reflectValue.Float() + } + + for _, a := range r.assertions { + ja, ok := a.(*subJSON) + if ok { + ja.necessaryFields[key] = value + return r + } + } + + jsonAssertor := &subJSON{necessaryFields: make(map[string]any)} + jsonAssertor.necessaryFields[key] = value + + r.assertions = append(r.assertions, jsonAssertor) + + return r +} From 99629f22527c8c1912c53de07b82c4bb5ff84e5a Mon Sep 17 00:00:00 2001 From: "ma.mikhaylov" Date: Thu, 18 Jan 2024 17:28:13 +0300 Subject: [PATCH 4/4] SubJSON assertor fix nil assignment --- request.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/request.go b/request.go index 1515f15..99b663f 100644 --- a/request.go +++ b/request.go @@ -144,7 +144,7 @@ func (r *Request) AssertSubJSON(body string) *Request { } } - jsonAssertor := new(subJSON) + jsonAssertor := &subJSON{necessaryFields: make(map[string]any)} js.ForEach(jsonAssertor.fillFunc(""))