govcr

package module
v4.5.0+incompatible Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 8, 2019 License: Apache-2.0 Imports: 18 Imported by: 13

README

govcr

A Word Of Warning

I'm in the process of partly rewriting govcr to offer better support for cassette mutations. This is necessary because when I first designed govcr, I wanted cassettes to be immutable as much as golang can achieve this. Since then, I have received requests to permit cassette mutations at recording time.

The next release of govcr will bring breaking changes for those who are using govcr v4 or older. In exchange for the inconvenience, it will bring new features and a refreshed code base for future enhancements.

If you're happy with govcr as it is, use a dependency manager to lock the version of govcr you wish to use!

End Of: A Word Of Warning

Records and replays HTTP / HTTPS interactions for offline unit / behavioural / integration tests thereby acting as an HTTP mock.

This project was inspired by php-vcr which is a PHP port of VCR for ruby.

This project is an adaptation for Google's Go / Golang programming language.

Install

go get github.com/seborama/govcr

For all available releases, please check the releases tab on github.

You can pick a specific major release for compatibility. For example, to use a v4.x release, use this command:

go get gopkg.in/seborama/govcr.v4

And your source code would use this import:

import "gopkg.in/seborama/govcr.v4"

Glossary of Terms

VCR: Video Cassette Recorder. In this context, a VCR refers to the overall engine and data that this project provides. A VCR is both an HTTP recorder and player. When you use a VCR, HTTP requests are replayed from previous recordings (tracks saved in cassette files on the filesystem). When no previous recording exists for the request, it is performed live on the HTTP server the request is intended to, after what it is saved to a track on the cassette.

cassette: a sequential collection of tracks. This is in effect a JSON file saved under directory ./govcr-fixtures (default). The cassette is given a name when creating the VCR which becomes the filename (with an extension of .cassette).

Long Play cassette: a cassette compressed in gzip format. Such cassettes have a name that ends with '.gz'.

tracks: a record of an HTTP request. It contains the request data, the response data, if available, or the error that occurred.

PCB: Printed Circuit Board. This is an analogy that refers to the ability to supply customisations to some aspects of the behaviour of the VCR (for instance, disable recordings or ignore certain HTTP headers in the request when looking for a previously recorded track).

Documentation

govcr is a wrapper around the Go http.Client which offers the ability to replay pre-recorded HTTP requests ('tracks') instead of live HTTP calls.

govcr can replay both successful and failed HTTP transactions.

The code documentation can be found on godoc.

When using govcr's http.Client, the request is matched against the tracks on the 'cassette':

  • The track is played where a matching one exists on the cassette,
  • or the request is executed live to the HTTP server and then recorded on cassette for the next time (unless option DisableRecording is used).

When multiple matching tracks exist for the same request on the cassette (this can be crafted manually inside the cassette or can be simulated when using RequestFilters), the tracks will be replayed in the same order as they were recorded. See the tests for an example (TestPlaybackOrder).

When the last request matching track has been replayed, govcr cycles back to the first track again and so on.

Cassette recordings are saved under ./govcr-fixtures (by default) as *.cassette files in JSON format.

You can enable Long Play mode that will compress the cassette content. This is enabled by using the cassette name suffix .gz. The compression used is standard gzip.

It should be noted that the cassette name will be of the form 'MyCassette.gz" in your code but it will appear as "MyCassette.cassette.gz" on the file system. You can use gzip to compress and de-compress cassettes at will. See TestLongPlay() in govcr_test.go for an example usage. After running this test, notice the presence of the file govcr-fixtures/TestLongPlay.cassette.gz. You can view its contents in various ways such as with the gzcat command.

VCRConfig

This structure contains parameters for configuring your govcr recorder.

VCRConfig.CassettePath - change the location of cassette files

Example:

    vcr := govcr.NewVCR("MyCassette",
        &govcr.VCRConfig{
            CassettePath: "./govcr-fixtures",
        })
VCRConfig.DisableRecording - playback or execute live without recording

Example:

    vcr := govcr.NewVCR("MyCassette",
        &govcr.VCRConfig{
            DisableRecording: true,
        })

In this configuration, govcr will still playback from cassette when a previously recorded track (HTTP interaction) exists or execute the request live if not. But in the latter case, it won't record a new track as per default behaviour.

VCRConfig.Logging - disable logging

Example:

    vcr := govcr.NewVCR("MyCassette",
        &govcr.VCRConfig{
            Logging: false,
        })

This simply redirects all govcr logging to the OS's standard Null device (e.g. nul on Windows, or /dev/null on UN*X, etc).

VCRConfig.RemoveTLS - disable TLS recording

Example:

    vcr := govcr.NewVCR("MyCassette",
        &govcr.VCRConfig{
            RemoveTLS: true,
        })

As RemoveTLS is enabled, govcr will not record the TLS data from the HTTP response on the cassette track.

Features

  • Record extensive details about the request, response or error (network error, timeout, etc) to provide as accurate a playback as possible compared to the live HTTP request.

  • Recordings are JSON files and can be read in an editor.

  • Custom Go http.Client's can be supplied.

  • Custom Go http.Transport / http.RoundTrippers.

  • http / https supported and any other protocol implemented by the supplied http.Client's http.RoundTripper.

  • Hook to define HTTP headers that should be ignored from the HTTP request when attempting to retrieve a track for playback. This is useful to deal with non-static HTTP headers (for example, containing a timestamp).

  • Hook to transform the Header / Body of an HTTP request to deal with non-static data. The purpose is similar to the hook for headers described above but with the ability to modify the data.

  • Hook to transform the Header / Body of the HTTP response to deal with non-static data. This is similar to the request hook however, the header / body of the request are also supplied (read-only) to help match data in the response with data in the request (such as a transaction Id).

  • Ability to switch off automatic recordings. This allows to play back existing records or make a live HTTP call without recording it to the cassette.

  • Record SSL certificates.

