replay

package module
v0.0.0-...-1075d47 Latest Latest
Warning

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

Go to latest
Published: Nov 4, 2019 License: ISC Imports: 14 Imported by: 0

README

Replay

Replay is a package for easily recording and replaying HTTP responses, e.g. for using canned API responses for unit testing.

This work is licensed under the ISC License, a copy of which can be found at LICENSE

Installation

go get -u github.com/richshaffer/replay

Documentation

https://godoc.org/github.com/richshaffer/replay

Example

package main

import (
	"flag"
	"fmt"
	"strings"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	"github.com/richshaffer/replay"
)

func main() {
	// If false, only already-recorded requests will succeed:
	record := flag.Bool("record", false, "enable recording new API responses")
	flag.Parse()

	client := replay.NewClient("testdata")
	client.Transport.(*replay.RoundTripper).OmitHeaders.Add("X-Amz-Date")

	config := aws.NewConfig().WithHTTPClient(client)
	sess := session.Must(session.NewSession(config))
	uploader := s3manager.NewUploader(sess)

	result, err := uploader.Upload(&s3manager.UploadInput{
		Bucket: aws.String("testbucket"),
		Key:    aws.String("testkey"),
		Body:   strings.NewReader("testbody"),
	})
	if err != nil {
		fmt.Printf("failed to upload file, %v\n", err)
		return
	}
	fmt.Printf("file uploaded to, %s\n", result.Location)
}

Documentation

Overview

Package replay enables creating and/or replaying canned HTTP server responses.

It is primarily intended to simplify providing canned HTTP responses for unit testing packages that rely on external HTTP services. It may have other uses.

Recordings can be created by either using the HTTP client returned by NewRecordingClient, or by setting the Recording field of the RoundTripper to true. The recording format is intended to be simple, so that they may also be created and edited manually for simple cases.

Recordings are identified uniquely by a pathname derived from HTTP request contents. Paths are constructed as a series of intermediate directories and a file as follows:

HTTP scheme / host(:port) / HTTP method / path / ... / request (. CRC) .json

The period and CRC beteen "request" and ".json" may not be present if a request contained no excluded query parameters, no excluded headers and also no body. Each component is also URL-encoded, if necessary, with url.QueryEscape, to avoid potentially invalid filenames for some platforms. As an example, a GET request for the URL

http://www.example.com/path/to/easy+street

generates the path name

http/www.example.com/GET/path/to/easy%2bstreet/request.json

The RoundTripper will first try to load a canned response from the path with the CRC extension, if a CRC is calculated. If no response is found, it will by default attempt to load the content from a path without the CRC extension. This behavior can be disabled by setting StrictPath to true.

The paths above are relative to the Dir field of RoundTripper, which is also taken as a parameter to the NewClient and NewRecordingClient functions.

The format of the recording files is also intended to be easily human-readable. The first part of the file is a JSON object with fields that will be mapped to the *http.Response object. The JSON object is followed by one newline. Any content after that is the body of the recorded response:

{
  "status": "404 Not Found",
  "status_code": 301,
  "proto": "HTTP/1.1",
  "proto_major": 1,
  "proto_minor": 1,
  "headers": {
    "Content-Type": [
      "text/plain"
    ],
  }
}
The requested content was not found.

A simple example use case may look something like this:

client := replay.NewClient("testdata")
// If allowRecording is false, this will only succeed if a recorded response
// exists under the "testdata" directory:
res, err := client.Get("https://api.ipify.org?format=json")

Index

Constants

View Source
const (
	// ModeRecordIfMissing enables playing back recordings that exist, and
	// recording new responses when a recording isn't found.
	ModeRecordIfMissing = iota
	// ModePlaybackOnly enables playing back content only.
	ModePlaybackOnly
	// ModeRecordOnly enables recording new content only.
	ModeRecordOnly
)

Variables

This section is empty.

Functions

func NewClient

func NewClient(dir string) *http.Client

NewClient returns an *http.Client which will return pre-recorded responses if the exists, or create new recordings if they are missing..

func NewPlaybackOnlyClient

func NewPlaybackOnlyClient(dir string) *http.Client

NewPlaybackOnlyClient returns an *http.Client which will only return pre- recorded responses. If no response is found, an error is returned.

func NewRecordOnlyClient

func NewRecordOnlyClient(dir string) *http.Client

NewRecordOnlyClient returns an *http.Client which will record new responses, even if a pre-recorded response exists.

Types

type Error

type Error struct {
	// Request is the *http.Request that was being processed when the error
	// occurred.
	Request *http.Request
	// Response is the *http.Response that was being processed when the error
	// occurred. It will be nil if the error occurred while loading a recording
	// from the local filesystem (as opposed to saving a new response).
	Response *http.Response
	// Err is the underlying error that was encountered while attempting to
	// manipulate a recording.
	Err error
}

Error is an error that may be returned by RoundTripper, and thus by the *http.Client returned by NewClient or NewRecordingClient. It can be used to differentiate an error encountered when trying to fetch or save a recording in local storage versus errors returned by the http package, such as for URL or network errors.

