1.0.1 • Published 3 years ago

http-tape v1.0.1

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

http-tape

http-tape is a proxy that can proxy requst/response to/from target addresses (as reverse-proxy), record it as JS scripts, which can be modified, and reproduce them later without connection with target end-point

Terms

  • Tape - JS file that produced by recording request/response
  • Player - Node.js application that generated after running http-tape
  • Target - target API address to proxy requests to

Purpose

Freeze APIs for testing with ability to extend it easily.

Contents

  1. Usage
  2. Configuration
    1. Options
    2. Modes
    3. Config priority
    4. Module path resolving
  3. Tapes
    1. Matching incoming requests

Usage

First of all you need to choose at least one Target, for example we'll be use this one: https://jsonplaceholder.typicode.com.

Start http-tape in RECORD mode and targeted on api and listening requests on port 3000:

http-tape --mode RECORD --target 3000:https://jsonplaceholder.typicode.com

Then to record new tapes you must do some requests on http://localhost:3000

curl -i 'http://localhost:3000/posts/1'
curl -i -X POST 'http://localhost:3000/posts' -H 'Content-type: application/json; charset=UTF-8' -d '{"title": "foo", "body": "bar", "userId": 2}'
curl -i -X PUT 'http://localhost:3000/posts/101' -H 'Content-type: application/json; charset=UTF-8' -d '{"title": "foo", "body": "baz", "id": 101}'
curl -i -X DELETE 'http://localhost:3000/posts/101'

After that to produce same responses on these requests you need run http-proxy in PLAY mode:

http-tape --mode PLAY --target 3000:https://jsonplaceholder.typicode.com

And now if you'll do the same requsts as those you did earlier you'll get same responses but now without target api.

But if you try to make request on path that hasn't been recorded as tape, you'll get 404 Not found response.

You can change this behaviour by composing multiple modes, consider you want to proxy requests that didn't match any tape to original target:

http-tape --mode 'PLAY|FORWARD' --target 3000:https://jsonplaceholder.typicode.com

After all you can modify recorded tapes to achieve your own needs. By default recorded tapes are stored in <current-working-directory>/recorded/<target>.

Consider you want to return dynamically constructed response according to data from request's query then you need modify one of recorded tapes like that:

const {copy} = require('../utils');
const {cmpKeys, cmpUrl, cmpSearchParams, hasKeys, eq} = require('../compare');

const {request, response} = {
	"request": {
		"method": "POST",
		"headers": {...},
		"url": "https://jsonplaceholder.typicode.com/posts?userId=2",
		"body": {...}
	},
	"response": {
		"headers": {...},
		"status": 201,
		"body": {
			"title": "fo",
			"body": "ba",
			"userId": 2,
			"id": 101
		}
	}
};

const compare = cmpKeys({
	method: eq,
	url: cmpUrl(cmpKeys({
		pathname: eq,
		searchParams: cmpSearchParams(hasKeys(['userId']))
	}))
});

const match = (incomingRequest) => compare(incomingRequest, request);

module.exports = {
	priority: 0,
	match,
	async respond(incomingRequest) {
		if (!match(incomingRequest)) {
			return;
		}

		const modifiedResponse = copy(response); // copy response object to avoid modifying it permanently between all requests
		const url = new URL(incomingRequest.url);
		const searchParms = url.searchParms;
		modifiedResponse.body.userId = searchParams.get('userId');

		return modifiedResponse;
	}
};

Configuration

http-tape can be configured via either config file or command-line arguments

Options

Command-lineConfigDescription
--help-Print possible arguments with description and exit
--config-Path (relative to cwd or abs) to config file
--modemodeSingle or composed mode to run in
--targettargetsFor cli syntax is 'port:target', for config is object of the form {port: target}
--player-templateplayerTemplatePathPath to template file for player
--tape-templatetapeTemplatePathPath to template file for tapes
--exportexportPathPath to directory that will be used as export root (default is ./recorded)
--body-codecbodyCodecPath to body-codec module
--middlewaresmiddlewaresPath to middlewares module
--tape-name-generatortapeNameGeneratorPath to tapeNameGenerator module
--overwrite-srcoverwriteSrcOverwrite sources in 'export' dir

