-
Notifications
You must be signed in to change notification settings - Fork 444
/
have_http_response.go
165 lines (141 loc) · 5.61 KB
/
have_http_response.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package matchers
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/onsi/gomega"
"github.com/onsi/gomega/gstruct"
"github.com/onsi/gomega/matchers"
"github.com/onsi/gomega/types"
)
var (
_ types.GomegaMatcher = new(HaveHttpResponseMatcher)
)
// HaveOkResponse expects a http response with a 200 status code
func HaveOkResponse() types.GomegaMatcher {
return HaveStatusCode(http.StatusOK)
}
// HaveStatusCode expects a http response with a particular status code
func HaveStatusCode(statusCode int) types.GomegaMatcher {
return HaveHttpResponse(&HttpResponse{
StatusCode: statusCode,
Body: gstruct.Ignore(),
})
}
// HaveExactResponseBody expects a 200 response with a body that matches the provided string
func HaveExactResponseBody(body string) types.GomegaMatcher {
return HaveHttpResponse(&HttpResponse{
StatusCode: http.StatusOK,
Body: body,
})
}
// HavePartialResponseBody expects a 200 response with a body that contains the provided substring
func HavePartialResponseBody(substring string) types.GomegaMatcher {
return HaveHttpResponse(&HttpResponse{
StatusCode: http.StatusOK,
Body: gomega.ContainSubstring(substring),
})
}
// HaveOkResponseWithHeaders expects an 200 response with a set of headers that match the provided headers
func HaveOkResponseWithHeaders(headers map[string]interface{}) types.GomegaMatcher {
return HaveHttpResponse(&HttpResponse{
StatusCode: http.StatusOK,
Body: gomega.BeEmpty(),
Headers: headers,
})
}
// HttpResponse defines the set of properties that we can validate from an http.Response
type HttpResponse struct {
// StatusCode is the expected status code for an http.Response
// Required
StatusCode int
// Body is the expected response body for an http.Response
// Body can be of type: {string, bytes, GomegaMatcher}
// Optional: If not provided, defaults to an empty string
Body interface{}
// Headers is the set of expected header values for an http.Response
// Each header can be of type: {string, GomegaMatcher}
// Optional: If not provided, does not perform header validation
Headers map[string]interface{}
// Custom is a generic matcher that can be applied to validate any other properties of an http.Response
// Optional: If not provided, does not perform additional validation
Custom types.GomegaMatcher
}
// HaveHttpResponse returns a GomegaMatcher which validates that an http.Response contains
// particular expected properties (status, body..etc)
// If an expected body isn't defined, we default to expecting an empty response
func HaveHttpResponse(expected *HttpResponse) types.GomegaMatcher {
expectedBody := expected.Body
if expectedBody == nil {
// Default to an empty body
expectedBody = ""
}
expectedCustomMatcher := expected.Custom
if expected.Custom == nil {
// Default to an always accept matcher
expectedCustomMatcher = gstruct.Ignore()
}
var partialResponseMatchers []types.GomegaMatcher
partialResponseMatchers = append(partialResponseMatchers, &matchers.HaveHTTPStatusMatcher{
Expected: []interface{}{
expected.StatusCode,
},
})
partialResponseMatchers = append(partialResponseMatchers, &matchers.HaveHTTPBodyMatcher{
Expected: expectedBody,
})
for headerName, headerMatch := range expected.Headers {
partialResponseMatchers = append(partialResponseMatchers, &matchers.HaveHTTPHeaderWithValueMatcher{
Header: headerName,
Value: headerMatch,
})
}
partialResponseMatchers = append(partialResponseMatchers, expectedCustomMatcher)
return &HaveHttpResponseMatcher{
Expected: expected,
responseMatcher: gomega.And(partialResponseMatchers...),
}
}
type HaveHttpResponseMatcher struct {
Expected *HttpResponse
responseMatcher types.GomegaMatcher
// An internal utility for tracking whether we have evaluated this matcher
// There is a comment within the Match method, outlining why we introduced this
evaluated bool
}
func (m *HaveHttpResponseMatcher) Match(actual interface{}) (success bool, err error) {
if m.evaluated {
// Matchers are intended to be short-lived, and we have seen inconsistent behaviors
// when evaluating the same matcher multiple times.
// For example, the underlying http body matcher caches the response body, so if you are wrapping this
// matcher in an Eventually, you need to create a new matcher each iteration.
// This error is intended to help prevent developers hitting this edge case
return false, errors.New("using the same matcher twice can lead to inconsistent behaviors")
}
m.evaluated = true
if ok, matchErr := m.responseMatcher.Match(actual); !ok {
return false, matchErr
}
return true, nil
}
func (m *HaveHttpResponseMatcher) FailureMessage(actual interface{}) (message string) {
return fmt.Sprintf("%s \n%s",
m.responseMatcher.FailureMessage(actual),
informativeComparison(m.Expected, actual))
}
func (m *HaveHttpResponseMatcher) NegatedFailureMessage(actual interface{}) (message string) {
return fmt.Sprintf("%s \n%s",
m.responseMatcher.NegatedFailureMessage(actual),
informativeComparison(m.Expected, actual))
}
// informativeComparison returns a string which presents data to the user to help them understand why a failure occurred.
// The HaveHttpResponseMatcher uses an And matcher, which intentionally short-circuits and only
// logs the first failure that occurred.
// To help developers, we print more details in this function.
// NOTE: Printing the actual http.Response is challenging (since the body has already been read), so for now
// we do not print it.
func informativeComparison(expected, actual interface{}) string {
expectedJson, _ := json.MarshalIndent(expected, "", " ")
return fmt.Sprintf("\nexpected: %s", expectedJson)
}