-
Notifications
You must be signed in to change notification settings - Fork 4
/
http.go
350 lines (326 loc) · 11.4 KB
/
http.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
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
package argot
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"reflect"
"regexp"
"strings"
"github.com/kylelemons/godebug/pretty"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/xeipuuv/gojsonschema"
)
// HttpCall captures all the state relating to a single HTTP call. It
// may be used multiple times. An HttpCall can only be used by a
// single go-routine at a time.
type HttpCall struct {
// The client used to perform the request.
Client *http.Client
// The request to be made.
Request *http.Request
// The response.
Response *http.Response
// The body which once received can be repeatedly reused.
ResponseBody []byte
}
// NewHttpCall creates a new HttpCall. If client is nil, a new
// http.Client is used.
func NewHttpCall(client *http.Client) *HttpCall {
if client == nil {
client = new(http.Client)
}
return &HttpCall{
Client: client,
}
}
// AssertNoRequest returns nil iff hc.Request is nil.
func (hc *HttpCall) AssertNoRequest() error {
if hc.Request == nil {
return nil
} else {
return errors.New("Request already set")
}
}
// AssertRequest returns nil iff hc.Request is non-nil.
func (hc *HttpCall) AssertRequest() error {
if hc.Request == nil {
return errors.New("No Request set")
} else {
return nil
}
}
// AssertNoResponse returns nil iff hc.Response is nil.
func (hc *HttpCall) AssertNoResponse() error {
if hc.Response == nil {
return nil
} else {
return errors.New("Response already set")
}
}
// EnsureResponse is idempotent. If there is already a response then
// it will return nil. Otherwise if there is no Request then it will
// return non-nil. Otherwise it will use hc.Client.Do to perform the
// request, set hc.Response, and return any error that occurs.
//
// Always use this in any step where you want to inspect the
// hc.Response.
func (hc *HttpCall) EnsureResponse() error {
if hc.Response != nil {
return nil
} else if hc.Request == nil {
return errors.New("Cannot ensure response: no request.")
} else if response, err := hc.Client.Do(hc.Request); err != nil {
safeURL := *hc.Request.URL
safeURL.User = nil
return fmt.Errorf("Error when making call of %v: %v", safeURL, err)
} else {
hc.Response = response
return nil
}
}
// ReceiveBody is idempotent. It will ensure there is a response using
// hc.EnsureResponse. If there is already a non-nil hc.ResponseBody
// then it will return nil. Otherwise it will receive the
// Response.Body, store it in hc.ResponseBody, and return any error
// that occurs.
//
// Always use this in any step where you want to inspect the
// hc.ResponseBody.
func (hc *HttpCall) ReceiveBody() error {
if err := hc.EnsureResponse(); err != nil {
return err
} else if hc.ResponseBody != nil {
return nil
} else {
defer hc.Response.Body.Close()
bites := new(bytes.Buffer)
if _, err = io.Copy(bites, hc.Response.Body); err != nil {
return err
} else {
hc.ResponseBody = bites.Bytes()
return nil
}
}
}
// Reset is idempotent. You should ensure this is called at the end of
// life for each HttpCall. It drains Response bodies if necessary, and
// cleans up resources.
func (hc *HttpCall) Reset() error {
hc.Request = nil
if hc.Response != nil && hc.ResponseBody == nil {
io.Copy(ioutil.Discard, hc.Response.Body)
hc.Response.Body.Close()
}
hc.Response = nil
hc.ResponseBody = nil
return nil
}
// NewRequest is a Step that when executed will create a new request
// using the given parameters. The step will automatically call
// hc.Reset to tidy up any previous use of hc, and thus prepare hc for
// the new request.
func (hc *HttpCall) NewRequest(method, urlStr string, body io.Reader) Step {
return NewNamedStep(fmt.Sprintf("NewRequest(%s: %s)", method, urlStr), func() error {
if err := hc.Reset(); err != nil {
return err
} else if req, err := http.NewRequest(method, urlStr, body); err != nil {
return err
} else {
hc.Request = req
return nil
}
})
}
// RequestHeader is a Step that when executed will set the given key
// and value as a header on the HTTP Request. This can only be done
// after hc.Request has been created (with NewRequest), and before
// hc.Response has been created.
func (hc *HttpCall) RequestHeader(key, value string) Step {
return NewNamedStep(fmt.Sprintf("RequestHeader(%s: %s)", key, value), func() error {
if err := AnyError(hc.AssertRequest(), hc.AssertNoResponse()); err != nil {
return err
} else {
hc.Request.Header.Set(key, value)
return nil
}
})
}
// Call is a Step that when executed performs the HTTP Request
// Call. This is not normally necessary: all steps that require a
// Response will perform the HTTP Request when necessary. However, in
// some tests, you may not care about inspecting the HTTP Response but
// nevertheless wish the HTTP Request to be made.
func (hc *HttpCall) Call() Step {
return NewNamedStep("Call", hc.EnsureResponse)
}
// ResponseStatusEquals is a Step that when executed ensures there is
// a non-nil hc.Response and errors unless the hc.Response.StatusCode
// equals the status parameter.
func (hc *HttpCall) ResponseStatusEquals(status int) Step {
return NewNamedStep(fmt.Sprintf("ResponseStatusEquals(%d)", status), func() error {
if err := hc.EnsureResponse(); err != nil {
return err
} else if hc.Response.StatusCode != status {
return fmt.Errorf("Status: Expected %d; found %d.", status, hc.Response.StatusCode)
} else {
return nil
}
})
}
// ResponseHeaderExists is a Step that when executed ensures there is
// a non-nil hc.Response and errors unless hc.Response.Header[key]
// exists. It says nothing about the value of the header.
func (hc *HttpCall) ResponseHeaderExists(key string) Step {
return NewNamedStep(fmt.Sprintf("ResponseHeaderExists(%s)", key), func() error {
if err := hc.EnsureResponse(); err != nil {
return err
} else if _, found := hc.Response.Header[key]; !found {
return fmt.Errorf("Header '%s' not found.", key)
} else {
return nil
}
})
}
// ResponseHeaderNotExists is a Step that when executed ensures there
// is a non-nil hc.Response and errors unless hc.Response.Header[key]
// does not exist.
func (hc *HttpCall) ResponseHeaderNotExists(key string) Step {
return NewNamedStep(fmt.Sprintf("ResponseHeaderNotExists(%s)", key), func() error {
if err := hc.EnsureResponse(); err != nil {
return err
} else if _, found := hc.Response.Header[key]; found {
return fmt.Errorf("Header '%s' found.", key)
} else {
return nil
}
})
}
// Diff two strings, output as coloured string, the expected parts will
// be removed/red if they're missing, the found/inserted parts will be
// green if present, if the parts are the same, no colour is applied.
func diff(expected string, got string) string {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(expected, got, false)
return dmp.DiffPrettyText(diffs)
}
// ResponseHeaderEquals is a Step that when executed ensures there is
// a non-nil hc.Response and errors unless the
// hc.Response.Header.Get(key) equals the value parameter. Note this
// is an exact match.
func (hc *HttpCall) ResponseHeaderEquals(key, value string) Step {
return NewNamedStep(fmt.Sprintf("ResponseHeaderEquals(%s: %s)", key, value), func() error {
if err := hc.EnsureResponse(); err != nil {
return err
} else if header := hc.Response.Header.Get(key); header != value {
return fmt.Errorf("Header: '%s': Diff: '%s'.", key, diff(value, header))
} else {
return nil
}
})
}
// ResponseHeaderContains is a Step that when executed ensures there
// is a non-nil hc.Response and errors unless the
// hc.Response.Header.Get(key) contains the value parameter using
// strings.Contains.
func (hc *HttpCall) ResponseHeaderContains(key, value string) Step {
return NewNamedStep(fmt.Sprintf("ResponseHeaderContains(%s: %s)", key, value), func() error {
if err := hc.EnsureResponse(); err != nil {
return err
} else if header := hc.Response.Header.Get(key); !strings.Contains(header, value) {
return fmt.Errorf("Header '%s': Expected '%s'; found '%s'.", key, value, header)
} else {
return nil
}
})
}
// ResponseBodyEquals is a Step that when executed ensures there is a
// non-nil hc.ResponseBody and errors unless the hc.ResponseBody
// equals the value parameter. Note this is an exact match.
func (hc *HttpCall) ResponseBodyEquals(value string) Step {
return NewNamedStep("ResponseBodyEquals", func() error {
if err := hc.ReceiveBody(); err != nil {
return err
} else if bodyStr := string(hc.ResponseBody); bodyStr != value {
return fmt.Errorf("Body: Diff: '%s'.", diff(value, bodyStr))
} else {
return nil
}
})
}
// ResponseBodyContains is a Step that when executed ensures there is
// a non-nil hc.ResponseBody and errors unless the hc.ResponseBody
// contains the value parameter using strings.Contains.
func (hc *HttpCall) ResponseBodyContains(value string) Step {
return NewNamedStep("ResponseBodyContains", func() error {
if err := hc.ReceiveBody(); err != nil {
return err
} else if !strings.Contains(string(hc.ResponseBody), value) {
return fmt.Errorf("Body: Expected '%s'; found '%s'.", value, string(hc.ResponseBody))
} else {
return nil
}
})
}
// ResponseBodyMatches is a Step that when executed ensures there is
// a non-nil hc.ResponseBody and errors unless the hc.ResponseBody
// matches the regular expression parameter.
func (hc *HttpCall) ResponseBodyMatches(pattern *regexp.Regexp) Step {
return NewNamedStep(fmt.Sprintf("ResponseBodyMatches(%v)", pattern), func() error {
if err := hc.ReceiveBody(); err != nil {
return err
} else if !pattern.MatchString(string(hc.ResponseBody)) {
return fmt.Errorf("Body: Expected to match the pattern '%v'; found '%s'.", pattern, string(hc.ResponseBody))
} else {
return nil
}
})
}
// ResponseBodyJSONSchema is a Step that when executed ensures there
// is a non-nil hc.ResponseBody and errors unless the hc.ResponseBody
// can be validated against the schema parameter using gojsonschema.
func (hc *HttpCall) ResponseBodyJSONSchema(schema string) Step {
return NewNamedStep("ResponseBodyJSONSchema", func() error {
if err := hc.ReceiveBody(); err != nil {
return err
} else {
schemaLoader := gojsonschema.NewStringLoader(schema)
bodyLoader := gojsonschema.NewStringLoader(string(hc.ResponseBody))
if result, err := gojsonschema.Validate(schemaLoader, bodyLoader); err != nil {
return err
} else if !result.Valid() {
msg := "Validation failure:\n"
for _, err := range result.Errors() {
msg += fmt.Sprintf("\t%v\n", err)
}
return errors.New(msg[:len(msg)-1])
} else {
return nil
}
}
})
}
// ResponseBodyJSONMatchesStruct is a Step that when executed ensures
// there is a non-nil hc.ResponseBody, parses it as JSON (via
// encoding/json) based on the type of the expected structure and errors
// unless it is equal to the expected value, as validated by the pretty
// package. The error will contain a structured diff output with a
// plus/"+" marking the values that were expected and a minus/"-"
// marking the values that were actually present.
func (hc *HttpCall) ResponseBodyJSONMatchesStruct(expected interface{}) Step {
return NewNamedStep("ResponseBodyJSONMatchesStruct", func() error {
parseAs := reflect.New(reflect.TypeOf(expected)).Interface()
if err := hc.ReceiveBody(); err != nil {
return err
} else if err := json.Unmarshal(hc.ResponseBody, parseAs); err != nil {
return err
} else if diff := pretty.Compare(parseAs, expected); diff != "" {
return fmt.Errorf("Did not match expected value: (-got +want)\n%s", diff)
} else {
return nil
}
})
}