Filter functions

In some scenarios, it may not possible to match tracks the way they were recorded.

For instance, the request contains a timestamp or a dynamically changing identifier, etc.

In other situations, the response needs a transformation to be receivable.

Building on from the above example, the response may need to provide the same identifier that the request sent.

Filters help you deal with this kind of practical aspects of dynamic exchanges.

Refer to examples/example6.go for advanced examples.

Influencing request comparison programatically at runtime.

RequestFilters receives the request Header / Body to allow their transformation. Both the live request and the recorded request on track are filtered in order to influence their comparison (e.g. remove an HTTP header to ignore it completely when matching).

Transformations are not persisted and only for the purpose of influencing comparison.

Runtime transforming of the response before sending it back to the client.

ResponseFilters is the flip side of RequestFilters. It receives the response Header / Body to allow their transformation. Unlike RequestFilters, this influences the response returned from the request to the client. The request header is also passed to ResponseFilter but read-only and solely for the purpose of extracting request data for situations where it is needed to transform the Response (such as to retrieve an identifier that must be the same in the request and the response).

Examples

Example 1 - Simple VCR

When no special HTTP Transport is required by your http.Client, you can use VCR with the default transport:

package main

import (
    "fmt"

    "github.com/seborama/govcr"
)

const example1CassetteName = "MyCassette1"

// Example1 is an example use of govcr.
func Example1() {
    vcr := govcr.NewVCR(example1CassetteName, nil)
    vcr.Client.Get("http://example.com/foo")
    fmt.Printf("%+v\n", vcr.Stats())
}

If the cassette exists and a track matches the request, it will be played back without any real HTTP call to the live server.

Otherwise, a real live HTTP call will be made and recorded in a new track added to the cassette.

Tip:

To experiment with this example, run it at least twice: the first time (when the cassette does not exist), it will make a live call. Subsequent executions will use the track on cassette to retrieve the recorded response instead of making a live call.

Example 2 - Custom VCR Transport

Sometimes, your application will create its own http.Client wrapper or will initialise the http.Client's Transport (for instance when using https).

In such cases, you can pass the http.Client object of your application to VCR.

VCR will wrap your http.Client with its own which you can inject back into your application.

package main

import (
    "crypto/tls"
    "fmt"
    "net/http"
    "time"

    "github.com/seborama/govcr"
)

const example2CassetteName = "MyCassette2"

// myApp is an application container.
type myApp struct {
    httpClient *http.Client
}

func (app myApp) Get(url string) {
    app.httpClient.Get(url)
}

// Example2 is an example use of govcr.
// It shows the use of a VCR with a custom Client.
// Here, the app executes a GET request.
func Example2() {
    // Create a custom http.Transport.
    tr := http.DefaultTransport.(*http.Transport)
    tr.TLSClientConfig = &tls.Config{
        InsecureSkipVerify: true, // just an example, not recommended
    }

    // Create an instance of myApp.
    // It uses the custom Transport created above and a custom Timeout.
    myapp := &myApp{
        httpClient: &http.Client{
            Transport: tr,
            Timeout:   15 * time.Second,
        },
    }

    // Instantiate VCR.
    vcr := govcr.NewVCR(example2CassetteName,
        &govcr.VCRConfig{
            Client: myapp.httpClient,
        })

    // Inject VCR's http.Client wrapper.
    // The original transport has been preserved, only just wrapped into VCR's.
    myapp.httpClient = vcr.Client

    // Run request and display stats.
    myapp.Get("https://example.com/foo")
    fmt.Printf("%+v\n", vcr.Stats())
}
Example 3 - Custom VCR, POST method

Please refer to the source file in the examples directory.

This example is identical to Example 2 but with a POST request rather than a GET.

Example 4 - Custom VCR with a RequestFilters

This example shows how to handle situations where a header in the request needs to be ignored (or the track would not match and hence would not be replayed).

For this example, logging is switched on. This is achieved with Logging: true in VCRConfig when calling NewVCR.

package main

import (
    "fmt"
    "strings"
    "time"

    "net/http"

    "github.com/seborama/govcr"
)

const example4CassetteName = "MyCassette4"

// Example4 is an example use of govcr.
// The request contains a custom header 'X-Custom-My-Date' which varies with every request.
// This example shows how to exclude a particular header from the request to facilitate
// matching a previous recording.
// Without the RequestFilters, the headers would not match and hence the playback would not
// happen!
func Example4() {
    vcr := govcr.NewVCR(example4CassetteName,
        &govcr.VCRConfig{
            RequestFilters: govcr.RequestFilters{
                govcr.RequestDeleteHeaderKeys("X-Custom-My-Date"),
            },
            Logging: true,
        })

    // create a request with our custom header
    req, err := http.NewRequest("POST", "http://example.com/foo", nil)
    if err != nil {
        fmt.Println(err)
    }
    req.Header.Add("X-Custom-My-Date", time.Now().String())

    // run the request
    vcr.Client.Do(req)
    fmt.Printf("%+v\n", vcr.Stats())
}

Tip:

Remove the RequestFilters from the VCRConfig and re-run the example. Check the stats: notice how the tracks no longer replay.

Example 5 - Custom VCR with a RequestFilters and ResponseFilters