Modes

  • RECORD - proxy incoming request to target and record new tape from result
  • PLAY - reply with previously recorded tapes if incoming request is matched
  • FORWARD - just forward incoming request to target

Also you can compose modes. Some useful examples are:

  • PLAY|FORWARD - reply with tape if is matched, if not - proxy to target
  • RECORD|PLAY - reply with tape if is matched or proxy request to target and record new tape

Config priority

From higher to lowest:

  1. Command-line arguments
  2. Custom config file
  3. Base config file (config.base.js)

Module path resolving

You can provide your own modules for next purposes:

  • Middlewares - user middlewares for manipulation request/response objects
  • Body codec - decode/encode body part from request/response objects
  • Tape name generator - produce string that will be used as base-name for tape filenames

When you do it via command-line arguments, path relative to current working directory or absolute is expected

but if configuration done via config file, path must be either relative to export root directory or absolute

Tapes

Tapes are just common-js modules that export object with two keys:

  • priority : (number) - highest number has highest priority (that means that tape with higher priority will respond before others)
  • respond : (fn) - function that matches incoming request with one of previously recorded and if it does, it returns corresponding response object or if it doesn't - nothing must be returned

example:

const {copy} = require('../utils');

const {request, response} = ...;
const compare = (a, b) => ...;
const match = (incomingRequest) => compare(incomingRequest, request);

module.exports = {
	priority: 0,
	match,
	async respond(incomingRequest) {
		if (!match(incomingRequest)) {
			return;
		}

		return copy(response);
	}
};

Matching incoming requests

Main purpose of a tape is to match an incoming request object and return corresponding response.

By default matching is done with applying compare function on two arguments, first is incoming request object (incomingRequest) and second is recorded previously (request)

Signature of compare function is: (a: any, b: any) => boolean

Tapes are generated with this compare function by default:

const {cmpKeys, cmpUrl, cmpSearchParams, exactKeys, eq} = require('../compare');

...

const compare = cmpKeys({ // compare each provided key of comparing objects with corresponding compare functions
	method: eq, // compare `method` key on equality
	url: cmpUrl( // compare `url` key as parsed object ...
		cmpKeys({ // compare keys of parsed url object with corresponding compare functions
			pathname: eq, // compare `pathname` key on equality
			searchParams: cmpSearchParams( // compare `searchParams` key as parsed object
				exactKeys([ ... `all query keys` ]) // each comparing object must contain provided keys (and only their), and they must be equal with each others
			)
	}))
});

This function is built via composition of other compare functions, which can help you to fit your own needs in matching request objects with each other.

Full list of compare helpers with their descriptions:

Pure compare functions:

  • truth - always return truth;
  • ignore - alias for truth
  • eq - strict equality
  • defined - returns true if first argument is defined

Functions that produce compare functions with provided arguments:

  • definedKey, args: (key: key which must be defined) - returns compare function that returns true if key of first argument is defined
  • eqVal, args: (val: any value) - returns compare function that returns true if first agument is strictly equal to val
  • cmpKeys, args: (cmpMap: object of the form {[key]: cmpFn}) - returns compare function that compares each provided key of comparing objects with corresponding cmpFn function
  • cmpEvery, args: (cmps: array of compare functions) - returns compare function that returns true if every provided function return true
  • cmpMapped, args: (cmp: compare function, mapFn: transform function) - returns compare function that will apply mapFn on each argument and compare results with cmp
  • eqKeys, args: (keys: array of keys) - returns compare function that returns true if all provided keys are strictly equal in comparing objects
  • eqKeysVal, args: (obj: object of the form {[key]: val}) - returns compare function that returns true if each provided key inside first argument is equal to val
  • hasKeys, args: (keys: array of keys) - returns compare function that returns true if first argument has all provided keys
  • onlyKeys, args (keys: array of keys) - returns compare function that returns true if first argument has all provided keys (and only their)
  • exactKeys, args (keys: array of keys) - returns compare function that returns true if each comparing object contains provided keys (and only their), and also if each key is equal to each other
  • cmpUrl: args: (cmp: compare function) - returns compare function that transforms arguments into URL object before apply cmp function on them
  • cmpSearchParams: args: (cmp: compare function) - returns compare function that transforms SearchParams class into object of the form {[queryKey]: queryVal} before apply cmp function on them