Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: HaveHTTPBody matcher #462

Merged
merged 1 commit into from Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions matchers.go
Expand Up @@ -427,6 +427,13 @@ func HaveHTTPStatus(expected interface{}) types.GomegaMatcher {
return &matchers.HaveHTTPStatusMatcher{Expected: expected}
}

// HaveHTTPBody matches if the body matches.
// Actual must be either a *http.Response or *httptest.ResponseRecorder.
// Expected must be either a string, []byte, or other matcher
func HaveHTTPBody(expected interface{}) types.GomegaMatcher {
return &matchers.HaveHTTPBodyMatcher{Expected: expected}
}

//And succeeds only if all of the given matchers succeed.
//The matchers are tried in order, and will fail-fast if one doesn't succeed.
// Expect("hi").To(And(HaveLen(2), Equal("hi"))
Expand Down
101 changes: 101 additions & 0 deletions matchers/have_http_body_matcher.go
@@ -0,0 +1,101 @@
package matchers

import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"

"github.com/onsi/gomega/format"
"github.com/onsi/gomega/types"
)

type HaveHTTPBodyMatcher struct {
Expected interface{}
cachedBody []byte
}

func (matcher *HaveHTTPBodyMatcher) Match(actual interface{}) (bool, error) {
body, err := matcher.body(actual)
if err != nil {
return false, err
}

switch e := matcher.Expected.(type) {
case string:
return (&EqualMatcher{Expected: e}).Match(string(body))
case []byte:
return (&EqualMatcher{Expected: e}).Match(body)
case types.GomegaMatcher:
return e.Match(body)
default:
return false, fmt.Errorf("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n%s", format.Object(matcher.Expected, 1))
}
}

func (matcher *HaveHTTPBodyMatcher) FailureMessage(actual interface{}) (message string) {
body, err := matcher.body(actual)
if err != nil {
return fmt.Sprintf("failed to read body: %s", err)
}

switch e := matcher.Expected.(type) {
case string:
return (&EqualMatcher{Expected: e}).FailureMessage(string(body))
case []byte:
return (&EqualMatcher{Expected: e}).FailureMessage(body)
case types.GomegaMatcher:
return e.FailureMessage(body)
default:
return fmt.Sprintf("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n%s", format.Object(matcher.Expected, 1))
}
}

func (matcher *HaveHTTPBodyMatcher) NegatedFailureMessage(actual interface{}) (message string) {
body, err := matcher.body(actual)
if err != nil {
return fmt.Sprintf("failed to read body: %s", err)
}

switch e := matcher.Expected.(type) {
case string:
return (&EqualMatcher{Expected: e}).NegatedFailureMessage(string(body))
case []byte:
return (&EqualMatcher{Expected: e}).NegatedFailureMessage(body)
case types.GomegaMatcher:
return e.NegatedFailureMessage(body)
default:
return fmt.Sprintf("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n%s", format.Object(matcher.Expected, 1))
}
}

// body returns the body. It is cached because once we read it in Match()
// the Reader is closed and it is not readable again in FailureMessage()
// or NegatedFailureMessage()
func (matcher *HaveHTTPBodyMatcher) body(actual interface{}) ([]byte, error) {
if matcher.cachedBody != nil {
return matcher.cachedBody, nil
}

body := func(a *http.Response) ([]byte, error) {
if a.Body != nil {
defer a.Body.Close()
var err error
matcher.cachedBody, err = ioutil.ReadAll(a.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body: %w", err)
}
}
return matcher.cachedBody, nil
}

switch a := actual.(type) {
case *http.Response:
return body(a)
case *httptest.ResponseRecorder:
return body(a.Result())
default:
return nil, fmt.Errorf("HaveHTTPBody matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n%s", format.Object(actual, 1))
}

}
187 changes: 187 additions & 0 deletions matchers/have_http_body_matcher_test.go
@@ -0,0 +1,187 @@
package matchers_test