This example shows how to handle situations where a transaction Id in the header needs to be present in the response. This could be as part of a contract validation between server and client.

Note: This is useful when some of the data in the request Header / Body needs to be transformed before it can be evaluated for comparison for playback.

package main

import (
    "fmt"
    "strings"
    "time"

    "net/http"

    "github.com/seborama/govcr"
)

const example5CassetteName = "MyCassette5"

// Example5 is an example use of govcr.
// Supposing a fictional application where the request contains a custom header
// 'X-Transaction-Id' which must be matched in the response from the server.
// When replaying, the request will have a different Transaction Id than that which was recorded.
// Hence the protocol (of this fictional example) is broken.
// To circumvent that, we inject the new request's X-Transaction-Id into the recorded response.
// Without the ResponseFilters, the X-Transaction-Id in the header would not match that
// of the recorded response and our fictional application would reject the response on validation!
func Example5() {
    vcr := govcr.NewVCR(example5CassetteName,
        &govcr.VCRConfig{
            RequestFilters: govcr.RequestFilters{
                govcr.RequestDeleteHeaderKeys("X-Transaction-Id"),
            },
			ResponseFilters: govcr.ResponseFilters{
				// overwrite X-Transaction-Id in the Response with that from the Request
				govcr.ResponseTransferHeaderKeys("X-Transaction-Id"),
			},
            Logging: true,
        })

    // create a request with our custom header
    req, err := http.NewRequest("POST", "http://example.com/foo5", nil)
    if err != nil {
        fmt.Println(err)
    }
    req.Header.Add("X-Transaction-Id", time.Now().String())

    // run the request
    resp, err := vcr.Client.Do(req)
    if err != nil {
        fmt.Println(err)
    }

    // verify outcome
    if req.Header.Get("X-Transaction-Id") != resp.Header.Get("X-Transaction-Id") {
        fmt.Println("Header transaction Id verification failed - this would be the live request!")
    } else {
        fmt.Println("Header transaction Id verification passed - this would be the replayed track!")
    }

    fmt.Printf("%+v\n", vcr.Stats())
}
More examples

Refer to examples/example6.go for advanced examples.

All examples are in the examples directory.

Stats

VCR provides some statistics.

To access the stats, call vcr.Stats() where vcr is the VCR instance obtained from NewVCR(...).

Run the examples

Please refer to the examples directory for examples of code and uses.

Observe the output of the examples between the 1st run and the 2nd run of each example.

The first time they run, they perform a live HTTP call (Executing request to live server).

However, on second execution (and sub-sequent executions as long as the cassette is not deleted) govcr retrieves the previously recorded request and plays it back without live HTTP call (Found a matching track). You can disconnect from the internet and still playback HTTP requests endlessly!

Make utility
make examples
Manually
cd examples
go run *.go
Output

First execution - notice the stats show that a track was recorded (from a live HTTP call).

Second execution - no track is recorded (no live HTTP call) but 1 track is loaded and played back.

Running Example1...
1st run =======================================================
{TracksLoaded:0 TracksRecorded:1 TracksPlayed:0}
2nd run =======================================================
{TracksLoaded:1 TracksRecorded:0 TracksPlayed:1}
Complete ======================================================

Running Example2...
1st run =======================================================
{TracksLoaded:0 TracksRecorded:1 TracksPlayed:0}
2nd run =======================================================
{TracksLoaded:1 TracksRecorded:0 TracksPlayed:1}
Complete ======================================================

Running Example3...
1st run =======================================================
{TracksLoaded:0 TracksRecorded:1 TracksPlayed:0}
2nd run =======================================================
{TracksLoaded:1 TracksRecorded:0 TracksPlayed:1}
Complete ======================================================

Running Example4...
1st run =======================================================
2018/10/25 00:12:56 INFO - Cassette 'MyCassette4' - Executing request to live server for POST http://www.example.com/foo
2018/10/25 00:12:56 INFO - Cassette 'MyCassette4' - Recording new track for POST http://www.example.com/foo as POST http://www.example.com/foo
{TracksLoaded:0 TracksRecorded:1 TracksPlayed:0}
2nd run =======================================================
2018/10/25 00:12:56 INFO - Cassette 'MyCassette4' - Found a matching track for POST http://www.example.com/foo
{TracksLoaded:1 TracksRecorded:0 TracksPlayed:1}
Complete ======================================================

Running Example5...
1st run =======================================================
2018/10/25 00:12:56 INFO - Cassette 'MyCassette5' - Executing request to live server for POST http://www.example.com/foo5
2018/10/25 00:12:56 INFO - Cassette 'MyCassette5' - Recording new track for POST http://www.example.com/foo5 as POST http://www.example.com/foo5
Header transaction Id verification failed - this would be the live request!
{TracksLoaded:0 TracksRecorded:1 TracksPlayed:0}
2nd run =======================================================
2018/10/25 00:12:56 INFO - Cassette 'MyCassette5' - Found a matching track for POST http://www.example.com/foo5
Header transaction Id verification passed - this would be the replayed track!
{TracksLoaded:1 TracksRecorded:0 TracksPlayed:1}
Complete ======================================================

Run the tests

make test

or

go test -race -cover

Bugs

  • None known

Improvements

  • When unmarshaling the cassette fails, rather than fail altogether, it would be preferable to revert to live HTTP call.

  • The code has a number of TODO's which should either be taken action upon or removed!

Limitations

Go empty interfaces (interface{})

Some properties / objects in http.Response are defined as interface{}. This can cause json.Unmarshall to fail (example: when the original type was big.Int with a big interger indeed - json.Unmarshal attempts to convert to float64 and fails).

