Codebase list golang-github-dnaeon-go-vcr / upstream/1.1.0
Import upstream version 1.1.0 Debian Janitor 2 years ago
10 changed file(s) with 397 addition(s) and 15 deletion(s). Raw diff Collapse all Expand all
00 language: go
11
22 go:
3 - 1.15
4 - 1.14
5 - 1.13
6 - 1.12
7 - 1.11
8 - 1.10
39 - 1.9
410 - 1.8
511 - 1.7
33 [![GoDoc](https://godoc.org/github.com/dnaeon/go-vcr?status.svg)](https://godoc.org/github.com/dnaeon/go-vcr)
44 [![Go Report Card](https://goreportcard.com/badge/github.com/dnaeon/go-vcr)](https://goreportcard.com/report/github.com/dnaeon/go-vcr)
55 [![codecov](https://codecov.io/gh/dnaeon/go-vcr/branch/master/graph/badge.svg)](https://codecov.io/gh/dnaeon/go-vcr)
6
76
87 `go-vcr` simplifies testing by recording your HTTP interactions and
98 replaying them in future runs in order to provide fast, deterministic
7574 ```
7675
7776 ## Custom Request Matching
77
7878 During replay mode, You can customize the way incoming requests are
7979 matched against the recorded request/response pairs by defining a
8080 Matcher function. For example, the following matcher will match on
8888 defer r.Stop() // Make sure recorder is stopped once done with it
8989
9090 r.SetMatcher(func(r *http.Request, i cassette.Request) bool {
91 var b bytes.Buffer
91 if r.Body == nil {
92 return cassette.DefaultMatcher(r, i)
93 }
94 var b bytes.Buffer
9295 if _, err := b.ReadFrom(r.Body); err != nil {
9396 return false
9497 }
97100 })
98101 ```
99102
103 ## Protecting Sensitive Data
104
105 You often provide sensitive data, such as API credentials, when making
106 requests against a service.
107 By default, this data will be stored in the recorded data but you probably
108 don't want this.
109 Removing or replacing data before it is stored can be done by adding one or
110 more `Filter`s to your `Recorder`.
111 Here is an example that removes the `Authorization` header from all requests:
112
113 ```go
114 r, err := recorder.New("fixtures/filters")
115 if err != nil {
116 log.Fatal(err)
117 }
118 defer r.Stop() // Make sure recorder is stopped once done with it
119
120 // Add a filter which removes Authorization headers from all requests:
121 r.AddFilter(func(i *cassette.Interaction) error {
122 delete(i.Request.Headers, "Authorization")
123 return nil
124 })
125 ```
126
127 ### Sensitive data in responses
128
129 Filters added using `*Recorder.AddFilter` are applied within VCR's custom `http.Transport`. This means that if you edit a response in such a filter then subsequent test code will see the edited response. This may not be desirable in all cases. For instance, if a response body contains an OAuth access token that is needed for subsequent requests, then redact the access token in `SaveFilter` will result in authorization failures.
130
131 Another way to edit recorded interactions is to use `*Recorder.AddSaveFilter`. Filters added with this method are applied just before interactions are saved when `*Recorder.Stop` is called.
132
133 ```go
134 r, err := recorder.New("fixtures/filters")
135 if err != nil {
136 log.Fatal(err)
137 }
138 defer r.Stop() // Make sure recorder is stopped once done with it
139
140 // Your test code will continue to see the real access token and
141 // it is redacted before the recorded interactions are saved
142 r.AddSaveFilter(func(i *cassette.Interaction) error {
143 if strings.Contains(i.URL, "/oauth/token") {
144 i.Response.Body = `{"access_token": "[REDACTED]"}`
145 }
146
147 return nil
148 })
149 ```
150
151 ## Passing Through Requests
152
153 Sometimes you want to allow specific requests to pass through to the remote
154 server without recording anything.
155
156 Globally, you can use `ModeDisabled` for this, but if you want to disable the
157 recorder for individual requests, you can add `Passthrough` functions to the
158 recorder. The function takes a pointer to the original request, and returns a
159 boolean, indicating if the request should pass through to the remote server.
160
161 Here's an example to pass through requests to a specific endpoint:
162
163 ```go
164 // Pass through the request to the remote server if the path matches "/login".
165 r.AddPassthrough(func(req *http.Request) bool {
166 return req.URL.Path == "/login"
167 })
168 ```
169
100170 ## License
101171
102172 `go-vcr` is Open Source and licensed under the
8383
8484 // Response duration (something like "100ms" or "10s")
8585 Duration string `yaml:"duration"`
86
87 replayed bool
8688 }
8789
8890 // Interaction type contains a pair of request/response for a
103105 return r.Method == i.Method && r.URL.String() == i.URL
104106 }
105107
108 // Filter function allows modification of an interaction before saving.
109 type Filter func(*Interaction) error
110
106111 // Cassette type
107112 type Cassette struct {
108113 // Name of the cassette
122127
123128 // Matches actual request with interaction requests.
124129 Matcher Matcher `yaml:"-"`
130
131 // Filters interactions before when they are captured.
132 Filters []Filter `yaml:"-"`
133
134 // SaveFilters are applied to interactions just before they are saved.
135 SaveFilters []Filter `yaml:"-"`
125136 }
126137
127138 // New creates a new empty cassette
132143 Version: cassetteFormatV1,
133144 Interactions: make([]*Interaction, 0),
134145 Matcher: DefaultMatcher,
146 Filters: make([]Filter, 0),
147 SaveFilters: make([]Filter, 0),
135148 }
136149
137150 return c
159172
160173 // GetInteraction retrieves a recorded request/response interaction
161174 func (c *Cassette) GetInteraction(r *http.Request) (*Interaction, error) {
162 c.Mu.RLock()
163 defer c.Mu.RUnlock()
175 c.Mu.Lock()
176 defer c.Mu.Unlock()
164177 for _, i := range c.Interactions {
165 if c.Matcher(r, i.Request) {
178 if !i.replayed && c.Matcher(r, i.Request) {
179 i.replayed = true
166180 return i, nil
167181 }
168182 }
179193 return nil
180194 }
181195
196 for _, interaction := range c.Interactions {
197 for _, filter := range c.SaveFilters {
198 if err := filter(interaction); err != nil {
199 return err
200 }
201 }
202 }
203
182204 // Create directory for cassette if missing
183205 cassetteDir := filepath.Dir(c.File)
184206 if _, err := os.Stat(cassetteDir); os.IsNotExist(err) {
0 module github.com/dnaeon/go-vcr
1
2 go 1.15
3
4 require (
5 github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5
6 gopkg.in/yaml.v2 v2.2.1
7 )
0 github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA=
1 github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
2 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3 gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
4 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
0 // Copyright (c) 2015-2016 Marin Atanasov Nikolov <dnaeon@gmail.com>
1 // Copyright (c) 2016 David Jack <davars@gmail.com>
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions
6 // are met:
7 // 1. Redistributions of source code must retain the above copyright
8 // notice, this list of conditions and the following disclaimer
9 // in this position and unchanged.
10 // 2. Redistributions in binary form must reproduce the above copyright
11 // notice, this list of conditions and the following disclaimer in the
12 // documentation and/or other materials provided with the distribution.
13 //
14 // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
15 // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
16 // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
17 // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
18 // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
19 // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
20 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
21 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
23 // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 //
25 // +build !go1.8
26
27 package recorder
28
29 import (
30 "io"
31 )
32
33 // isNoBody returns true iff r is an http.NoBody.
34 // http.NoBody didn't exist before Go 1.7, so the version in this file
35 // always returns false.
36 func isNoBody(r io.ReadCloser) bool { return false }
0 // Copyright (c) 2015-2016 Marin Atanasov Nikolov <dnaeon@gmail.com>
1 // Copyright (c) 2016 David Jack <davars@gmail.com>
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions
6 // are met:
7 // 1. Redistributions of source code must retain the above copyright
8 // notice, this list of conditions and the following disclaimer
9 // in this position and unchanged.
10 // 2. Redistributions in binary form must reproduce the above copyright
11 // notice, this list of conditions and the following disclaimer in the
12 // documentation and/or other materials provided with the distribution.
13 //
14 // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
15 // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
16 // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
17 // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
18 // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
19 // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
20 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
21 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
23 // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 //
25 // +build go1.8
26
27 package recorder
28
29 import (
30 "io"
31 "net/http"
32 )
33
34 // isNoBody returns true iff r is an http.NoBody.
35 func isNoBody(r io.ReadCloser) bool { return r == http.NoBody }
6060
6161 // realTransport is the underlying http.RoundTripper to make real requests
6262 realTransport http.RoundTripper
63 }
63
64 // Pass through requests.
65 Passthroughs []Passthrough
66 }
67
68 // Passthrough function allows ignoring certain requests.
69 type Passthrough func(*http.Request) bool
6470
6571 // SetTransport can be used to configure the behavior of the 'real' client used in record-mode
6672 func (r *Recorder) SetTransport(t http.RoundTripper) {
7177 func requestHandler(r *http.Request, c *cassette.Cassette, mode Mode, realTransport http.RoundTripper) (*cassette.Interaction, error) {
7278 // Return interaction from cassette if in replay mode
7379 if mode == ModeReplaying {
80 if err := r.Context().Err(); err != nil {
81 return nil, err
82 }
7483 return c.GetInteraction(r)
7584 }
7685
92101 }
93102
94103 reqBody := &bytes.Buffer{}
95 if r.Body != nil {
104 if r.Body != nil && !isNoBody(r.Body) {
96105 // Record the request body so we can add it to the cassette
97106 r.Body = ioutil.NopCloser(io.TeeReader(r.Body, reqBody))
98107 }
103112 if err != nil {
104113 return nil, err
105114 }
115 defer resp.Body.Close()
106116
107117 respBody, err := ioutil.ReadAll(resp.Body)
108118 if err != nil {
125135 Code: resp.StatusCode,
126136 },
127137 }
138 for _, filter := range c.Filters {
139 err = filter(interaction)
140 if err != nil {
141 return nil, err
142 }
143 }
128144 c.AddInteraction(interaction)
129145
130146 return interaction, nil
187203 if r.mode == ModeDisabled {
188204 return r.realTransport.RoundTrip(req)
189205 }
206 for _, passthrough := range r.Passthroughs {
207 if passthrough(req) {
208 return r.realTransport.RoundTrip(req)
209 }
210 }
211
190212 // Pass cassette and mode to handler, so that interactions can be
191213 // retrieved or recorded depending on the current recorder mode
192214 interaction, err := requestHandler(req, r.cassette, r.mode, r.realTransport)
253275 r.cassette.Matcher = matcher
254276 }
255277 }
278
279 // AddPassthrough appends a hook to determine if a request should be ignored by the
280 // recorder.
281 func (r *Recorder) AddPassthrough(pass Passthrough) {
282 r.Passthroughs = append(r.Passthroughs, pass)
283 }
284
285 // AddFilter appends a hook to modify a request before it is recorded.
286 //
287 // Filters are useful for filtering out sensitive parameters from the recorded data.
288 func (r *Recorder) AddFilter(filter cassette.Filter) {
289 if r.cassette != nil {
290 r.cassette.Filters = append(r.cassette.Filters, filter)
291 }
292 }
293
294 // AddSaveFilter appends a hook to modify a request before it is saved.
295 //
296 // This filter is suitable for treating recorded responses to remove sensitive data. Altering responses using a regular
297 // AddFilter can have unintended consequences on code that is consuming responses.
298 func (r *Recorder) AddSaveFilter(filter cassette.Filter) {
299 if r.cassette != nil {
300 r.cassette.SaveFilters = append(r.cassette.SaveFilters, filter)
301 }
302 }
303
304 // Mode returns recorder state
305 func (r *Recorder) Mode() Mode {
306 return r.mode
307 }
8080 func TestRecord(t *testing.T) {
8181 runID, cassPath, tests := setupTests(t, "record_test")
8282
83 serverURL := httpRecorderTest(t, runID, tests, cassPath, recorder.ModeRecording)
83 r, serverURL := httpRecorderTest(t, runID, tests, cassPath, recorder.ModeRecording)
8484
8585 c, err := cassette.Load(cassPath)
8686 if err != nil {
8787 t.Fatal(err)
88 }
89
90 if m := r.Mode(); m != recorder.ModeRecording {
91 t.Fatalf("Expected recording mode, got %v", m)
8892 }
8993
9094 for i, test := range tests {
9599 }
96100
97101 // Re-run without the actual server
98 r, err := recorder.New(cassPath)
102 r, err = recorder.New(cassPath)
99103 if err != nil {
100104 t.Fatal(err)
101105 }
102106 defer r.Stop()
107
108 if m := r.Mode(); m != recorder.ModeReplaying {
109 t.Fatalf("Expected replaying mode, got %v", m)
110 }
103111
104112 // Use a custom matcher that includes matching on request body
105113 r.SetMatcher(func(r *http.Request, i cassette.Request) bool {
121129 func TestModeContextTimeout(t *testing.T) {
122130 // Record initial requests
123131 runID, cassPath, tests := setupTests(t, "record_playback_timeout")
124 serverURL := httpRecorderTest(t, runID, tests, cassPath, recorder.ModeReplaying)
132 _, serverURL := httpRecorderTest(t, runID, tests, cassPath, recorder.ModeReplaying)
125133
126134 // Re-run without the actual server
127135 r, err := recorder.New(cassPath)
134142 ctx, cancelFn := context.WithCancel(context.Background())
135143 cancelFn()
136144 _, err := test.performReq(t, ctx, serverURL, r)
137 if err == nil {
138 t.Fatal("Expected cancellation error")
145 if err == nil || err == cassette.ErrInteractionNotFound {
146 t.Fatalf("Expected cancellation error, got %v", err)
139147 }
140148 }
141149 }
173181 func TestModeDisabled(t *testing.T) {
174182 runID, cassPath, tests := setupTests(t, "record_disabled_test")
175183
176 httpRecorderTest(t, runID, tests, cassPath, recorder.ModeDisabled)
184 r, _ := httpRecorderTest(t, runID, tests, cassPath, recorder.ModeDisabled)
185
186 if m := r.Mode(); m != recorder.ModeDisabled {
187 t.Fatalf("Expected disabled mode, got %v", m)
188 }
177189
178190 _, err := cassette.Load(cassPath)
179191 // Expect the file to not exist if record is disabled
180192 if !os.IsNotExist(err) {
181193 t.Fatal(err)
194 }
195 }
196
197 func TestPassthrough(t *testing.T) {
198 runID, cassPath, tests := setupTests(t, "test_passthrough")
199 recorder, server := httpRecorderTestSetup(t, runID, cassPath, recorder.ModeRecording)
200 serverURL := server.URL
201
202 // Add a passthrough configuration which does not record any requests with
203 // a specific body.
204 recorder.AddPassthrough(func(r *http.Request) bool {
205 if r.Body == nil {
206 return false
207 }
208 var b bytes.Buffer
209 if _, err := b.ReadFrom(r.Body); err != nil {
210 return false
211 }
212 r.Body = ioutil.NopCloser(&b)
213
214 return b.String() == "alt body"
215 })
216
217 t.Log("make http requests")
218 for _, test := range tests {
219 test.perform(t, serverURL, recorder)
220 }
221
222 // Make sure recorder is stopped once done with it
223 server.Close()
224 t.Log("server shut down")
225
226 recorder.Stop()
227 t.Log("recorder stopped")
228
229 // Load the cassette we just stored:
230 c, err := cassette.Load(cassPath)
231 if err != nil {
232 t.Fatal(err)
233 }
234
235 // Assert that no body exists matching our pass through test
236 for _, i := range c.Interactions {
237 body := i.Request.Body
238 if body == "alt body" {
239 t.Fatalf("unexpected recording:\t%s", body)
240 }
241 }
242 }
243
244 func TestFilter(t *testing.T) {
245 dummyBody := "[REDACTED]"
246
247 runID, cassPath, tests := setupTests(t, "test_filter")
248 recorder, server := httpRecorderTestSetup(t, runID, cassPath, recorder.ModeRecording)
249 serverURL := server.URL
250
251 // Add a filter which replaces each request body in the stored cassette:
252 recorder.AddFilter(func(i *cassette.Interaction) error {
253 i.Request.Body = dummyBody
254 return nil
255 })
256
257 t.Log("make http requests")
258 for _, test := range tests {
259 test.perform(t, serverURL, recorder)
260 }
261
262 // Make sure recorder is stopped once done with it
263 server.Close()
264 t.Log("server shut down")
265
266 recorder.Stop()
267 t.Log("recorder stopped")
268
269 // Load the cassette we just stored:
270 c, err := cassette.Load(cassPath)
271 if err != nil {
272 t.Fatal(err)
273 }
274
275 // Assert that each body has been set to our dummy value
276 for i := range tests {
277 body := c.Interactions[i].Request.Body
278 if body != dummyBody {
279 t.Fatalf("got:\t%s\n\twant:\t%s", string(body), string(dummyBody))
280 }
281 }
282 }
283
284 func TestSaveFilter(t *testing.T) {
285 dummyBody := "[REDACTED]"
286
287 runID, cassPath, tests := setupTests(t, "test_save_filter")
288 recorder, server := httpRecorderTestSetup(t, runID, cassPath, recorder.ModeRecording)
289 serverURL := server.URL
290
291 // Add a filter which replaces each request body in the stored cassette:
292 recorder.AddSaveFilter(func(i *cassette.Interaction) error {
293 i.Response.Body = dummyBody
294 return nil
295 })
296
297 t.Log("make http requests")
298 for _, test := range tests {
299 test.perform(t, serverURL, recorder)
300 }
301
302 // Make sure recorder is stopped once done with it
303 server.Close()
304 t.Log("server shut down")
305
306 recorder.Stop()
307 t.Log("recorder stopped")
308
309 // Load the cassette we just stored:
310 c, err := cassette.Load(cassPath)
311 if err != nil {
312 t.Fatal(err)
313 }
314
315 // Assert that each body has been set to our dummy value
316 for i := range tests {
317 body := c.Interactions[i].Response.Body
318 if body != dummyBody {
319 t.Fatalf("got:\t%s\n\twant:\t%s", string(body), string(dummyBody))
320 }
182321 }
183322 }
184323
201340 return recorder, server
202341 }
203342
204 func httpRecorderTest(t *testing.T, runID string, tests []recordTest, cassPath string, mode recorder.Mode) string {
343 func httpRecorderTest(t *testing.T, runID string, tests []recordTest, cassPath string, mode recorder.Mode) (*recorder.Recorder, string) {
205344 recorder, server := httpRecorderTestSetup(t, runID, cassPath, mode)
206345 serverURL := server.URL
207346
217356 recorder.Stop()
218357 t.Log("recorder stopped")
219358
220 return serverURL
359 return recorder, serverURL
221360 }
222361
223362 func (test recordTest) perform(t *testing.T, url string, r *recorder.Recorder) {
0 // +build tools
1
2 package tools
3
4 import (
5 _ "github.com/modocache/gover"
6 )