import (
"bytes"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("HaveHTTPBody", func() {
When("ACTUAL is *http.Response", func() {
It("matches the body", func() {
const body = "this is the body"
resp := &http.Response{Body: ioutil.NopCloser(strings.NewReader(body))}
Expect(resp).To(HaveHTTPBody(body))
})

It("mismatches the body", func() {
const body = "this is the body"
resp := &http.Response{Body: ioutil.NopCloser(strings.NewReader(body))}
Expect(resp).NotTo(HaveHTTPBody("something else"))
})
})

When("ACTUAL is *httptest.ResponseRecorder", func() {
It("matches the body", func() {
const body = "this is the body"
resp := &httptest.ResponseRecorder{Body: bytes.NewBufferString(body)}
Expect(resp).To(HaveHTTPBody(body))
})

It("mismatches the body", func() {
const body = "this is the body"
resp := &httptest.ResponseRecorder{Body: bytes.NewBufferString(body)}
Expect(resp).NotTo(HaveHTTPBody("something else"))
})
})

When("ACTUAL is neither *http.Response nor *httptest.ResponseRecorder", func() {
It("errors", func() {
failures := InterceptGomegaFailures(func() {
Expect("foo").To(HaveHTTPBody("bar"))
})
Expect(failures).To(ConsistOf("HaveHTTPBody matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n <string>: foo"))
})
})

When("EXPECTED is []byte", func() {
It("matches the body", func() {
const body = "this is the body"
resp := &http.Response{Body: ioutil.NopCloser(strings.NewReader(body))}
Expect(resp).To(HaveHTTPBody([]byte(body)))
})

It("mismatches the body", func() {
const body = "this is the body"
resp := &http.Response{Body: ioutil.NopCloser(strings.NewReader(body))}
Expect(resp).NotTo(HaveHTTPBody([]byte("something else")))
})
})

When("EXPECTED is a submatcher", func() {
It("matches the body", func() {
resp := &http.Response{Body: ioutil.NopCloser(strings.NewReader(`{"some":"json"}`))}
Expect(resp).To(HaveHTTPBody(MatchJSON(`{ "some": "json" }`)))
})

It("mismatches the body", func() {
resp := &http.Response{Body: ioutil.NopCloser(strings.NewReader(`{"some":"json"}`))}
Expect(resp).NotTo(HaveHTTPBody(MatchJSON(`{ "something": "different" }`)))
})
})

When("EXPECTED is something else", func() {
It("errors", func() {
failures := InterceptGomegaFailures(func() {
resp := &http.Response{Body: ioutil.NopCloser(strings.NewReader("body"))}
Expect(resp).To(HaveHTTPBody(map[int]bool{}))
})
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(Equal("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n <map[int]bool | len:0>: {}"))
})
})

Describe("FailureMessage", func() {
Context("EXPECTED is string", func() {
It("returns a match failure message", func() {
failures := InterceptGomegaFailures(func() {
resp := &http.Response{Body: ioutil.NopCloser(strings.NewReader("this is the body"))}
Expect(resp).To(HaveHTTPBody("this is a different body"))
})
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(Equal(`Expected
<string>: this is the body
to equal
<string>: this is a different body`), failures[0])
})
})

Context("EXPECTED is []byte", func() {
It("returns a match failure message", func() {
failures := InterceptGomegaFailures(func() {
resp := &http.Response{Body: ioutil.NopCloser(strings.NewReader("this is the body"))}
Expect(resp).To(HaveHTTPBody([]byte("this is a different body")))
})
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(MatchRegexp(`^Expected
<\[\]uint8 \| len:\d+, cap:\d+>: this is the body
to equal
<\[\]uint8 ]| len:\d+, cap:\d+>: this is a different body$`))
})
})

Context("EXPECTED is submatcher", func() {
It("returns a match failure message", func() {
failures := InterceptGomegaFailures(func() {
resp := &http.Response{Body: ioutil.NopCloser(strings.NewReader(`{"some":"json"}`))}
Expect(resp).To(HaveHTTPBody(MatchJSON(`{"other":"stuff"}`)))
})
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(Equal(`Expected
<string>: {
"some": "json"
}
to match JSON of
<string>: {
"other": "stuff"
}`))
})
})
})

Describe("NegatedFailureMessage", func() {
Context("EXPECTED is string", func() {
It("returns a negated failure message", func() {
const body = "this is the body"
failures := InterceptGomegaFailures(func() {
resp := &http.Response{Body: ioutil.NopCloser(strings.NewReader(body))}
Expect(resp).NotTo(HaveHTTPBody(body))
})
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(Equal(`Expected
<string>: this is the body
not to equal
<string>: this is the body`))
})
})

Context("EXPECTED is []byte", func() {
It("returns a match failure message", func() {
const body = "this is the body"
failures := InterceptGomegaFailures(func() {
resp := &http.Response{Body: ioutil.NopCloser(strings.NewReader(body))}
Expect(resp).NotTo(HaveHTTPBody([]byte(body)))
})
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(MatchRegexp(`^Expected
<\[\]uint8 \| len:\d+, cap:\d+>: this is the body
not to equal
<\[\]uint8 \| len:\d+, cap:\d+>: this is the body$`))
})
})

Context("EXPECTED is submatcher", func() {
It("returns a match failure message", func() {
const body = `{"some":"json"}`
failures := InterceptGomegaFailures(func() {
resp := &http.Response{Body: ioutil.NopCloser(strings.NewReader(body))}
Expect(resp).NotTo(HaveHTTPBody(MatchJSON(body)))
})
Expect(failures).To(HaveLen(1))
Expect(failures[0]).To(Equal(`Expected
<string>: {
"some": "json"
}
not to match JSON of
<string>: {
"some": "json"
}`))
})
})
})
})