Currently, this is dealt with by converting the output of the JSON produced by json.Marshal (big.Int is changed to a string).

Support for multiple values in HTTP headers

Repeat HTTP headers may not be properly handled. A long standing TODO in the code exists but so far no one has complained :-)

HTTP transport errors

govcr also records http.Client errors (network down, blocking firewall, timeout, etc) in the track for future play back.

Since errors is an interface, when it is unmarshalled into JSON, the Go type of the error is lost.

To circumvent this, govcr serialises the object type (ErrType) and the error message (ErrMsg) in the track record.

Objects cannot be created by name at runtime in Go. Rather than re-create the original error object, govcr creates a standard error object with an error string made of both the ErrType and ErrMsg.

In practice, the implications for you depend on how much you care about the error type. If all you need to know is that an error occurred, you won't mind this limitation.

Mitigation: Support for common errors (network down) has been implemented. Support for more error types can be implemented, if there is appetite for it.

Contribute

You are welcome to submit a PR to contribute.

Please follow a TDD workflow: tests must be present and avoid toxic DDT (dev driven testing).

Documentation

Overview

Package govcr records and replays HTTP interactions for offline unit / behavioural / integration tests thereby acting as an HTTP mock.

This project was inspired by php-vcr which is a PHP port of VCR for ruby.

For usage and more information, please refer to the project's README at:

https://github.com/seborama/govcr

Example (Number1SimpleVCR)

Example_simpleVCR is an example use of govcr. It shows how to use govcr in the simplest case when the default http.Client suffices.

package main

import (
	"fmt"
	"io/ioutil"
	"strings"

	"github.com/seborama/govcr"
)

const example1CassetteName = "MyCassette1"

func runTestEx1() {
	// Create vcr and make http call
	vcr := govcr.NewVCR(example1CassetteName, nil)
	resp, _ := vcr.Client.Get("http://www.example.com/foo")

	// Show results
	fmt.Printf("%d ", resp.StatusCode)
	fmt.Printf("%s ", resp.Header.Get("Content-Type"))

	body, _ := ioutil.ReadAll(resp.Body)
	resp.Body.Close()
	fmt.Printf("%v ", strings.Contains(string(body), "domain in examples without prior coordination or asking for permission."))

	fmt.Printf("%+v\n", vcr.Stats())
}

// Example_simpleVCR is an example use of govcr.
// It shows how to use govcr in the simplest case when the default
// http.Client suffices.
func main() {
	// Delete cassette to enable live HTTP call
	govcr.DeleteCassette(example1CassetteName, "")

	// 1st run of the test - will use live HTTP calls
	runTestEx1()
	// 2nd run of the test - will use playback
	runTestEx1()

}
Output:

404 text/html; charset=UTF-8 true {TracksLoaded:0 TracksRecorded:1 TracksPlayed:0}
404 text/html; charset=UTF-8 true {TracksLoaded:1 TracksRecorded:0 TracksPlayed:1}
Example (Number2CustomClientVCR1)

Example2 is an example use of govcr. It shows the use of a VCR with a custom Client. Here, the app executes a GET request.

package main

import (
	"crypto/tls"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
	"time"

	"github.com/seborama/govcr"
)

const example2CassetteName = "MyCassette2"

// myApp is an application container.
type myApp struct {
	httpClient *http.Client
}

func (app *myApp) Get(url string) (*http.Response, error) {
	return app.httpClient.Get(url)
}

func (app *myApp) Post(url string) (*http.Response, error) {
	// beware: don't use a ReadCloser, only a Reader!
	body := strings.NewReader(`{"Msg": "This is an example request"}`)
	return app.httpClient.Post(url, "application/json", body)
}

func runTestEx2(app *myApp) {
	var samples = []struct {
		f    func(string) (*http.Response, error)
		body string
	}{
		{app.Get, "domain in examples without prior coordination or asking for permission."},
		{app.Post, "404 - Not Found"},
	}

	// Instantiate VCR.
	vcr := govcr.NewVCR(example2CassetteName,
		&govcr.VCRConfig{
			Client: app.httpClient,
		})

	// Inject VCR's http.Client wrapper.
	// The original transport has been preserved, only just wrapped into VCR's.
	app.httpClient = vcr.Client

	for _, td := range samples {
		// Run HTTP call
		resp, _ := td.f("https://www.example.com/foo")

		// Show results
		fmt.Printf("%d ", resp.StatusCode)
		fmt.Printf("%s ", resp.Header.Get("Content-Type"))

		body, _ := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		fmt.Printf("%v - ", strings.Contains(string(body), td.body))
	}

	fmt.Printf("%+v\n", vcr.Stats())
}

// Example2 is an example use of govcr.
// It shows the use of a VCR with a custom Client.
// Here, the app executes a GET request.
func main() {
	// Create a custom http.Transport.
	tr := http.DefaultTransport.(*http.Transport)
	tr.TLSClientConfig = &tls.Config{
		InsecureSkipVerify: true, // just an example, not recommended
	}

	// Create an instance of myApp.
	// It uses the custom Transport created above and a custom Timeout.
	app := &myApp{
		httpClient: &http.Client{
			Transport: tr,
			Timeout:   15 * time.Second,
		},
	}

	// Delete cassette to enable live HTTP call
	govcr.DeleteCassette(example2CassetteName, "")

	// 1st run of the test - will use live HTTP calls
	runTestEx2(app)
	// 2nd run of the test - will use playback
	runTestEx2(app)

}
Output:

