Skip to content

Commit

Permalink
feat: HaveHTTPBody matcher (#462)
Browse files Browse the repository at this point in the history
  • Loading branch information
blgm committed Aug 19, 2021
1 parent e5b3157 commit 504e1f2
Show file tree
Hide file tree
Showing 3 changed files with 295 additions and 0 deletions.
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"
}`))
})
})
})
})

0 comments on commit 504e1f2

Please sign in to comment.