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
- 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
Freeze APIs for testing with ability to extend it easily.
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;
}
};
http-tape can be configured via either config file or command-line arguments
Command-line | Config | Description |
---|---|---|
--help |
- | Print possible arguments with description and exit |
--config |
- | Path (relative to cwd or abs) to config file |
--mode |
mode |
Single or composed mode to run in |
--target |
targets |
For cli syntax is 'port:target', for config is object of the form {[port]: target} |
--player-template |
playerTemplatePath |
Path to template file for player |
--tape-template |
tapeTemplatePath |
Path to template file for tapes |
--export |
exportPath |
Path to directory that will be used as export root (default is ./recorded ) |
--body-codec |
bodyCodec |
Path to body-codec module |
--middlewares |
middlewares |
Path to middlewares module |
--tape-name-generator |
tapeNameGenerator |
Path to tapeNameGenerator module |
--overwrite-src |
overwriteSrc |
Overwrite sources in 'export' dir |
- 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
From higher to lowest:
- Command-line arguments
- Custom config file
- Base config file (config.base.js)
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 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);
}
};
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 fortruth
-
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 ifkey
of first argument is defined -
eqVal
, args: (val
: any value) - returns compare function that returns true if first agument is strictly equal toval
-
cmpKeys
, args: (cmpMap
: object of the form{[key]: cmpFn}
) - returns compare function that compares each providedkey
of comparing objects with correspondingcmpFn
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 applymapFn
on each argument and compare results withcmp
-
eqKeys
, args: (keys
: array of keys) - returns compare function that returns true if all providedkeys
are strictly equal in comparing objects -
eqKeysVal
, args: (obj
: object of the form{[key]: val}
) - returns compare function that returns true if each providedkey
inside first argument is equal toval
-
hasKeys
, args: (keys
: array of keys) - returns compare function that returns true if first argument has all providedkeys
-
onlyKeys
, args (keys
: array of keys) - returns compare function that returns true if first argument has all providedkeys
(and only their) -
exactKeys
, args (keys
: array of keys) - returns compare function that returns true if each comparing object contains providedkeys
(and only their), and also if each key is equal to each other -
cmpUrl
: args: (cmp
: compare function) - returns compare function that transforms arguments intoURL
object before applycmp
function on them -
cmpSearchParams
: args: (cmp
: compare function) - returns compare function that transformsSearchParams
class into object of the form{[queryKey]: queryVal}
before applycmp
function on them