404 text/html; charset=UTF-8 true - 404 text/html; charset=UTF-8 true - {TracksLoaded:0 TracksRecorded:2 TracksPlayed:0}
404 text/html; charset=UTF-8 true - 404 text/html; charset=UTF-8 true - {TracksLoaded:2 TracksRecorded:0 TracksPlayed:2}
Example (Number3HeaderExclusionVCR)

Example_simpleVCR is an example use of govcr. It shows how to use govcr in the simplest case when the default http.Client suffices.

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
	"time"

	"github.com/seborama/govcr"
)

const example3CassetteName = "MyCassette3"

func runTestEx3() {
	var samples = []struct {
		method string
		body   string
	}{
		{"GET", "domain in examples without prior coordination or asking for permission."},
		{"POST", "404 - Not Found"},
		{"PUT", ""},
		{"DELETE", ""},
	}

	// Create vcr
	vcr := govcr.NewVCR(example3CassetteName,
		&govcr.VCRConfig{
			RequestFilters: govcr.RequestFilters{
				govcr.RequestDeleteHeaderKeys("X-Custom-My-Date"),
			},
		})

	for _, td := range samples {
		// Create a request with our custom header
		req, _ := http.NewRequest(td.method, "http://www.example.com/foo", nil)
		req.Header.Add("X-Custom-My-Date", time.Now().String())

		// Make http call
		resp, _ := vcr.Client.Do(req)

		// Show results
		fmt.Printf("%d ", resp.StatusCode)
		fmt.Printf("%s ", resp.Header.Get("Content-Type"))

		body, _ := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		fmt.Printf("%v ", strings.Contains(string(body), td.body))
	}

	fmt.Printf("%+v\n", vcr.Stats())
}

// Example_simpleVCR is an example use of govcr.
// It shows how to use govcr in the simplest case when the default
// http.Client suffices.
func main() {
	// Delete cassette to enable live HTTP call
	govcr.DeleteCassette(example3CassetteName, "")

	// 1st run of the test - will use live HTTP calls
	runTestEx3()
	// 2nd run of the test - will use playback
	runTestEx3()

}
Output:

404 text/html; charset=UTF-8 true 404 text/html; charset=UTF-8 true 404 text/html; charset=UTF-8 true 404 text/html; charset=UTF-8 true {TracksLoaded:0 TracksRecorded:4 TracksPlayed:0}
404 text/html; charset=UTF-8 true 404 text/html; charset=UTF-8 true 404 text/html; charset=UTF-8 true 404 text/html; charset=UTF-8 true {TracksLoaded:4 TracksRecorded:0 TracksPlayed:4}
Example (Number4SimpleVCR)

Example_simpleVCR is an example use of govcr. It shows a simple use of a Long Play cassette (i.e. compressed).

package main

import (
	"fmt"
	"io/ioutil"
	"strings"

	"github.com/seborama/govcr"
)

const example4CassetteName = "MyCassette4"

func runTestEx4() {
	// Create vcr and make http call
	vcr := govcr.NewVCR(example4CassetteName, nil)
	resp, _ := vcr.Client.Get("http://www.example.com/foo")

	// Show results
	fmt.Printf("%d ", resp.StatusCode)
	fmt.Printf("%s ", resp.Header.Get("Content-Type"))

	body, _ := ioutil.ReadAll(resp.Body)
	resp.Body.Close()
	fmt.Printf("%v ", strings.Contains(string(body), "domain in examples without prior coordination or asking for permission."))

	fmt.Printf("%+v\n", vcr.Stats())
}

// Example_simpleVCR is an example use of govcr.
// It shows a simple use of a Long Play cassette (i.e. compressed).
func main() {
	// Delete cassette to enable live HTTP call
	govcr.DeleteCassette(example4CassetteName, "")

	// 1st run of the test - will use live HTTP calls
	runTestEx4()
	// 2nd run of the test - will use playback
	runTestEx4()

}
Output:

404 text/html; charset=UTF-8 true {TracksLoaded:0 TracksRecorded:1 TracksPlayed:0}
404 text/html; charset=UTF-8 true {TracksLoaded:1 TracksRecorded:0 TracksPlayed:1}
Example (Number6ConditionalRewrites)

Example_simpleVCR is an example use of govcr. It shows how to use govcr in the simplest case when the default http.Client suffices.

package main

import (
	"fmt"
	"math/rand"
	"net/http"
	"time"

	"github.com/seborama/govcr"
)

const example6CassetteName = "MyCassette6"

