-
Notifications
You must be signed in to change notification settings - Fork 0
/
registry.go
333 lines (297 loc) · 12.2 KB
/
registry.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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
package httpregistry
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/http/httputil"
"reflect"
)
// Registry represents a collection of matches that associate to a http request a http response.
// It contains all the Match that were registered and after the server is called it contains all the reasons why a request did not match with a particular match
// the testing.T is used to signal that there was an unexpected error or that not all the responses were consumed as expected
type Registry struct {
t TestingT
matches []match
misses []miss
}
// NewRegistry creates a new empty Registry
func NewRegistry(t TestingT) *Registry {
reg := Registry{t: t}
return ®
}
// Add adds to the registry a 200 response for any requests
//
// reg := httpregistry.NewRegistry(t)
// reg.Add()
// reg.GetServer()
//
// will create a http server that returns 200 on calling anything.
func (reg *Registry) Add() {
request := NewRequest()
reg.matches = append(reg.matches, newConsumableResponsesMatch(request, []Response{OkResponse}))
}
// AddURL adds to the registry a 200 response for a request that matches the URL
//
// reg := httpregistry.NewRegistry(t)
// reg.AddURL("/foo")
// reg.GetServer()
//
// will create a http server that returns 200 on calling GET "/foo" and fails the test on anything else
func (reg *Registry) AddURL(URL string) {
request := NewRequest().WithURL(URL)
reg.matches = append(reg.matches, newConsumableResponsesMatch(request, []Response{OkResponse}))
}
// AddURLWithStatusCode adds to the registry a statusCode response for a request that matches the URL
//
// reg := httpregistry.NewRegistry(t)
// reg.AddURLWithStatusCode("/foo", 401)
// reg.GetServer()
//
// will create a http server that returns 401 on calling GET "/foo" and fails the test on anything else
func (reg *Registry) AddURLWithStatusCode(URL string, statusCode int) {
request := NewRequest().WithURL(URL)
response := NewResponse().WithStatus(statusCode)
reg.matches = append(reg.matches, newConsumableResponsesMatch(request, []Response{response}))
}
// AddMethod adds to the registry a 200 response for a request that matches the method
//
// reg := httpregistry.NewRegistry(t)
// reg.AddMethod("/foo")
// reg.GetServer()
//
// will create a http server that returns 200 on calling GET "/foo" and fails the test on anything else
func (reg *Registry) AddMethod(method string) {
request := NewRequest().WithMethod(method)
reg.matches = append(reg.matches, newConsumableResponsesMatch(request, []Response{OkResponse}))
}
// AddMethodWithStatusCode adds to the registry a statusCode response for a request that matches the method
//
// reg := httpregistry.NewRegistry(t)
// reg.AddMethodWithStatusCode("/foo", 401)
// reg.GetServer()
//
// will create a http server that returns 401 on calling GET "/foo" and fails the test on anything else
func (reg *Registry) AddMethodWithStatusCode(method string, statusCode int) {
request := NewRequest().WithMethod(method)
response := NewResponse().WithStatus(statusCode)
reg.matches = append(reg.matches, newConsumableResponsesMatch(request, []Response{response}))
}
// AddMethodAndURL adds to the registry a 200 response for a request that matches method and URL
//
// reg := httpregistry.NewRegistry(t)
// reg.AddMethodAndURL(GET, "/foo")
// reg.GetServer()
//
// will create a http server that returns 200 on calling GET "/foo" and fails the test on anything else
func (reg *Registry) AddMethodAndURL(method string, URL string) {
request := NewRequest().WithMethod(method).WithURL(URL)
reg.matches = append(reg.matches, newConsumableResponsesMatch(request, []Response{OkResponse}))
}
// AddMethodAndURLWithStatusCode adds to the registry a statusCode response for a request that matches method and URL
//
// reg := httpregistry.NewRegistry(t)
// reg.AddSimpleRequest(PUT, "/foo", 204)
// reg.GetServer()
//
// will create a http server that returns 204 on calling GET "/foo" and fails the test on anything else
func (reg *Registry) AddMethodAndURLWithStatusCode(method string, URL string, statusCode int) {
request := NewRequest().WithMethod(method).WithURL(URL)
response := NewResponse().WithStatus(statusCode)
reg.matches = append(reg.matches, newConsumableResponsesMatch(request, []Response{response}))
}
// AddBody adds to the registry a statusCode response for a request that matches method and URL
//
// reg := httpregistry.NewRegistry(t)
// reg.AddSimpleRequest(PUT, "/foo", 204)
// reg.GetServer()
//
// will create a http server that returns 204 on calling GET "/foo" and fails the test on anything else
func (reg *Registry) AddBody(body []byte) {
request := NewRequest().WithBody(body)
reg.matches = append(reg.matches, newConsumableResponsesMatch(request, []Response{OkResponse}))
}
// AddRequest adds to the registry a 200 response for a generic request that needs to be matched
//
// reg := httpregistry.NewRegistry(t)
// reg.AddRequest(
// httpregistry.NewRequest(GET, "/foo", httpregistry.WithRequestHeader("header", "value"))
// )
// reg.GetServer()
//
// will create a http server that returns 200 on calling GET "/foo" with the correct header and fails the test on anything else
func (reg *Registry) AddRequest(request Request) {
reg.matches = append(reg.matches, newConsumableResponsesMatch(request, []Response{OkResponse}))
}
// AddRequestWithResponse adds to the registry a generic response for a generic request that needs to be matched
//
// reg := httpregistry.NewRegistry(t)
// reg.AddRequest(
// httpregistry.NewRequest(GET, "/foo", httpregistry.WithRequestHeader("header", "value")),
// httpregistry.NewResponse(http.StatusCreated, []byte{"hello"}),
// )
// reg.GetServer()
//
// will create a http server that returns 204 with "hello" as body on calling GET "/foo" with the correct header and fails the test on anything else
func (reg *Registry) AddRequestWithResponse(request Request, response Response) {
reg.matches = append(reg.matches, newConsumableResponsesMatch(request, []Response{response}))
}
// AddRequestWithResponses adds to the registry multiple responses for a generic request that needs to be matched.
// The responses are consumed by the calls so if more calls than responses will happen then the test will fail
//
// reg := httpregistry.NewRegistry(t)
// reg.AddRequestWithResponses(
// httpregistry.NewRequest(GET, "/foo", httpregistry.WithRequestHeader("header", "value")),
// httpregistry.NewResponse(http.StatusCreated, []byte{"hello"}),
// httpregistry.NewResponse(http.Ok, []byte{"hello again"}),
// )
// reg.GetServer()
//
// will create a http server that returns 204 with "hello" as body on calling GET "/foo" the first call with the correct header,
// it returns 200 with "hello again" as body on the second call with the correct header and fails the test on anything else
func (reg *Registry) AddRequestWithResponses(request Request, responses ...Response) {
reg.matches = append(reg.matches, newConsumableResponsesMatch(request, responses))
}
// GetMatchesForRequest returns the *http.Request that matched a generic Request
func (reg *Registry) GetMatchesForRequest(r Request) []*http.Request {
for _, match := range reg.matches {
if match.Request().Equal(r) {
return match.Matches()
}
}
return []*http.Request{}
}
// GetMatchesForURL returns the http.Requests that matched a specific URL independently of the method used to call it
func (reg *Registry) GetMatchesForURL(url string) []*http.Request {
for _, match := range reg.matches {
r := match.Request()
if r.urlAsRegex.MatchString(url) {
return match.Matches()
}
}
return []*http.Request{}
}
// GetMatchesURLAndMethod returns the http.Requests that matched a specific method, URL pair
func (reg *Registry) GetMatchesURLAndMethod(url string, method string) []*http.Request {
for _, match := range reg.matches {
r := match.Request()
if r.urlAsRegex.MatchString(url) && r.Method == method {
return match.Matches()
}
}
return []*http.Request{}
}
// doesRegisteredMatchMatchIncomingRequest checks if the incoming request is a match for the match that we are currently evaluating.
// If it is not a match this function will return a slice of miss objects that explain why the match is not possible.
func doesRegisteredMatchMatchIncomingRequest(registeredMatch match, r *http.Request) (bool, []miss) {
// The default request matches everything so no point in checking further
if registeredMatch.Request().Equal(NewRequest()) {
return true, nil
}
expectedURL := registeredMatch.Request().URL
expectedURLAsRegex := registeredMatch.Request().urlAsRegex
expectedMethod := registeredMatch.Request().Method
expectedHeaders := registeredMatch.Request().Headers
expectedBody := registeredMatch.Request().Body
misses := []miss{}
// if the match contains the default values then there is no point in saying that something was missed
if expectedURL != "" {
if expectedURLAsRegex.MatchString(r.URL.String()) {
return true, nil
}
misses = append(misses, newMiss(registeredMatch, pathDoesNotMatch))
}
if expectedMethod != "" {
if expectedMethod == r.Method {
return true, nil
}
misses = append(misses, newMiss(registeredMatch, methodDoesNotMatch))
}
if len(expectedBody) > 0 {
body, err := io.ReadAll(r.Body)
if err != nil {
panic(fmt.Errorf("cannot read the body of the request: %w", err))
}
if reflect.DeepEqual(expectedBody, body) {
return true, nil
}
}
if len(expectedHeaders) > 0 {
headersMisses := []miss{}
for headerToMatch, valueToMatch := range expectedHeaders {
value := r.Header.Get(headerToMatch)
if value == "" || value != valueToMatch {
misses = append(misses, newMiss(registeredMatch, headerDoesNotMatch))
}
}
if len(headersMisses) == 0 {
return true, nil
}
misses = append(misses, headersMisses...)
}
return false, misses
}
// GetServer returns a httptest.Server designed to match all the requests registered with the Registry
func (reg *Registry) GetServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// We reset the misses since if a previous request matched it is pointless to record that some of the mocks did not match it.
// If said request did not match then the test would have crashed in any case so the information in misses is useless.
reg.misses = []miss{}
for _, possibleMatch := range reg.matches {
doesMatch, misses := doesRegisteredMatchMatchIncomingRequest(possibleMatch, r)
if !doesMatch {
reg.misses = append(reg.misses, misses...)
continue
}
response, err := possibleMatch.NextResponse()
if err != nil {
if errors.Is(errNoNextResponseFound, err) {
reg.misses = append(reg.misses, newMiss(possibleMatch, outOfResponses))
continue
}
}
possibleMatch.RecordMatch(r)
for k, v := range response.Headers {
w.Header().Add(k, v)
}
w.WriteHeader(response.StatusCode)
_, err = w.Write(response.Body)
if err != nil {
panic("cannot write body of request")
}
return
}
res, err := httputil.DumpRequest(r, true)
if err != nil {
reg.t.Errorf("impossible to dump http request with error: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
reg.t.Errorf("no registered request matched %v\n The reasons why this is the case are returned in the body", string(res))
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(reg.Why()))
}))
}
// CheckAllResponsesAreConsumed fails the test if there are unused responses at the end of the test.
// This is useful to check if all the expected calls happened or if there is an unexpected behavior happening.
func (reg *Registry) CheckAllResponsesAreConsumed() {
for _, match := range reg.matches {
response, err := match.NextResponse()
if err == nil {
reg.t.Errorf("request %v has %v as unused response", match.Request(), response)
}
}
}
// Why returns a string that contains all the reasons why the request submitted to the registry failed to match with the registered requests.
// The envision use of this function is just as a helper when debugging the tests,
// most of the time it might not be obvious if there is a typo or a small error.
func (reg *Registry) Why() string {
output, err := json.Marshal(reg.misses)
if err != nil {
reg.t.Errorf("impossible to serialize matches to json: %v", err)
}
return string(output)
}