func (*Error) Error

func (r *Error) Error() string

type PathGenerator

type PathGenerator struct {
	// OmitHeaders is a set of headers to exclude from path calculations.
	// Requests with different content in these headers can still return the
	// same unique path.
	OmitHeaders StringSet
	// OmitQuery is a set of query parameters to exclude from path calculations.
	// Requests with different content in these parameters can still return the
	// same unique path.
	OmitQuery StringSet
	// MungeRequestBody can be used to edit which bytes of the request body
	// are used to calculate the path CRC. It may be nil or return the same
	// io.Reader that is passed in. It does not alter the request that is sent
	// to the server.
	MungeRequestBody func(*http.Request, io.Reader) io.Reader
}

PathGenerator creates a unique path for a given *http.Request.

func NewPathGenerator

func NewPathGenerator() *PathGenerator

NewPathGenerator creates a new generator for recording path names.

func (*PathGenerator) RecordingPath

func (p *PathGenerator) RecordingPath(req *http.Request) (*RecordingPath, error)

RecordingPath returns the unique path for the given request.

func (*PathGenerator) RequestCRC

func (p *PathGenerator) RequestCRC(req *http.Request) (string, error)

RequestCRC generates a checksum based on the contents of any headers, query string parameters and body in the request. Any headers in OmitHeaders or any query string parameters in OmitQuery are not considered. If there are no headers, query string parameters and body to consider, returns an empty string.

type Recording

type Recording struct {
	Status     string      `json:"status,omitempty"`
	StatusCode int         `json:"status_code,omitempty"`
	Proto      string      `json:"proto,omitempty"`
	ProtoMajor int         `json:"proto_major,omitempty"`
	ProtoMinor int         `json:"proto_minor,omitempty"`
	Headers    http.Header `json:"headers,omitempty"`
	Body       []byte      `json:"-"`
}

A Recording represents a recorded HTTP server response. The fields map directly to fields in http.Response, except for Body, which is the body of the server response.

func LoadRecording

func LoadRecording(path string) (*Recording, error)

LoadRecording loads a Recording object from the given file path.

func NewRecording

func NewRecording(res *http.Response) (*Recording, error)

NewRecording returns a new, populated Recording struct from the given *http.Response. The http.Response Body is read and replaced.

func (*Recording) Response

func (r *Recording) Response() *http.Response

Response returns an *http.Response object from the populated Recording.

func (*Recording) Save

func (r *Recording) Save(path string) error

Save writes the Recording to the given path. The file is written to a temporary file and then renamed to ensure atomicity.

type RecordingPath

type RecordingPath struct {
	// contains filtered or unexported fields
}

RecordingPath contains a relative path for a recording.

func (*RecordingPath) GenericPath

func (r *RecordingPath) GenericPath() string

GenericPath returns a generic path for the request. The filename portion is always "request.json", even if a checksum was calculated.

func (*RecordingPath) Path

func (r *RecordingPath) Path() string

Path returns a canonical filename generated for the request. If a checksum can be calculated over the request query parameters, headers and body, the filename portion of the path will be "recording." + checksum + "".json". If there are no query parameters, no headers and no body, the returned path will be GenericPath().

type RoundTripper

type RoundTripper struct {
	// RoundTripper is the http.RoundTripper used to process HTTP requests if
	// a recorded response is not available on disk. It will be unused if
	// Record is false.
	http.RoundTripper
	// Dir is the base directory where HTTP responses are read from and recored
	// to.
	Dir string
	// Mode determines if responses are recorded, played back, or recorded only
	// if missing.
	Mode int
	// PathGenerator is used to generate unique paths for retrieving and saving
	// responses. The paths generated are relative to Dir.
	*PathGenerator
	// StrictPath, if true, will prevent RoundTripper from loading responses
	// without a checksum. The default is to attempt to load a recording from
	// the path without a checksum in cases where the path including the
	// checksum does not exist.
	StrictPath bool
}

RoundTripper implemnts a wrapper around an instance of the http.RoundTripper interface type. It attempts toload canned responses from recordings on disk. If one is not found, it can also use the wrapped RoundTripper to fetch the response and record it to disk for later use.

func (*RoundTripper) RoundTrip

func (r *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip wraps the underyling RoundTrip implementation in order to enable loading or recording HTTP server responses.

type StringSet

type StringSet map[string]struct{}

StringSet implements a set of string values.

func DefaultOmitHeaders

func DefaultOmitHeaders() StringSet

DefaultOmitHeaders returns a default set of headers to omit from recording path generation.

func NewStringSet

func NewStringSet(args ...string) StringSet

NewStringSet returns a new set initialized with optional values.

func (StringSet) Add

func (ss StringSet) Add(args ...string)

Add adds the provided value(s) to the set.

func (StringSet) Del

func (ss StringSet) Del(args ...string)

Del removes the provided value(s) from the set.

Jump to

Keyboard shortcuts

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