// Example6 is an example use of govcr.
// This will show how to do conditional rewrites.
// For example, your request has a "/order/{random}" path
// and we want to rewrite it to /order/1234 so we can match it later.
// We change the response status code.
// We add headers based on request method.
func runTestEx6(rng *rand.Rand) {
	cfg := govcr.VCRConfig{
		Logging: true,
	}

	// The filter will neutralize a value in the URL.
	// In this case we rewrite /order/{random} to /order/1234
	replacePath := govcr.RequestFilter(func(req govcr.Request) govcr.Request {
		// Replace path with a predictable one.
		req.URL.Path = "/order/1234"
		return req
	})
	// Only execute when we match path.
	replacePath = replacePath.OnPath(`example\.com\/order\/`)

	// Add to request filters.
	cfg.RequestFilters.Add(replacePath)
	cfg.RequestFilters.Add(govcr.RequestDeleteHeaderKeys("X-Transaction-Id"))

	// Add filters
	cfg.ResponseFilters.Add(
		// Always transfer 'X-Transaction-Id' as in example 5.
		govcr.ResponseTransferHeaderKeys("X-Transaction-Id"),

		// Change status 404 to 202.
		func(resp govcr.Response) govcr.Response {
			if resp.StatusCode == http.StatusNotFound {
				resp.StatusCode = http.StatusAccepted
			}
			return resp
		},

		// Add header if method was "GET"
		govcr.ResponseFilter(func(resp govcr.Response) govcr.Response {
			resp.Header.Add("method-was-get", "true")
			return resp
		}).OnMethod(http.MethodGet),

		// Add header if method was "POST"
		govcr.ResponseFilter(func(resp govcr.Response) govcr.Response {
			resp.Header.Add("method-was-post", "true")
			return resp
		}).OnMethod(http.MethodPost),

		// Add actual request URL to header.
		govcr.ResponseFilter(func(resp govcr.Response) govcr.Response {
			url := resp.Request().URL
			resp.Header.Add("get-url", url.String())
			return resp
		}).OnMethod(http.MethodGet),
	)

	orderID := fmt.Sprint(rng.Uint64())
	vcr := govcr.NewVCR(example6CassetteName, &cfg)

	// create a request with our custom header and a random url part.
	req, err := http.NewRequest("POST", "http://www.example.com/order/"+orderID, nil)
	if err != nil {
		fmt.Println(err)
	}
	runExample6Request(req, vcr)

	// create a request with our custom header and a random url part.
	req, err = http.NewRequest("GET", "http://www.example.com/order/"+orderID, nil)
	if err != nil {
		fmt.Println(err)
	}
	runExample6Request(req, vcr)

}

func runExample6Request(req *http.Request, vcr *govcr.VCRControlPanel) {
	req.Header.Add("X-Transaction-Id", time.Now().String())
	// run the request
	resp, err := vcr.Client.Do(req)
	if err != nil {
		fmt.Println(err)
		return
	}
	// verify outcome
	if req.Header.Get("X-Transaction-Id") != resp.Header.Get("X-Transaction-Id") {
		fmt.Println("Header transaction Id verification FAILED - this would be the live request!")
	} else {
		fmt.Println("Header transaction Id verification passed - this would be the replayed track!")
	}

	// print outcome.
	fmt.Println("Status code:", resp.StatusCode, " (should be 404 on real and 202 on replay)")
	fmt.Println("method-was-get:", resp.Header.Get("method-was-get"), "(should never be true on GET)")
	fmt.Println("method-was-post:", resp.Header.Get("method-was-post"), "(should be true on replay on POST)")
	fmt.Println("get-url:", resp.Header.Get("get-url"), "(actual url of the request, not of the track)")
	fmt.Printf("%+v\n", vcr.Stats())
}

// Example_simpleVCR is an example use of govcr.
// It shows how to use govcr in the simplest case when the default
// http.Client suffices.
func main() {
	// Delete cassette to enable live HTTP call
	govcr.DeleteCassette(example6CassetteName, "")

	// We need a predictable RNG
	rng := rand.New(rand.NewSource(6))

	// 1st run of the test - will use live HTTP calls
	runTestEx6(rng)
	// 2nd run of the test - will use playback
	runTestEx6(rng)

}
Output:

Header transaction Id verification FAILED - this would be the live request!
Status code: 404  (should be 404 on real and 202 on replay)
method-was-get:  (should never be true on GET)
method-was-post:  (should be true on replay on POST)
get-url:  (actual url of the request, not of the track)
{TracksLoaded:0 TracksRecorded:1 TracksPlayed:0}
Header transaction Id verification FAILED - this would be the live request!
Status code: 404  (should be 404 on real and 202 on replay)
method-was-get:  (should never be true on GET)
method-was-post:  (should be true on replay on POST)
get-url:  (actual url of the request, not of the track)
{TracksLoaded:0 TracksRecorded:2 TracksPlayed:0}
Header transaction Id verification passed - this would be the replayed track!
Status code: 202  (should be 404 on real and 202 on replay)
method-was-get:  (should never be true on GET)
method-was-post: true (should be true on replay on POST)
get-url:  (actual url of the request, not of the track)
{TracksLoaded:2 TracksRecorded:0 TracksPlayed:1}
Header transaction Id verification passed - this would be the replayed track!
Status code: 202  (should be 404 on real and 202 on replay)
method-was-get: true (should never be true on GET)
method-was-post:  (should be true on replay on POST)
get-url: http://www.example.com/order/7790075977082629872 (actual url of the request, not of the track)
{TracksLoaded:2 TracksRecorded:0 TracksPlayed:2}
Example (Number7BodyInjection)

Example_number7BodyInjection will show how bodies can be rewritten. We will take a varying ID from the request URL, neutralize it and also change the ID in the body of the response.

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"math/rand"
	"net/http"
	"net/http/httptest"
	"regexp"

	"github.com/seborama/govcr"
)

const example7CassetteName = "MyCassette7"

