diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2e11c1a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: Test and Build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + + lint: + name: Linting + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v2 + + - name: Lint and Vet + uses: golangci/golangci-lint-action@v2 + with: + version: latest + args: --timeout=3m + + test: + name: Test + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.17 + + - name: Test + run: go test -count 1 -parallel 2 ./... diff --git a/Makefile b/Makefile index 7ac27a8..207fe9f 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ test: deps go test -timeout 20s -cover ./... # note: -race significantly degrades performance hence a high "timeout" value and reduced parallelism - go test -timeout 60s -cover $(GORACE) -parallel 2 ./... + go test -timeout 120s -cover $(GORACE) -parallel 2 ./... lint: deps ./golangci-lint.sh || : diff --git a/README.md b/README.md index 30d55b4..2bf6e31 100644 --- a/README.md +++ b/README.md @@ -93,10 +93,7 @@ Settings are populated via `With*` options: - `WithCassette` loads the specified cassette.\ Note that it is also possible to call `LoadCassette` from the vcr instance. - See `vcrsettings.go` for more options such as `WithRequestMatcher`, `WithTrackRecordingMutators`, `WithTrackReplayingMutators`, ... -- TODO in v5: `WithDisableRecording` disables track recording (but will replay matching tracks) - TODO in v5: `WithLogging` enables logging to help understand what govcr is doing internally. -- TODO in v5: `WithSaveTLS` enables saving TLS in the track response.\ - Note: this doesn't work well because of limitations in Go's json package and unspecified `any` in the PublicKey certificate struct. ## Match a request to a cassette track diff --git a/cassette/cassette.go b/cassette/cassette.go index f63781e..b5edc2e 100644 --- a/cassette/cassette.go +++ b/cassette/cassette.go @@ -12,6 +12,7 @@ import ( "sync" "sync/atomic" + "github.com/google/uuid" "github.com/pkg/errors" "github.com/seborama/govcr/v6/cassette/track" @@ -105,11 +106,15 @@ func (k7 *Cassette) ReplayTrack(trackNumber int32) (*track.Track, error) { // AddTrack to cassette. // Note that the Track does not receive mutations here, it must be mutated // before passed to the cassette for recording. -func (k7 *Cassette) AddTrack(track *track.Track) { +func (k7 *Cassette) AddTrack(trk *track.Track) { k7.trackSliceMutex.Lock() defer k7.trackSliceMutex.Unlock() - k7.Tracks = append(k7.Tracks, *track) + if trk.UUID == "" { + trk.UUID = uuid.NewString() + } + + k7.Tracks = append(k7.Tracks, *trk) } // IsLongPlay returns true if the cassette content is compressed. @@ -194,12 +199,12 @@ func transformInterfacesInJSON(jsonString []byte) []byte { } // AddTrackToCassette saves a new track using the specified details to a cassette. -func AddTrackToCassette(cassette *Cassette, aTrack *track.Track) error { +func AddTrackToCassette(cassette *Cassette, trk *track.Track) error { // mark track as replayed since it's coming from a live Request! - aTrack.SetReplayed(true) + trk.SetReplayed(true) // add track to cassette - cassette.AddTrack(aTrack) + cassette.AddTrack(trk) // save cassette return cassette.save() diff --git a/controlpanel.go b/controlpanel.go index d3ebbfa..2767d30 100644 --- a/controlpanel.go +++ b/controlpanel.go @@ -49,6 +49,16 @@ func (controlPanel *ControlPanel) AddRecordingMutators(trackMutators ...track.Mu controlPanel.vcrTransport().AddRecordingMutators(trackMutators...) } +// SetRecordingMutators replaces the set of recording Track Mutator's in the VCR. +func (controlPanel *ControlPanel) SetRecordingMutators(trackMutators ...track.Mutator) { + controlPanel.vcrTransport().SetRecordingMutators(trackMutators...) +} + +// ClearRecordingMutators clears the set of recording Track Mutator's from the VCR. +func (controlPanel *ControlPanel) ClearRecordingMutators() { + controlPanel.vcrTransport().ClearRecordingMutators() +} + // AddReplayingMutators adds a set of replaying Track Mutator's to the VCR. // Replaying happens AFTER the request has been matched. As such, while the track's Request // could be mutated, it will have no effect. @@ -57,6 +67,16 @@ func (controlPanel *ControlPanel) AddReplayingMutators(trackMutators ...track.Mu controlPanel.vcrTransport().AddReplayingMutators(trackMutators...) } +// SetReplayingMutators replaces the set of replaying Track Mutator's in the VCR. +func (controlPanel *ControlPanel) SetReplayingMutators(trackMutators ...track.Mutator) { + controlPanel.vcrTransport().SetReplayingMutators(trackMutators...) +} + +// ClearReplayingMutators clears the set of replaying Track Mutator's from the VCR. +func (controlPanel *ControlPanel) ClearReplayingMutators() { + controlPanel.vcrTransport().ClearReplayingMutators() +} + // HTTPClient returns the http.Client that contains the VCR. func (controlPanel *ControlPanel) HTTPClient() *http.Client { return controlPanel.client diff --git a/go.mod b/go.mod index 786f0e1..7593d51 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/seborama/govcr/v6 go 1.17 require ( + github.com/google/uuid v1.3.0 github.com/jinzhu/copier v0.3.5 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.0 diff --git a/go.sum b/go.sum index e78c663..da7a84e 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/govcr_test.go b/govcr_test.go index d4533c9..46f5578 100644 --- a/govcr_test.go +++ b/govcr_test.go @@ -232,7 +232,7 @@ func (suite *GoVCRTestSuite) TestVCR_OfflineMode() { // 3rd execution of set of calls -- still offline only // we've run out of tracks and we're in offline mode so we expect a transport error - req, err := http.NewRequest(http.MethodGet, suite.testServer.URL, nil) // +fmt.Sprintf("?i=%d", i), nil) + req, err := http.NewRequest(http.MethodGet, suite.testServer.URL, nil) suite.Require().NoError(err) resp, err := suite.vcr.HTTPClient().Do(req) suite.Require().Error(err) diff --git a/govcr_wb_test.go b/govcr_wb_test.go index 75dbabe..eaa79d3 100644 --- a/govcr_wb_test.go +++ b/govcr_wb_test.go @@ -1,10 +1,14 @@ package govcr import ( + "crypto/tls" + "crypto/x509" "fmt" "io/ioutil" + "mime/multipart" "net/http" "net/http/httptest" + "net/url" "os" "testing" "time" @@ -71,10 +75,140 @@ func (suite *GoVCRWBTestSuite) TearDownTest() { } func (suite *GoVCRWBTestSuite) TestRoundTrip_DoesNotChangeLiveRequestOrResponse() { + suite.vcr.SetLiveOnlyMode(true) // ensure we always make live calls for this test + + trackDestroyerMutator := track.Mutator( + func(trk *track.Track) { + newURL := url.URL{ + Scheme: "x", + Opaque: "y", + User: &url.Userinfo{}, + Host: "z", + Path: "a", + RawPath: "b", + ForceQuery: false, + RawQuery: "c", + Fragment: "d", + RawFragment: "e", + } + + trk.Request = track.Request{ + Method: "m", + URL: &newURL, + Proto: "p", + ProtoMajor: 2, + ProtoMinor: 3, + Header: map[string][]string{"h": {"v"}}, + Body: []byte("body bytes"), + ContentLength: 200, + TransferEncoding: []string{"x"}, + Close: false, + Host: "y", + Form: map[string][]string{"i": {"n"}}, + PostForm: map[string][]string{"j": {"m"}}, + MultipartForm: &multipart.Form{ + Value: map[string][]string{"p": {"u"}}, + File: map[string][]*multipart.FileHeader{ + "s": {{ + Filename: "f", + Header: map[string][]string{"e": {"d"}}, + Size: 7, + }}, + }, + }, + Trailer: map[string][]string{"k": {"l"}}, + RemoteAddr: "b", + RequestURI: "c", + } + + trk.Response = &track.Response{ + Status: "s1", + StatusCode: 3, + Proto: "p1", + ProtoMajor: 7, + ProtoMinor: 8, + Header: map[string][]string{"ch": {"cv"}}, + Body: []byte("resp body"), + ContentLength: 123, + TransferEncoding: []string{"t blah"}, + Trailer: map[string][]string{"h54": {"34v"}}, + TLS: &tls.ConnectionState{ + Version: 120, + HandshakeComplete: false, + DidResume: false, + CipherSuite: 7, + NegotiatedProtocol: "sfd", + NegotiatedProtocolIsMutual: false, + ServerName: "4dc", + PeerCertificates: []*x509.Certificate{{}}, + VerifiedChains: [][]*x509.Certificate{{{}}}, + SignedCertificateTimestamps: [][]byte{[]byte("asd")}, + OCSPResponse: []byte("asdad"), + TLSUnique: []byte("fyjyt"), + }, + } + + trk.ErrMsg = func(s string) *string { return &s }("err msg") + trk.ErrType = func(s string) *string { return &s }("err type") + }) + suite.vcr.SetRequestMatcher(NewBlankRequestMatcher()) - suite.Fail("implement me") - // TODO: create a VCR with WithTrackRecordingMutators and WithTrackReplayingMutators - // and confirm that both the live request and response remain un-mutated. + suite.vcr.SetRecordingMutators(trackDestroyerMutator) // replace all existing mutators with this one + suite.vcr.ClearReplayingMutators() // remove mutators + + err := suite.vcr.LoadCassette(suite.cassetteName) + suite.NoError(err) + + // 1st call + req, err := http.NewRequest(http.MethodGet, suite.testServer.URL, nil) + suite.Require().NoError(err) + + preRoundTripReq := track.CloneHTTPRequest(req) + + resp, err := suite.vcr.HTTPClient().Do(req) + suite.Require().NoError(err) + + expectedStats := &stats.Stats{ + TotalTracks: 1, + TracksLoaded: 0, + TracksRecorded: 1, + TracksPlayed: 0, + } + suite.Require().EqualValues(expectedStats, suite.vcr.Stats()) + + postRoundTripReq := track.CloneHTTPRequest(req) + suite.Require().EqualValues(preRoundTripReq, postRoundTripReq) + + // 2nd call + suite.vcr.ClearRecordingMutators() // remove mutators + suite.vcr.ClearReplayingMutators() // remove mutators + + req, err = http.NewRequest(http.MethodGet, suite.testServer.URL, nil) + suite.Require().NoError(err) + + resp2, err := suite.vcr.HTTPClient().Do(req) + suite.Require().NoError(err) + + expectedStats = &stats.Stats{ + TotalTracks: 2, + TracksLoaded: 0, + TracksRecorded: 2, // we haven't ejected the cassette + TracksPlayed: 0, + } + suite.Require().EqualValues(expectedStats, suite.vcr.Stats()) + + // for simplification, we're using our own track.Response + // we'll make the assumption that if that's well, the rest ought to be too. + vcrResp := track.ToResponse(resp) + suite.Assert().Equal([]byte("Hello, server responds '1' to query ''"), vcrResp.Body) + + vcrResp2 := track.ToResponse(resp2) + suite.Assert().Equal([]byte("Hello, server responds '2' to query ''"), vcrResp2.Body) + + vcrResp, vcrResp2 = nil, nil + + suite.Require().EqualValues(vcrResp, vcrResp2) + } func (suite *GoVCRWBTestSuite) TestRoundTrip_WithRecordingAndReplayingMutations() { diff --git a/pcb.go b/pcb.go index a5d0e8d..0486acc 100644 --- a/pcb.go +++ b/pcb.go @@ -92,6 +92,16 @@ func (pcb *PrintedCircuitBoard) AddRecordingMutators(mutators ...track.Mutator) pcb.trackRecordingMutators = pcb.trackRecordingMutators.Add(mutators...) } +// SetRecordingMutators replaces the set of recording Track Mutator's in the VCR. +func (pcb *PrintedCircuitBoard) SetRecordingMutators(trackMutators ...track.Mutator) { + pcb.trackRecordingMutators = trackMutators +} + +// ClearRecordingMutators clears the set of recording Track Mutator's from the VCR. +func (pcb *PrintedCircuitBoard) ClearRecordingMutators() { + pcb.trackRecordingMutators = nil +} + // AddReplayingMutators adds a collection of replaying TrackMutator's. // Replaying happens AFTER the request has been matched. As such, while the track's Request // could be mutated, it will have no effect. @@ -100,6 +110,16 @@ func (pcb *PrintedCircuitBoard) AddReplayingMutators(mutators ...track.Mutator) pcb.trackReplayingMutators = pcb.trackReplayingMutators.Add(mutators...) } +// SetReplayingMutators replaces the set of replaying Track Mutator's in the VCR. +func (pcb *PrintedCircuitBoard) SetReplayingMutators(trackMutators ...track.Mutator) { + pcb.trackReplayingMutators = trackMutators +} + +// ClearReplayingMutators clears the set of replaying Track Mutator's from the VCR. +func (pcb *PrintedCircuitBoard) ClearReplayingMutators() { + pcb.trackReplayingMutators = nil +} + // RequestMatcher is an interface that exposes the method to perform request comparison. // request comparison involves the HTTP request and the track request recorded on cassette. // TODO: there could be a case to have RequestMatchers (plural) that would work akin to track.Mutators. diff --git a/vcrtransport.go b/vcrtransport.go index 6d5a00f..b9aa806 100644 --- a/vcrtransport.go +++ b/vcrtransport.go @@ -110,6 +110,16 @@ func (t *vcrTransport) AddRecordingMutators(mutators ...track.Mutator) { t.pcb.AddRecordingMutators(mutators...) } +// SetRecordingMutators replaces the set of recording Track Mutator's in the VCR. +func (t *vcrTransport) SetRecordingMutators(trackMutators ...track.Mutator) { + t.pcb.SetRecordingMutators(trackMutators...) +} + +// ClearRecordingMutators clears the set of recording Track Mutator's from the VCR. +func (t *vcrTransport) ClearRecordingMutators() { + t.pcb.ClearRecordingMutators() +} + // AddReplayingMutators adds a set of replaying Track Mutator's to the VCR. // Replaying happens AFTER the request has been matched. As such, while the track's Request // could be mutated, it will have no effect. @@ -118,6 +128,16 @@ func (t *vcrTransport) AddReplayingMutators(mutators ...track.Mutator) { t.pcb.AddReplayingMutators(mutators...) } +// SetReplayingMutators replaces the set of replaying Track Mutator's in the VCR. +func (t *vcrTransport) SetReplayingMutators(trackMutators ...track.Mutator) { + t.pcb.SetReplayingMutators(trackMutators...) +} + +// ClearReplayingMutators clears the set of replaying Track Mutator's from the VCR. +func (t *vcrTransport) ClearReplayingMutators() { + t.pcb.ClearReplayingMutators() +} + func (t *vcrTransport) stats() *stats.Stats { return t.cassette.Stats() }