Skip to content

Commit

Permalink
Merge pull request #104 from c-roussel/add-multiple-responses-responder
Browse files Browse the repository at this point in the history
feat: multiple responses responder
  • Loading branch information
maxatome authored Jan 27, 2021
2 parents c34dbbb + d1b2862 commit 6150913
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 0 deletions.
66 changes: 66 additions & 0 deletions response.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"strconv"
"strings"
"sync"

"github.com/jarcoal/httpmock/internal"
)
Expand Down Expand Up @@ -125,6 +126,71 @@ 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.
//
// 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{}
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.
Expand Down
70 changes: 70 additions & 0 deletions response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}) {})

Expand Down

0 comments on commit 6150913

Please sign in to comment.