// runTestEx7 is an example use of govcr.
// This will show how bodies can be rewritten.
// We will take a varying ID from the request URL, neutralize it and also change the ID in the body of the response.
func runTestEx7(rng *rand.Rand) {
	cfg := govcr.VCRConfig{
		Logging: true,
	}

	// Order is out example body we want to modify.
	type Order struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	}

	// Regex to extract the ID from the URL.
	reOrderID := regexp.MustCompile(`/order/([^/]+)`)

	// Create a local test server that serves out responses.
	handler := func(w http.ResponseWriter, r *http.Request) {
		id := reOrderID.FindStringSubmatch(r.URL.String())
		if len(id) < 2 {
			w.WriteHeader(404)
			return
		}

		w.WriteHeader(200)
		b, err := json.Marshal(Order{
			ID:   id[1],
			Name: "Test Order",
		})
		if err != nil {
			w.WriteHeader(500)
			return
		}
		w.Header().Add("Content-Type", "application/json")
		w.WriteHeader(200)
		w.Write(b)
	}
	server := httptest.NewServer(http.HandlerFunc(handler))
	defer server.Close()

	// The filter will neutralize a value in the URL.
	// In this case we rewrite /order/{random} to /order/1234
	// and replacing the host so it doesn't depend on the random port number.
	replacePath := govcr.RequestFilter(func(req govcr.Request) govcr.Request {
		req.URL.Path = "/order/1234"
		req.URL.Host = "127.0.0.1"
		return req
	})

	// Only execute when we match path.
	cfg.RequestFilters.Add(replacePath.OnPath(`/order/`))

	cfg.ResponseFilters.Add(
		govcr.ResponseFilter(func(resp govcr.Response) govcr.Response {
			req := resp.Request()

			// Find the requested ID:
			orderID := reOrderID.FindStringSubmatch(req.URL.String())

			// Unmarshal body.
			var o Order
			err := json.Unmarshal(resp.Body, &o)
			if err != nil {
				panic(err)
			}

			// Change the ID
			o.ID = orderID[1]

			// Replace the body.
			resp.Body, err = json.Marshal(o)
			if err != nil {
				panic(err)
			}
			return resp
		}).OnStatus(200),
	)

	orderID := fmt.Sprint(rng.Uint64())
	vcr := govcr.NewVCR(example7CassetteName, &cfg)

	// create a request with our custom header and a random url part.
	req, err := http.NewRequest("GET", server.URL+"/order/"+orderID, nil)
	if err != nil {
		fmt.Println(err)
	}

	// run the request
	resp, err := vcr.Client.Do(req)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	// print outcome.
	// Remove host name for consistent output
	req.URL.Host = "127.0.0.1"
	fmt.Println("GET", req.URL.String())
	fmt.Println("Status code:", resp.StatusCode)
	body, _ := ioutil.ReadAll(resp.Body)
	fmt.Println("Returned Body:", string(body))
	fmt.Printf("%+v\n", vcr.Stats())
}

// Example_number7BodyInjection will show how bodies can be rewritten.
// We will take a varying ID from the request URL, neutralize it and also change the ID in the body of the response.
func main() {
	// Delete cassette to enable live HTTP call
	govcr.DeleteCassette(example7CassetteName, "")

	// We need a predictable RNG
	rng := rand.New(rand.NewSource(7))

	// 1st run of the test - will use live HTTP calls
	runTestEx7(rng)
	// 2nd run of the test - will use playback
	runTestEx7(rng)

}
Output:

GET http://127.0.0.1/order/8475284246537043955
Status code: 200
Returned Body: {"id":"8475284246537043955","name":"Test Order"}
{TracksLoaded:0 TracksRecorded:1 TracksPlayed:0}
GET http://127.0.0.1/order/2135276795452531224
Status code: 200
Returned Body: {"id":"2135276795452531224","name":"Test Order"}
{TracksLoaded:1 TracksRecorded:0 TracksPlayed:1}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func CassetteExistsAndValid

func CassetteExistsAndValid(cassetteName, cassettePath string) bool

CassetteExistsAndValid verifies a cassette file exists and is seemingly valid.

func DeleteCassette

func DeleteCassette(cassetteName, cassettePath string) error

DeleteCassette removes the cassette file from disk.

func GetFirstValue added in v1.6.0

func GetFirstValue(hdr http.Header, key string) string

GetFirstValue is a utility function that extracts the first value of a header key. The reason for this function is that some servers require case sensitive headers which prevent the use of http.Header.Get() as it expects header keys to be canonicalized.

Types

type Request added in v1.6.0

type Request struct {
	Header http.Header
	Body   []byte
	Method string
	URL    url.URL
}

A Request provides the request parameters. Notice of warning: 'Request' contains fields that are subject to shallow copy:

  • url.URL which itself contains a pointer.
  • Header which is a map.
  • Body which is a slice.

As a result, when copying a 'Request', the shallow copy shares those above mentioned fields' data! A change to the (shallow) copy will also change the source object!

type RequestFilter added in v1.6.0

type RequestFilter func(req Request) Request

A RequestFilter can be used to remove / amend undesirable header / body elements from the request.

For instance, if your application sends requests with a timestamp held in a part of the header / body, you likely want to remove it or force a static timestamp via RequestFilterFunc to ensure that the request body matches those saved on the cassette's track.

A Filter should return the request with any modified values.

func RequestAddHeaderValue added in v1.6.0

func RequestAddHeaderValue(key, value string) RequestFilter

RequestAddHeaderValue will add or overwrite a header to the request before the request is matched against the cassette.

func RequestDeleteHeaderKeys added in v1.6.0

func RequestDeleteHeaderKeys(keys ...string) RequestFilter

RequestDeleteHeaderKeys will delete one or more header keys on the request before the request is matched against the cassette.

func RequestExcludeHeaderFunc added in v1.6.0

func RequestExcludeHeaderFunc(fn func(key string) bool) RequestFilter

RequestExcludeHeaderFunc is a hook function that is used to filter the Header.

Typically this can be used to remove / amend undesirable custom headers from the request.

For instance, if your application sends requests with a timestamp held in a custom header, you likely want to exclude it from the comparison to ensure that the request headers are considered a match with those saved on the cassette's track.

