From 2220adb6af84c691e4b5052f4ef4b465832e556e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Roussel?= Date: Tue, 26 Jan 2021 23:29:30 +0100 Subject: [PATCH 1/2] feat: multiple responses responder ResponderFromMultipleResponses spawns a responder returning each response in the provided order --- response.go | 43 +++++++++++++++++++++++++++++ response_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/response.go b/response.go index 85c7ab8..cb20535 100644 --- a/response.go +++ b/response.go @@ -9,6 +9,7 @@ import ( "net/http" "strconv" "strings" + "sync" "github.com/jarcoal/httpmock/internal" ) @@ -125,6 +126,48 @@ func ResponderFromResponse(resp *http.Response) Responder { } } +// ResponderFromMultipleResponses wraps an *http.Response list in a Responder. +// +// Each response will be returned in the order of the provided list. +// If the responder is called more than the size of the provided list, an error +// will be thrown. +// +// Be careful, except for responses generated by httpmock +// (NewStringResponse and NewBytesResponse functions) for which there +// is no problems, it is the caller responsibility to ensure the +// response body can be read several times and concurrently if needed, +// as it is shared among all Responder returned responses. +// +// For home-made responses, NewRespBodyFromString and +// NewRespBodyFromBytes functions can be used to produce response +// bodies that can be read several times and concurrently. +func ResponderFromMultipleResponses(responses []*http.Response, fn ...func(...interface{})) Responder { + responseIndex := 0 + mutex := sync.Mutex{} + return func(req *http.Request) (*http.Response, error) { + mutex.Lock() + defer mutex.Unlock() + defer func() { responseIndex++ }() + if responseIndex >= len(responses) { + err := internal.StackTracer{ + Err: fmt.Errorf("not enough responses provided: responder called %d time(s) but %d response(s) provided", responseIndex+1, len(responses)), + } + if len(fn) > 0 { + err.CustomFn = fn[0] + } + return nil, err + } + res := *responses[responseIndex] + // Our stuff: generate a new io.ReadCloser instance sharing the same buffer + if body, ok := responses[responseIndex].Body.(*dummyReadCloser); ok { + res.Body = body.copy() + } + + res.Request = req + return &res, nil + } +} + // NewErrorResponder creates a Responder that returns an empty request and the // given error. This can be used to e.g. imitate more deep http errors for the // client. diff --git a/response_test.go b/response_test.go index 49931c7..e4836e1 100644 --- a/response_test.go +++ b/response_test.go @@ -53,6 +53,76 @@ func TestResponderFromResponse(t *testing.T) { } } +func TestResponderFromResponses(t *testing.T) { + jsonResponse, err := NewJsonResponse(200, map[string]string{"test": "toto"}) + if err != nil { + t.Errorf("NewJsonResponse failed: %s", err) + } + + responder := ResponderFromMultipleResponses( + []*http.Response{ + jsonResponse, + NewStringResponse(200, "hello world"), + }, + ) + + req, err := http.NewRequest(http.MethodGet, testURL, nil) + if err != nil { + t.Fatal("Error creating request") + } + response1, err := responder(req) + if err != nil { + t.Error("Error should be nil") + } + + testURLWithQuery := testURL + "?a=1" + req, err = http.NewRequest(http.MethodGet, testURLWithQuery, nil) + if err != nil { + t.Fatal("Error creating request") + } + response2, err := responder(req) + if err != nil { + t.Error("Error should be nil") + } + + // Body should be the same for both responses + assertBody(t, response1, `{"test":"toto"}`) + assertBody(t, response2, "hello world") + + // Request should be non-nil and different for each response + if response1.Request != nil && response2.Request != nil { + if response1.Request.URL.String() != testURL { + t.Errorf("Expected request url %s, got: %s", testURL, response1.Request.URL.String()) + } + if response2.Request.URL.String() != testURLWithQuery { + t.Errorf("Expected request url %s, got: %s", testURLWithQuery, response2.Request.URL.String()) + } + } else { + t.Error("response.Request should not be nil") + } + + // ensure we can't call the responder more than the number of responses it embeds + _, err = responder(req) + if err == nil { + t.Error("Error should not be nil") + } else if err.Error() != "not enough responses provided: responder called 3 time(s) but 2 response(s) provided" { + t.Error("Invalid error message") + } + + // fn usage + responder = ResponderFromMultipleResponses([]*http.Response{}, func(args ...interface{}) {}) + _, err = responder(req) + if err == nil { + t.Error("Error should not be nil") + } else if err.Error() != "not enough responses provided: responder called 1 time(s) but 0 response(s) provided" { + t.Errorf("Invalid error message") + } else if ne, ok := err.(internal.StackTracer); !ok { + t.Errorf(`err type mismatch, got %T, expected internal.StackTracer`, err) + } else if ne.CustomFn == nil { + t.Error(`ne.CustomFn should not be nil`) + } +} + func TestNewNotFoundResponder(t *testing.T) { responder := NewNotFoundResponder(func(args ...interface{}) {}) From d1b2862c16bfae07c6c64c0d2ba0cde0a7dc03c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Soul=C3=A9?= Date: Wed, 27 Jan 2021 14:01:32 +0100 Subject: [PATCH 2/2] doc: enhance comment --- response.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/response.go b/response.go index cb20535..5d3bc9f 100644 --- a/response.go +++ b/response.go @@ -141,6 +141,29 @@ func ResponderFromResponse(resp *http.Response) Responder { // For home-made responses, NewRespBodyFromString and // NewRespBodyFromBytes functions can be used to produce response // bodies that can be read several times and concurrently. +// +// If all responses have been returned and fn is passed +// and non-nil, it acts as the fn parameter of NewNotFoundResponder, +// allowing to dump the stack trace to localize the origin of the +// call. +// import ( +// "github.com/jarcoal/httpmock" +// "testing" +// ) +// ... +// func TestMyApp(t *testing.T) { +// ... +// // This responder is callable only once, then an error is returned and +// // the stacktrace of the call logged using t.Log() +// httpmock.RegisterResponder("GET", "/foo/bar", +// httpmock.ResponderFromMultipleResponses( +// []*http.Response{ +// httpmock.NewStringResponse(200, `{"name":"bar"}`), +// httpmock.NewStringResponse(404, `{"mesg":"Not found"}`), +// }, +// t.Log), +// ) +// } func ResponderFromMultipleResponses(responses []*http.Response, fn ...func(...interface{})) Responder { responseIndex := 0 mutex := sync.Mutex{}