Skip to content

Commit

Permalink
feat: HaveHTTPStatus multiple expected values (#465)
Browse files Browse the repository at this point in the history
* feat: formatter for HTTP responses

Previously when the HaveHTTPStatus() matcher failed, the whole HTTP
response object was printed out, and any message was rendered in
bytes rather than as a string.

This introduces a formatter for HTTP responses, so that the key data is
presented in a helpful format.

* feat: HaveHTTPStatus multiple expected values

Sometimes more than one HTTP status is acceptable. For example a server
may (correctly) return HTTP 204 for an empty response, or it might
return HTTP 200 for an empty response - and we might want to accept
either.

We can now write:
```go
Expect(resp).To(HaveHTTPStatus(http.StatusOK, http.StatusNoContent))
```
And it will match either. This is simpler than using Or() or
SatisfyAny().

Co-authored-by: Onsi Fakhouri <[email protected]>
  • Loading branch information
blgm and onsi authored Aug 19, 2021
1 parent dd83a96 commit aa69f1b
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 31 deletions.
3 changes: 2 additions & 1 deletion matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,8 @@ func BeADirectory() types.GomegaMatcher {
//Expected must be either an int or a string.
// Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) // asserts that resp.StatusCode == 200
// Expect(resp).Should(HaveHTTPStatus("404 Not Found")) // asserts that resp.Status == "404 Not Found"
func HaveHTTPStatus(expected interface{}) types.GomegaMatcher {
// Expect(resp).Should(HaveHTTPStatus(http.StatusOK, http.StatusNoContent)) // asserts that resp.StatusCode == 200 || resp.StatusCode == 204
func HaveHTTPStatus(expected ...interface{}) types.GomegaMatcher {
return &matchers.HaveHTTPStatusMatcher{Expected: expected}
}

Expand Down
38 changes: 29 additions & 9 deletions matchers/have_http_status_matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

type HaveHTTPStatusMatcher struct {
Expected interface{}
Expected []interface{}
}

func (matcher *HaveHTTPStatusMatcher) Match(actual interface{}) (success bool, err error) {
Expand All @@ -26,22 +26,42 @@ func (matcher *HaveHTTPStatusMatcher) Match(actual interface{}) (success bool, e
return false, fmt.Errorf("HaveHTTPStatus matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n%s", format.Object(actual, 1))
}

switch e := matcher.Expected.(type) {
case int:
return resp.StatusCode == e, nil
case string:
return resp.Status == e, nil
if len(matcher.Expected) == 0 {
return false, fmt.Errorf("HaveHTTPStatus matcher must be passed an int or a string. Got nothing")
}

return false, fmt.Errorf("HaveHTTPStatus matcher must be passed an int or a string. Got:\n%s", format.Object(matcher.Expected, 1))
for _, expected := range matcher.Expected {
switch e := expected.(type) {
case int:
if resp.StatusCode == e {
return true, nil
}
case string:
if resp.Status == e {
return true, nil
}
default:
return false, fmt.Errorf("HaveHTTPStatus matcher must be passed int or string types. Got:\n%s", format.Object(expected, 1))
}
}

return false, nil
}

func (matcher *HaveHTTPStatusMatcher) FailureMessage(actual interface{}) (message string) {
return fmt.Sprintf("Expected\n%s\n%s\n%s", formatHttpResponse(actual), "to have HTTP status", format.Object(matcher.Expected, 1))
return fmt.Sprintf("Expected\n%s\n%s\n%s", formatHttpResponse(actual), "to have HTTP status", matcher.expectedString())
}

func (matcher *HaveHTTPStatusMatcher) NegatedFailureMessage(actual interface{}) (message string) {
return fmt.Sprintf("Expected\n%s\n%s\n%s", formatHttpResponse(actual), "not to have HTTP status", format.Object(matcher.Expected, 1))
return fmt.Sprintf("Expected\n%s\n%s\n%s", formatHttpResponse(actual), "not to have HTTP status", matcher.expectedString())
}

func (matcher *HaveHTTPStatusMatcher) expectedString() string {
var lines []string
for _, expected := range matcher.Expected {
lines = append(lines, format.Object(expected, 1))
}
return strings.Join(lines, "\n")
}

func formatHttpResponse(input interface{}) string {
Expand Down
125 changes: 104 additions & 21 deletions matchers/have_http_status_matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,68 @@ import (
)

var _ = Describe("HaveHTTPStatus", func() {
When("ACTUAL is *http.Response", func() {
When("EXPECTED is integer", func() {
It("matches the StatusCode", func() {
When("EXPECTED is single integer", func() {
It("matches the StatusCode", func() {
resp := &http.Response{StatusCode: http.StatusOK}
Expect(resp).To(HaveHTTPStatus(http.StatusOK))
Expect(resp).NotTo(HaveHTTPStatus(http.StatusNotFound))
})
})

When("EXPECTED is single string", func() {
It("matches the Status", func() {
resp := &http.Response{Status: "200 OK"}
Expect(resp).To(HaveHTTPStatus("200 OK"))
Expect(resp).NotTo(HaveHTTPStatus("404 Not Found"))
})
})

When("EXPECTED is empty", func() {
It("errors", func() {
failures := InterceptGomegaFailures(func() {
resp := &http.Response{StatusCode: http.StatusOK}
Expect(resp).To(HaveHTTPStatus(http.StatusOK))
Expect(resp).NotTo(HaveHTTPStatus(http.StatusNotFound))
Expect(resp).To(HaveHTTPStatus())
})
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(Equal("HaveHTTPStatus matcher must be passed an int or a string. Got nothing"))
})
})

When("EXPECTED is string", func() {
It("matches the Status", func() {
resp := &http.Response{Status: "200 OK"}
Expect(resp).To(HaveHTTPStatus("200 OK"))
Expect(resp).NotTo(HaveHTTPStatus("404 Not Found"))
When("EXPECTED is not a string or integer", func() {
It("errors", func() {
failures := InterceptGomegaFailures(func() {
resp := &http.Response{StatusCode: http.StatusOK}
Expect(resp).To(HaveHTTPStatus(true))
})
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(Equal("HaveHTTPStatus matcher must be passed int or string types. Got:\n <bool>: true"))
})
})

When("EXPECTED is anything else", func() {
It("does not match", func() {
failures := InterceptGomegaFailures(func() {
resp := &http.Response{StatusCode: http.StatusOK}
Expect(resp).NotTo(HaveHTTPStatus(true))
})
Expect(failures).To(ConsistOf("HaveHTTPStatus matcher must be passed an int or a string. Got:\n <bool>: true"))
When("EXPECTED is a list of strings and integers", func() {
It("matches the StatusCode and Status", func() {
resp := &http.Response{
Status: "200 OK",
StatusCode: http.StatusOK,
}
Expect(resp).To(HaveHTTPStatus(http.StatusOK, http.StatusNoContent, http.StatusNotFound))
Expect(resp).To(HaveHTTPStatus("204 Feeling Fine", "200 OK", "404 Not Found"))
Expect(resp).To(HaveHTTPStatus("204 Feeling Fine", http.StatusOK, "404 Not Found"))
Expect(resp).To(HaveHTTPStatus(http.StatusNoContent, "200 OK", http.StatusNotFound))
Expect(resp).NotTo(HaveHTTPStatus(http.StatusNotFound, http.StatusNoContent, http.StatusGone))
Expect(resp).NotTo(HaveHTTPStatus("204 Feeling Fine", "201 Sleeping", "404 Not Found"))
Expect(resp).NotTo(HaveHTTPStatus(http.StatusNotFound, "404 Not Found", http.StatusGone))
})
})

When("EXPECTED is a list containing non-string or integer types", func() {
It("errors", func() {
failures := InterceptGomegaFailures(func() {
resp := &http.Response{StatusCode: http.StatusOK}
Expect(resp).To(HaveHTTPStatus(http.StatusGone, "204 No Content", true, http.StatusNotFound))
})
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(Equal("HaveHTTPStatus matcher must be passed int or string types. Got:\n <bool>: true"))
})
})

Expand All @@ -62,7 +99,8 @@ var _ = Describe("HaveHTTPStatus", func() {
resp := &httptest.ResponseRecorder{Code: http.StatusOK}
Expect(resp).NotTo(HaveHTTPStatus(nil))
})
Expect(failures).To(ConsistOf("HaveHTTPStatus matcher must be passed an int or a string. Got:\n <nil>: nil"))
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(Equal("HaveHTTPStatus matcher must be passed int or string types. Got:\n <nil>: nil"))
})
})
})
Expand All @@ -72,12 +110,13 @@ var _ = Describe("HaveHTTPStatus", func() {
failures := InterceptGomegaFailures(func() {
Expect("foo").To(HaveHTTPStatus(http.StatusOK))
})
Expect(failures).To(ConsistOf("HaveHTTPStatus matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n <string>: foo"))
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(Equal("HaveHTTPStatus matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n <string>: foo"))
})
})

Describe("FailureMessage", func() {
It("returns message", func() {
It("returns a message for a single expected value", func() {
failures := InterceptGomegaFailures(func() {
resp := &http.Response{
StatusCode: http.StatusBadGateway,
Expand All @@ -96,10 +135,32 @@ var _ = Describe("HaveHTTPStatus", func() {
to have HTTP status
<int>: 200`), failures[0])
})

It("returns a message for a multiple expected values", func() {
failures := InterceptGomegaFailures(func() {
resp := &http.Response{
StatusCode: http.StatusBadGateway,
Status: "502 Bad Gateway",
Body: ioutil.NopCloser(strings.NewReader("did not like it")),
}
Expect(resp).To(HaveHTTPStatus(http.StatusOK, http.StatusNotFound, "204 No content"))
})
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(Equal(`Expected
<*http.Response>: {
Status: <string>: "502 Bad Gateway"
StatusCode: <int>: 502
Body: <string>: "did not like it"
}
to have HTTP status
<int>: 200
<int>: 404
<string>: 204 No content`), failures[0])
})
})

Describe("NegatedFailureMessage", func() {
It("returns message", func() {
It("returns a message for a single expected value", func() {
failures := InterceptGomegaFailures(func() {
resp := &http.Response{
StatusCode: http.StatusOK,
Expand All @@ -118,5 +179,27 @@ to have HTTP status
not to have HTTP status
<int>: 200`), failures[0])
})

It("returns a message for a multiple expected values", func() {
failures := InterceptGomegaFailures(func() {
resp := &http.Response{
StatusCode: http.StatusOK,
Status: "200 OK",
Body: ioutil.NopCloser(strings.NewReader("got it!")),
}
Expect(resp).NotTo(HaveHTTPStatus(http.StatusOK, "204 No content", http.StatusGone))
})
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(Equal(`Expected
<*http.Response>: {
Status: <string>: "200 OK"
StatusCode: <int>: 200
Body: <string>: "got it!"
}
not to have HTTP status
<int>: 200
<string>: 204 No content
<int>: 410`), failures[0])
})
})
})

0 comments on commit aa69f1b

Please sign in to comment.