Parameters:

  • parameter 1 - Name of header key in the Request

Return value: true - exclude header key from comparison false - retain header key for comparison

Deprecated - This function will be removed on or after April 25th 2019

func (RequestFilter) OnMethod added in v1.6.0

func (r RequestFilter) OnMethod(method ...string) RequestFilter

OnMethod will return a request filter that will only apply 'r' if the request method matches one of the specified methods in the argument list. Original filter is unmodified.

func (RequestFilter) OnPath added in v1.6.0

func (r RequestFilter) OnPath(pathRegEx string) RequestFilter

OnPath will return a request filter that will only apply 'r' if the url string of the request matches the supplied regex. Original filter is unmodified.

type RequestFilters added in v1.6.0

type RequestFilters []RequestFilter

RequestFilters is a slice of RequestFilter

func (*RequestFilters) Add added in v1.6.0

func (r *RequestFilters) Add(filters ...RequestFilter)

Add one or more filters at the end of the filter chain.

func (*RequestFilters) Prepend added in v1.6.0

func (r *RequestFilters) Prepend(filters ...RequestFilter)

Prepend one or more filters before the current ones.

type Response added in v1.6.0

type Response struct {

	// The content returned in the response.
	Body       []byte
	Header     http.Header
	StatusCode int
	// contains filtered or unexported fields
}

Response provides the response parameters. When returned from a ResponseFilter these values will be returned instead.

func (Response) Request added in v1.6.0

func (r Response) Request() Request

Request returns the request. This is the request after RequestFilters have been applied.

type ResponseFilter added in v1.6.0

type ResponseFilter func(resp Response) Response

ResponseFilter is a hook function that is used to filter the Response Header / Body.

It works similarly to RequestFilterFunc but applies to the Response and also receives a copy of the Request context (if you need to pick info from it to override the response).

Return the modified response.

func ResponseAddHeaderValue added in v1.6.0

func ResponseAddHeaderValue(key, value string) ResponseFilter

ResponseAddHeaderValue will add/overwrite a header to the response when it is returned from vcr playback.

func ResponseChangeBody added in v1.6.0

func ResponseChangeBody(fn func(b []byte) []byte) ResponseFilter

ResponseChangeBody will allows to change the body. Supply a function that does input to output transformation.

func ResponseDeleteHeaderKeys added in v1.6.0

func ResponseDeleteHeaderKeys(keys ...string) ResponseFilter

ResponseDeleteHeaderKeys will delete one or more headers on the response when returned from vcr playback.

func ResponseTransferHeaderKeys added in v1.6.0

func ResponseTransferHeaderKeys(keys ...string) ResponseFilter

ResponseTransferHeaderKeys will transfer one or more header from the Request to the Response.

func (ResponseFilter) OnMethod added in v1.6.0

func (r ResponseFilter) OnMethod(method ...string) ResponseFilter

OnMethod will return a Response filter that will only apply 'r' if the request method matches one of the specified methods in the argument list.. Original filter is unmodified.

func (ResponseFilter) OnPath added in v1.6.0

func (r ResponseFilter) OnPath(pathRegEx string) ResponseFilter

OnPath will return a Response filter that will only apply 'r' if the url string of the Response matches the supplied regex. Original filter is unmodified.

func (ResponseFilter) OnStatus added in v1.6.0

func (r ResponseFilter) OnStatus(status ...int) ResponseFilter

OnStatus will return a Response filter that will only apply 'r' if the response status matches one of the supplied statuses. Original filter is unmodified.

type ResponseFilters added in v1.6.0

type ResponseFilters []ResponseFilter

ResponseFilters is a slice of ResponseFilter

func (*ResponseFilters) Add added in v1.6.0

func (r *ResponseFilters) Add(filters ...ResponseFilter)

Add one or more filters at the end of the filter chain.

func (*ResponseFilters) Prepend added in v1.6.0

func (r *ResponseFilters) Prepend(filters ...ResponseFilter)

Prepend one or more filters before the current ones.

type Stats

type Stats struct {
	// TracksLoaded is the number of tracks that were loaded from the cassette.
	TracksLoaded int32

	// TracksRecorded is the number of new tracks recorded by VCR.
	TracksRecorded int32

	// TracksPlayed is the number of tracks played back straight from the cassette.
	// I.e. tracks that were already present on the cassette and were played back.
	TracksPlayed int32
}

Stats holds information about the cassette and VCR runtime.

type VCRConfig

type VCRConfig struct {
	Client *http.Client

	// Filter to run before request is matched against cassettes.
	RequestFilters RequestFilters

	// Filter to run before a response is returned.
	ResponseFilters ResponseFilters

	// LongPlay will compress data on cassettes.
	LongPlay         bool
	DisableRecording bool
	Logging          bool
	CassettePath     string

	// RemoveTLS will remove TLS from the Response when recording.
	// TLS information is rarely needed and takes up a lot of space.
	RemoveTLS bool
}

VCRConfig holds a set of options for the VCR.

type VCRControlPanel

type VCRControlPanel struct {
	Client *http.Client
}

VCRControlPanel holds the parts of a VCR that can be interacted with. Client is the HTTP client associated with the VCR.

func NewVCR

func NewVCR(cassetteName string, vcrConfig *VCRConfig) *VCRControlPanel

NewVCR creates a new VCR and loads a cassette. A RoundTripper can be provided when a custom Transport is needed (for example to provide certificates, etc)

func (*VCRControlPanel) Stats

func (vcr *VCRControlPanel) Stats() Stats

Stats returns Stats about the cassette and VCR session.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL