From dd83a96aa916716ec90acf794f6d10bcbb4822d4 Mon Sep 17 00:00:00 2001 From: George Blue Date: Thu, 19 Aug 2021 21:42:01 +0100 Subject: [PATCH] feat: HaveHTTPHeaderWithValue() matcher (#463) Co-authored-by: Onsi Fakhouri --- matchers.go | 11 ++ .../have_http_header_with_value_matcher.go | 81 +++++++++ ...ave_http_header_with_value_matcher_test.go | 161 ++++++++++++++++++ 3 files changed, 253 insertions(+) create mode 100644 matchers/have_http_header_with_value_matcher.go create mode 100644 matchers/have_http_header_with_value_matcher_test.go diff --git a/matchers.go b/matchers.go index cde14cfa9..85e3bb51d 100644 --- a/matchers.go +++ b/matchers.go @@ -427,6 +427,17 @@ func HaveHTTPStatus(expected interface{}) types.GomegaMatcher { return &matchers.HaveHTTPStatusMatcher{Expected: expected} } +// HaveHTTPHeaderWithValue succeeds if the header is found and the value matches. +// Actual must be either a *http.Response or *httptest.ResponseRecorder. +// Expected must be a string header name, followed by a header value which +// can be a string, or another matcher. +func HaveHTTPHeaderWithValue(header string, value interface{}) types.GomegaMatcher { + return &matchers.HaveHTTPHeaderWithValueMatcher{ + Header: header, + Value: value, + } +} + // 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 diff --git a/matchers/have_http_header_with_value_matcher.go b/matchers/have_http_header_with_value_matcher.go new file mode 100644 index 000000000..c256f452e --- /dev/null +++ b/matchers/have_http_header_with_value_matcher.go @@ -0,0 +1,81 @@ +package matchers + +import ( + "fmt" + "net/http" + "net/http/httptest" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +type HaveHTTPHeaderWithValueMatcher struct { + Header string + Value interface{} +} + +func (matcher *HaveHTTPHeaderWithValueMatcher) Match(actual interface{}) (success bool, err error) { + headerValue, err := matcher.extractHeader(actual) + if err != nil { + return false, err + } + + headerMatcher, err := matcher.getSubMatcher() + if err != nil { + return false, err + } + + return headerMatcher.Match(headerValue) +} + +func (matcher *HaveHTTPHeaderWithValueMatcher) FailureMessage(actual interface{}) string { + headerValue, err := matcher.extractHeader(actual) + if err != nil { + panic(err) // protected by Match() + } + + headerMatcher, err := matcher.getSubMatcher() + if err != nil { + panic(err) // protected by Match() + } + + diff := format.IndentString(headerMatcher.FailureMessage(headerValue), 1) + return fmt.Sprintf("HTTP header %q:\n%s", matcher.Header, diff) +} + +func (matcher *HaveHTTPHeaderWithValueMatcher) NegatedFailureMessage(actual interface{}) (message string) { + headerValue, err := matcher.extractHeader(actual) + if err != nil { + panic(err) // protected by Match() + } + + headerMatcher, err := matcher.getSubMatcher() + if err != nil { + panic(err) // protected by Match() + } + + diff := format.IndentString(headerMatcher.NegatedFailureMessage(headerValue), 1) + return fmt.Sprintf("HTTP header %q:\n%s", matcher.Header, diff) +} + +func (matcher *HaveHTTPHeaderWithValueMatcher) getSubMatcher() (types.GomegaMatcher, error) { + switch m := matcher.Value.(type) { + case string: + return &EqualMatcher{Expected: matcher.Value}, nil + case types.GomegaMatcher: + return m, nil + default: + return nil, fmt.Errorf("HaveHTTPHeaderWithValue matcher must be passed a string or a GomegaMatcher. Got:\n%s", format.Object(matcher.Value, 1)) + } +} + +func (matcher *HaveHTTPHeaderWithValueMatcher) extractHeader(actual interface{}) (string, error) { + switch r := actual.(type) { + case *http.Response: + return r.Header.Get(matcher.Header), nil + case *httptest.ResponseRecorder: + return r.Result().Header.Get(matcher.Header), nil + default: + return "", fmt.Errorf("HaveHTTPHeaderWithValue matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n%s", format.Object(actual, 1)) + } +} diff --git a/matchers/have_http_header_with_value_matcher_test.go b/matchers/have_http_header_with_value_matcher_test.go new file mode 100644 index 000000000..a5cda8db0 --- /dev/null +++ b/matchers/have_http_header_with_value_matcher_test.go @@ -0,0 +1,161 @@ +package matchers_test + +import ( + "net/http" + "net/http/httptest" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("HaveHTTPHeader", func() { + It("can match an HTTP header", func() { + resp := &http.Response{} + resp.Header = make(http.Header) + resp.Header.Add("fake-header", "fake value") + Expect(resp).To(HaveHTTPHeaderWithValue("fake-header", "fake value")) + }) + + It("can mismatch an HTTP header", func() { + resp := &http.Response{} + resp.Header = make(http.Header) + resp.Header.Add("fake-header", "fake value") + Expect(resp).NotTo(HaveHTTPHeaderWithValue("other-header", "fake value")) + Expect(resp).NotTo(HaveHTTPHeaderWithValue("fake-header", "other value")) + }) + + When("the header is set more than once", func() { + It("matches the first value and not the second", func() { + resp := &http.Response{} + resp.Header = make(http.Header) + resp.Header.Add("fake-header", "fake value1") + resp.Header.Add("fake-header", "fake value2") + Expect(resp).To(HaveHTTPHeaderWithValue("fake-header", "fake value1")) + Expect(resp).NotTo(HaveHTTPHeaderWithValue("fake-header", "fake value2")) + }) + }) + + When("ACTUAL is *httptest.ResponseRecorder", func() { + It("can match an HTTP header", func() { + resp := &httptest.ResponseRecorder{} + resp.Header().Add("fake-header", "fake value") + Expect(resp).To(HaveHTTPHeaderWithValue("fake-header", "fake value")) + }) + + It("can mismatch an HTTP header", func() { + resp := &httptest.ResponseRecorder{} + resp.Header().Add("fake-header", "fake value") + Expect(resp).NotTo(HaveHTTPHeaderWithValue("other-header", "fake value")) + Expect(resp).NotTo(HaveHTTPHeaderWithValue("fake-header", "other value")) + }) + }) + + When("ACTUAL is neither *http.Response nor *httptest.ResponseRecorder", func() { + It("errors", func() { + failures := InterceptGomegaFailures(func() { + Expect("foo").To(HaveHTTPHeaderWithValue("bar", "baz")) + }) + Expect(failures).To(HaveLen(1)) + Expect(failures[0]).To(Equal("HaveHTTPHeaderWithValue matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n : foo")) + }) + }) + + When("EXPECTED VALUE is a matcher", func() { + It("can match an HTTP header", func() { + resp := &http.Response{} + resp.Header = make(http.Header) + resp.Header.Add("fake-header", "fake value") + Expect(resp).To(HaveHTTPHeaderWithValue("fake-header", ContainSubstring("value"))) + }) + + It("can mismatch an HTTP header", func() { + resp := &http.Response{} + resp.Header = make(http.Header) + resp.Header.Add("fake-header", "fake value") + Expect(resp).NotTo(HaveHTTPHeaderWithValue("fake-header", ContainSubstring("foo"))) + }) + }) + + When("EXPECTED VALUE is something else", func() { + It("errors", func() { + failures := InterceptGomegaFailures(func() { + resp := &http.Response{} + Expect(resp).To(HaveHTTPHeaderWithValue("bar", 42)) + }) + Expect(failures).To(HaveLen(1)) + Expect(failures[0]).To(Equal("HaveHTTPHeaderWithValue matcher must be passed a string or a GomegaMatcher. Got:\n : 42")) + }) + }) + + Describe("FailureMessage", func() { + When("matching a string", func() { + It("returns message", func() { + failures := InterceptGomegaFailures(func() { + resp := &http.Response{} + resp.Header = make(http.Header) + resp.Header.Add("fake-header", "fake value") + Expect(resp).To(HaveHTTPHeaderWithValue("fake-header", "other value")) + }) + Expect(failures).To(HaveLen(1)) + Expect(failures[0]).To(Equal(`HTTP header "fake-header": + Expected + : fake value + to equal + : other value`), failures[0]) + }) + }) + + When("matching a matcher", func() { + It("returns message", func() { + failures := InterceptGomegaFailures(func() { + resp := &http.Response{} + resp.Header = make(http.Header) + resp.Header.Add("fake-header", "fake value") + Expect(resp).To(HaveHTTPHeaderWithValue("fake-header", ContainSubstring("other"))) + }) + Expect(failures).To(HaveLen(1)) + Expect(failures[0]).To(Equal(`HTTP header "fake-header": + Expected + : fake value + to contain substring + : other`), failures[0]) + }) + }) + }) + + Describe("NegatedFailureMessage", func() { + When("matching a string", func() { + It("returns message", func() { + failures := InterceptGomegaFailures(func() { + resp := &http.Response{} + resp.Header = make(http.Header) + resp.Header.Add("fake-header", "fake value") + Expect(resp).NotTo(HaveHTTPHeaderWithValue("fake-header", "fake value")) + }) + Expect(failures).To(HaveLen(1)) + Expect(failures[0]).To(Equal(`HTTP header "fake-header": + Expected + : fake value + not to equal + : fake value`), failures[0]) + }) + }) + + When("matching a matcher", func() { + It("returns message", func() { + failures := InterceptGomegaFailures(func() { + resp := &http.Response{} + resp.Header = make(http.Header) + resp.Header.Add("fake-header", "fake value") + Expect(resp).NotTo(HaveHTTPHeaderWithValue("fake-header", ContainSubstring("value"))) + }) + Expect(failures).To(HaveLen(1)) + Expect(failures[0]).To(Equal(`HTTP header "fake-header": + Expected + : fake value + not to contain substring + : value`), failures[0]) + }) + }) + }) +})