Skip to content

Commit

Permalink
Merge pull request #55 from disintegrator/before-save-filters
Browse files Browse the repository at this point in the history
feat: add `*Recorder.AddSaveFilter` to redact interactions before saving
  • Loading branch information
dnaeon authored Oct 10, 2020
2 parents 96fa97c + d50a810 commit fbfc31c
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 1 deletion.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,30 @@ r.AddFilter(func(i *cassette.Interaction) error {
})
```

### Sensitive data in responses

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.

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.

```go
r, err := recorder.New("fixtures/filters")
if err != nil {
log.Fatal(err)
}
defer r.Stop() // Make sure recorder is stopped once done with it

// Your test code will continue to see the real access token and
// it is redacted before the recorded interactions are saved
r.AddSaveFilter(func(i *cassette.Interaction) error {
if strings.Contains(i.URL, "/oauth/token") {
i.Response.Body = `{"access_token": "[REDACTED]"}`
}

return nil
})
```

## Passing Through Requests

Sometimes you want to allow specific requests to pass through to the remote
Expand Down
14 changes: 13 additions & 1 deletion cassette/cassette.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,11 @@ type Cassette struct {
// Matches actual request with interaction requests.
Matcher Matcher `yaml:"-"`

// Filters interactions before being saved.
// Filters interactions before when they are captured.
Filters []Filter `yaml:"-"`

// SaveFilters are applied to interactions just before they are saved.
SaveFilters []Filter `yaml:"-"`
}

// New creates a new empty cassette
Expand All @@ -142,6 +145,7 @@ func New(name string) *Cassette {
Interactions: make([]*Interaction, 0),
Matcher: DefaultMatcher,
Filters: make([]Filter, 0),
SaveFilters: make([]Filter, 0),
}

return c
Expand Down Expand Up @@ -190,6 +194,14 @@ func (c *Cassette) Save() error {
return nil
}

for _, interaction := range c.Interactions {
for _, filter := range c.SaveFilters {
if err := filter(interaction); err != nil {
return err
}
}
}

// Create directory for cassette if missing
cassetteDir := filepath.Dir(c.File)
if _, err := os.Stat(cassetteDir); os.IsNotExist(err) {
Expand Down
10 changes: 10 additions & 0 deletions recorder/recorder.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,16 @@ func (r *Recorder) AddFilter(filter cassette.Filter) {
}
}

// AddSaveFilter appends a hook to modify a request before it is saved.
//
// This filter is suitable for treating recorded responses to remove sensitive data. Altering responses using a regular
// AddFilter can have unintended consequences on code that is consuming responses.
func (r *Recorder) AddSaveFilter(filter cassette.Filter) {
if r.cassette != nil {
r.cassette.SaveFilters = append(r.cassette.SaveFilters, filter)
}
}

// Mode returns recorder state
func (r *Recorder) Mode() Mode {
return r.mode
Expand Down
39 changes: 39 additions & 0 deletions recorder/recorder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,46 @@ func TestFilter(t *testing.T) {
t.Fatalf("got:\t%s\n\twant:\t%s", string(body), string(dummyBody))
}
}
}

func TestSaveFilter(t *testing.T) {
dummyBody := "[REDACTED]"

runID, cassPath, tests := setupTests(t, "test_save_filter")
recorder, server := httpRecorderTestSetup(t, runID, cassPath, recorder.ModeRecording)
serverURL := server.URL

// Add a filter which replaces each request body in the stored cassette:
recorder.AddSaveFilter(func(i *cassette.Interaction) error {
i.Response.Body = dummyBody
return nil
})

t.Log("make http requests")
for _, test := range tests {
test.perform(t, serverURL, recorder)
}

// Make sure recorder is stopped once done with it
server.Close()
t.Log("server shut down")

recorder.Stop()
t.Log("recorder stopped")

// Load the cassette we just stored:
c, err := cassette.Load(cassPath)
if err != nil {
t.Fatal(err)
}

// Assert that each body has been set to our dummy value
for i := range tests {
body := c.Interactions[i].Response.Body
if body != dummyBody {
t.Fatalf("got:\t%s\n\twant:\t%s", string(body), string(dummyBody))
}
}
}

func httpRecorderTestSetup(t *testing.T, runID string, cassPath string, mode recorder.Mode) (*recorder.Recorder, *httptest.Server) {
Expand Down

0 comments on commit fbfc31c

Please sign in to comment.