This is the instance passed to the hook
function, under property mock
of the single argument object — hook: ({mock}) => {...}
.
mocksFormat
: the format to use to store mockssetMocksFormat(value)
set themocksFormat
value
kassette can store mocks in two different formats: folder
or har
.
The default value of the global mocksFormat
setting is folder
, except in the following case: if the global mocksHarFile
setting is defined and the global mocksFolder
setting is not defined, then the default value of the global mocksFormat
setting is har
.
When mocksFormat
is folder
, kassette stores each request with its response in one folder containing up to 7 files, as described below.
In below file names, [ext]
is an extension computed based on the actual type of the content in the file. If no extension could be determined, the file will actual drop the .[ext]
part.
Mock files:
data.json
: headers, status code and message, request time, creation date and body file namebody.[ext]
: the content of the body of the backend response
Input request (from client to proxy), for debug:
input-request-data.json
: headers, method, URL and body file nameinput-request-body.[ext]
: the content of the body
Forwarded request (from proxy to backend), for debug:
forwarded-request-data.json
: headers, method, URL, whether body was eventually astring
or aBuffer
and body file nameforwarded-request-body.[ext]
: the content of the body
Checksum: as described in dedicated section, if a checksum was computed, the content generated to compute it will be output in a file named checkum
.
Apart from the two first files from this list (data.json
and body.[ext]
), which are mandatory to be able to replay mocks, the generation of all the other files is mostly only useful to debug and can be disabled with the following settings:
saveInputRequestData
saveInputRequestBody
saveForwardedRequestData
saveForwardedRequestBody
saveChecksumContent
Also, the saveDetailedTimings
setting can be used to control whether detailed timings (blocked
, dns
, connect
, send
, wait
, receive
, ssl
) are stored in the timings
field in the data.json
file (in addition to the global time
field).
You likely won't have to change that, since most of the time you will want to keep your mocks under a same root folder, the distinction being made by calling setLocalPath
instead (see below).
mocksFolder
: the root folder of all mockssetMocksFolder(value)
: setmocksFolder
value by providing any combination of arrays of path parts, as forsetLocalPath
. You can pass an absolute path, or a relative one which will be resolved againstoptions.root
.
-
mockFolderFullPath
: the full, absolute path of the mock, built fromlocalPath
/defaultLocalPath
,mocksFolder
and possiblyoptions.root
(see below) ifmocksFolder
is not absolute -
localPath
: the local path of the mock, relative tomocksFolder
. Can be set by the user, otherwise will be the default valuedefaultLocalPath
-
defaultLocalPath
: the default local path of the mock, relative tomocksFolder
. It uses the URL pathname to build an equivalent folders hierarchy, and appends the HTTP method as a leaf folder. -
setLocalPath(value)
: setlocalPath
by providing any combination of arrays of path parts.Examples:
setLocalPath([mock.request.method, mock.request.pathname])
will use the HTTP method followed by the URL pathnamesetLocalPath([prefix, mock.request.pathname.split('/').slice(2), addSuffix ? [suffix, '-static-suffix'] : null])
will concatenateprefix
- all portions of the URL pathname except the first one (also excluding the very first one which is empty since
pathname
has a leading slash) - optionally a suffix sequence depending on a boolean
When mocksFormat
is har
, kassette can store several requests/responses in the same har file.
kassette mostly follows the har format specification.
If the har file name has the .yml
or .yaml
extension, kassette uses the YAML format instead of the standard JSON format to read and write the file.
Some parts of the specification are not implemented and some custom fields are added, as described in the API reference.
As with the folder
format, it is possible to control what is included in the har file through the following settings:
saveInputRequestData
saveInputRequestBody
saveForwardedRequestData
saveForwardedRequestBody
saveChecksumContent
saveDetailedTimings
mocksHarFile
: contains the full path to the har file to use. If the file name has the.yml
or.yaml
extension, kassette uses the YAML format instead of the standard JSON format to read and write the file.setMocksHarFile(value)
: sets themocksHarFile
value by providing any combination of arrays of path parts, as forsetMocksFolder
. You can pass an absolute path, or a relative one which will be resolved againstoptions.root
.
mockHarKey
: key under which the entry will be read or written in the har file. Can be set by the user, otherwise will be the default valuedefaultMockHarKey
. How the key is stored in the har file depends on the har key manager.defaultMockHarKey
: specifies the default mock har key to use in casesetMockHarKey
is not called with a non-null value. It is computed by calling the har key manager with the current request. With the default har key manager, it is the concatenation of the HTTP method with the full URL, separated by a forward slash.setMockHarKey(value)
: sets themockHarKey
value. If an array is set (which can be nested), it is flattened with null items removed, and joined with forward slashes to produce a string.
Each entry in a har file is supposed to have a corresponding unique key (a string). The har key manager is both a getter and a setter for the key of an entry.
The har key manager is a function that is called either to get the key of an entry (when the key parameter is undefined) or to set it (when the key parameter is defined).
Here is the default har key manager:
export const defaultHarKeyManager: HarKeyManager = (entry: HarFormatEntry, key?: string) => {
const defaultKey = joinPath(
entry._kassetteMockKey ?? [entry.request?.method, entry.request?.url],
);
if (key && key !== defaultKey) {
entry._kassetteMockKey = key;
return key;
}
return defaultKey;
};
The har key manager should not modify the entry when the key parameter is undefined.
When the key parameter is defined, the har key manager is supposed to change the provided entry, in order to store the key in it, because after the call, the entry will be persisted in the har file. In this case, the key parameter either comes from a call to setMockHarKey
, or from defaultMockHarKey
.
In order to compute the defaultMockHarKey
property, the har key manager is called with an entry that includes the request but not the response (and with an undefined key parameter).
In all cases, the har key manager is expected to return the key of the entry. If an array is returned (which can be nested), it is flattened with null items removed, and joined with forward slashes to produce a string.
The default har key manager is expected to work fine for most use cases, especially when working with a har file recorded with kassette. With the default har key manager, if a key is set with setMockHarKey
, it is stored in the _kassetteMockKey
field. Otherwise, the default key is the concatenation of the request method and url, with a separating forward slash. It can be useful to replace the default har key manager with a custom one especially when working with har files that are produced by other applications than kassette, if the default key is not convenient.
mocksHarKeyManager
: contains the mocks har key manager to use for this requestsetMocksHarKeyManager(value)
: sets themocksHarKeyManager
value
checksum({type?, format?, method?, pathname?, body?, query?, headers?, customData?})
: compute a checksum using content from the request. See details below.
It is semantically equivalent to choosing what matters to differentiate a request to another. Thanks to the computed checksum, you will be able to add it to the path of the mock and that way use different mocks for semantically different requests.
But it is difficult to predict what will actually be relevant or not for you, and that's why we provide many options to include/exclude data.
But first of all, a few properties related to how to output the checksum:
type
: customize the checksum type. Check Node.js API for more information:crypto.createHash(type)
. Default value issha256
.format
: customize the output format. Check Node.js API for more information:hash.digest(format)
. Default value ishex
.
To include or exclude data, not every kind of data has the same complexity. For instance, the HTTP method is simple: use it or don't use it. But for things like query parameters, headers, body: you might want to select/filter.
Here are the options you can give:
protocol
:- content: protocol (normalized to lower case) such as
http
orhttps
, without the trailing:
- possible values:
true
to include it,false
to exclude it- an object
{include: true|false}
which has the same meaning as above (provided for consistency with other options below)
- default value:
false
- content: protocol (normalized to lower case) such as
hostname
:- content: host name (normalized to lower case)
- possible values:
true
to include it,false
to exclude it- an object with properties:
include
:true
orfalse
, same meaning as abovefilter(hostname: string) => string
: a function used to filter the content of the hostname
- default value:
false
port
:- content: port number or an empty string when using the default port for the protocol
- possible values:
true
to include it,false
to exclude it- an object
{include: true|false}
which has the same meaning as above (provided for consistency with other options below)
- default value:
false
method
:- content: HTTP method (normalized to lower case)
- possible values:
true
to include it,false
to exclude it- an object
{include: true|false}
which has the same meaning as above (provided for consistency with other options below)
- default value:
false
(since method is in default mock's path)
pathname
:- content: URL's pathname
- possible values:
true
to include it,false
to exclude it- an object with properties:
include
:true
orfalse
, same meaning as abovefilter(pathname: string) => string
: a function used to filter the content of the pathname
- default is
false
(since pathname is in default mock's path)
body
:- content: body/payload
- possible values:
true
to include it,false
to exclude it- an object with properties:
include
:true
orfalse
, same meaning as abovefilter(body: Buffer) => Buffer | string
: a function used to filter the content of the body
- default is
true
, including the whole body without filtering
query
:- content: query parameters
- possible values:
true
to include it,false
to exclude it. Also, thecaseSensitive
option described below is used with its default value in this case.- an object with properties:
include
:true
orfalse
, same meaning as abovefilter(parameters: object) => object
: a function used to filter the query parameters. Note that if filter is provided, the options following below are ignored, that would be duplicate.caseSensitive
: whether keys should be treated case sensitive or not.true
by default. When set tofalse
, output object contains lower cased keys.- whitelist/blacklist options:
mode
:'whitelist'
(default) or'blacklist'
keys
: a list of keys to keep if inwhitelist
mode or to reject if inblacklist
mode. IfcaseSensitive
isfalse
, comparison of keys is not case sensitive.
- default is
true
, including all the query parameters
headers
, same as forquery
, except:- content: HTTP headers
- default value:
false
, because otherwise it would include all headers, and some might be always different (like dates for instance) caseSensitive
isfalse
by default
customData
: any custom value which can be JSON stringified
Note that all given filtering functions can be synchronous or asynchronous.
Also note that if you provide one of the options, giving a configuration object but omitting the include
property: it will be true
by default. That makes sense because if you configure the way one piece of content is included, you might want it to be included by default. Having include
is a way to easily enable/disable a piece of content using a variable, while for instance having a generic filter along with it.
Example:
const checksum = await mock.checksum({
type: 'MD5',
format: 'binary',
method: true, // equivalent to {include: true}
protocol: true,
hostname: true,
port: true,
pathname: {
// but not necessary, we will omit it for other properties
include: true,
// remove first part of pathname (pathname starts with leading slash)
filter: pathname => [''].concat(pathname.split('/').slice(2)).join('/'),
},
body: {
// remove all occurrences of given id pattern
filter: body => body.toString().replace(/.../g, '...'),
},
query: {
include: queryRelevant, // would be a boolean variable
mode: 'blacklist',
keys: ['id'], // would keep all except "id" parameter
},
headers: {
// would remove all headers which are dates (using a fictive function "isDate")
filter: headers => {
const output = {};
for (const [key, value] of Object.entries(headers)) {
if (!isDate(value)) output[key] = value;
}
return output;
}
},
customData: ..., // because we never want to block the users of our APIs
})
The method returns the actual checksum value, that you can then use for instance to add to the mock's path.
It also stores the computed content in property checksumContent
(in the mock
object), as a string. It is built according to your options and the request's data and used to compute the checksum. This can be handy for debugging.
Note that we designed the API so that it is usually not needed to call the checksum
method more than once for a given request/mock.
Also, checksum data is persisted, so that you can debug more easily, especially by committing it into your SCM to analyze changes across versions of your code. File is along with the other files of the mock under file name checksum
.
The delay that will be used to send the response to the client when the data is taken from the local mock is computed like this:
- default value is taken from the global user configuration, and resolution continues as described below
- if value is a number it will be used directly
- if value is
'recorded'
:- if the local mock has been read already, will return the value recorded in it
- if the local mock has not been read or doesn't exist, will return the value used for empty/default mocks
API:
delay
: get the currently computed delay, either from explicitly set input or from the default value from the global configurationsetDelay(delay)
: set the delay, any value as described above ('recorded'
or a number), or passnull
to unset it and use the default value
The mode drives how getPayloadAndFillResponse()
and process()
will behave:
'manual'
: don't do anything, leaving the responsibility to the user to call proper APIs to manage local files and/or backend querying, and response filling'remote'
: forward the request to the remote backend and never touch the local mock'download'
: get payload from remote backend by forwarding request, create the local mock from this payload, and fill the response with it'local_or_remote'
:- if local mock exists, read it and fill the response with it
- if local mock doesn't exist, do as for
'remote'
mode - typical use case: play with backend while basing part of the API on mocks
'local_or_download'
:- if local mock exists, read it and fill the response with it
- if local mock doesn't exist, do as for
'download'
mode - typical use case: complete missing mocks but keep existing ones intact
'local'
:- if local mock exists, read it and fill the response with it
- if local mock doesn't exist, create a minimal payload with a 404 status code, do not persist it and fill the response with it
API:
mode
: get the current mode, either explicitly set or the default value from the global configurationsetMode(mode)
: set the mode, any value as described above ('manual'
,'remote'
,'local_or_download'
,'local'
), or passnull
to unset it and use the default value
The URL of the remote backend, from which only protocol, hostname and port are used.
The default value is the special "*"
value, which means reading from the request the remote backend to target. This is useful when using kassette as a browser proxy.
Can be set to null
or ""
, in which case anything leading to sending the request to the remote backend will trigger an exception and stop the program.
API:
remoteURL
: get the current remote URL, either explicitly set or the default value from the global configuration, which can benull
setRemoteURL(url)
: set the remote URL, or passnull
to unset it and use the default value
The payload represents the content of a response from the backend, no matter if it actually comes from it or if it was created manually.
From the payload, response to the client can be filled.
The payload can also be persisted and read, to avoid contacting the backend later on.
A payload is considered read-only, so all of the properties below are read-only. When creating the payload manually, you need to specify all the properties.
data
headers
: map of registered headers, excluding the ignored headersignoredHeaders
: map of ignored headers'content-length'
: because the modification of the response body will change the content length anyways'connection'
,'keep-alive'
,'transfer-encoding'
: because connection-specific headers are managed internally
status
: an object{code, message}
, containing the status code as a number and the status message as a stringbodyFileName
: the path to the file containing thebody
content, relative to the mock'smockFolderFullPath
time
: the time it took to receive the response from the backend, in milliseconds
body
: the body content, as aBuffer
or a string
Most of the APIs accept and return a wrapped payload, which has the following properties:
payload
: the actual payload as described aboveorigin
: the source of the payload, which can have these values:'local'
: if the payload was read from local mock'remote'
: if the payload was fetched from the remote backend by forwarding the request'user'
: if the payload has been created from the user, manually usingcreatePayload(payload)
'proxy'
: if the payload has been created from kassette itself, especially for404 Not found
errors (in'local'
mode) and502 Bad Gateway
errors (when kassette cannot reach the remote server)
- for persisted payload (local files), see below the section "Managing local files"
async fetchPayload(): wrappedPayload
: forward the client request to the remote backend and get a wrapped payload from the response in outputcreatePayload(payload): wrappedPayload
: create a wrapped payload from the given payload datasetPayload(payload)
: set the current local payload, with a custom one you would have created
fillResponseFromPayload(wrappedPayload)
: use data present in given wrapped payload to fill in the responsesourcePayload
: as soon as response is filled with a payload, holds the reference to this payload's wrapper (wrapper is useful here to know where does the payload used for the response come from); before that it isundefined
async hasLocalMock()
: returnstrue
if there is a local mock for the current request, orfalse
otherwiseasync hasNoLocalMock()
: the opposite ofhasLocalMock
, for convenience
async readLocalPayload()
: returns a wrapped payload built from data persisted in local mock. If no local mock is present, returnsundefined
async persistPayload(wrappedPayload)
: takes the given wrapped payload and persists it in a local mock
async downloadPayload(): wrappedPayload
: combinefetchPayload()
andpersistPayload(wrappedPayload)
and return the wrapped payloadasync readOrDownloadPayload(): wrappedPayload
: return the wrapped local payload if exists usingreadLocalPayload()
, otherwise usedownloadPayload()
and return this wrapped payloadasync readOrFetchPayload(): wrappedPayload
: return the wrapped local payload if exists usingreadLocalPayload()
, otherwise usefetchPayload()
and return this wrapped payloadasync readLocalPayloadAndFillResponse()
: combinereadLocalPayload()
andfillResponseFromPayload(wrappedPayload)
if there is a local payload, returningtrue
, otherwise do nothing and returnfalse
async getPayloadAndFillResponse()
: depending on themode
, get the payload (remote / local / default) and usefillResponseFromPayload(wrappedPayload)
with that payload. If mode is'manual'
do nothing.
process()
is automatically called AFTER the user hook function is executed. See description to know how to prevent this.
async sendResponse()
: send the response back to the client, with the previously specified delay if payload is not remoteasync process()
: combinegetPayloadAndFillResponse()
andsendResponse()
- if mode is
'manual'
do nothing. That's the way to prevent anything useless or not wanted to be done automatically for you after the hook has finished executing. - it uses a private guard to make sure it is executed only once. Therefore, if you call it in the hook, the automatic call made for you after the hook execution will actually not do anything (same if you call it yourself multiple times).
- if mode is
The mock instance contains a property request
, with the API described below.
original
: the original Node.js object representing the request. Use it if you can't do what you want with this current wrapper.
url
: the URL as a Node.jsURL
objectprotocol
: the protocol part of the URL such ashttp
orhttps
, without the trailing:
hostname
: the hostname part of the URL (not including the port number, if any)port
: the port part of the URL (if different from the default port for the protocol)pathname
: the path part of the URL, including the leading/
(but not including the anchor/hash nor the query/search)queryParameters
: the query/search parameters taken from the URL, as a read-only map of strings
headers
: the headers, as defined on theoriginal
request objectmethod
: the HTTP method, in lower casebody
: the body content, as aBuffer
connectionsStack
: an array of objects{protocol, hostname, port}
describing the stack of connections. The first element in the array is the initial connection to the port kassette is listening to. A new object is added for each intercepted call to the HTTPCONNECT
method, with the corresponding hostname/port specified in the parameter ofCONNECT
. The protocol can behttp
orhttps
. Usually,connectionsStack
will contain two items when kassette is used as a browser proxy and intercepting a connection to a secure website (https), and only one item in other cases. It is however possible to use kassette for more advanced scenarios with multiple layers of proxy servers, and in that caseconnectionsStack
reflects the corresponding layers of proxy servers.connection
: a shortcut to the last item inconnectionsStack
The mock instance contains a property response
, with the API described below.
The idea is to store the data, be able to alter it as much as wanted, without never applying it to the actual response object. It is processed only when needed to send the response.
original
: the original Node.js object representing the response. Use it if you can't do what you want with this current wrapper.async send()
: send the response, applying the data previously given
body
: the body of the response.- if
json
is explicitly set totrue
, it will be serialized into JSON (andcontent-type
header will be set toapplication/json
) - it will also be if
json
is not explicitly set but thebody
value is not a string, not aBuffer
, and notnull
either - after that, the result will eventually be converted to a
Buffer
to be sent
- if
setData(data)
: a convenient method to set thebody
value and setjson
totrue
json
:- when set: explicitly set its value
- when read: return the current value of
json
, either explicitly set by the user or computed as described above forbody
setHeaders(headers)
: merge givenheaders
map with the previously set headers (initial set is an empty map)- a header value can be a number, a string, or an array of strings
- put a
null
value to suppress a header
headers
: the currently set headers
status
: an object{code, message}
, where each property is optional. Ifcode
is never given, a default value of200
is applied.
DISCLAIMER: As a user, you are not likely to need using any of this here.
The mock instance contains a property options
, with:
- the
root
path used to resolve relative paths in the application - the
userConfiguration
object, containing the processed user configuration (merged from all configuration inputs)- each property is wrapped in an object
{origin, value}
, whereorigin
tells where the value comes from andvalue
is the value of the property - origins can be:
'cli'
,'file'
,'api'
,'default'
- configuration properties are described in configuration documentation
- each property is wrapped in an object
skipLogs
: iftrue
, will simplify the logging output for this request handling iteration, logging only the one line when the request is received but nothing else afterwards
The package exports the following function to launch the proxy programmatically:
async runFromAPI(options)
options
:apiConfiguration
: the configuration object, as described in configuration documentation. Has the least precedence, behind the CLI and the configuration file. However, this object is forwarded to the configuration file'sgetConfiguration({apiConfiguration})
method, asapiConfiguration
, so you can apply your own logic to determine what configuration to actually useconfigurationPath
, optional: the path to a configuration file, so you can reuse one nicelyfileConfigurationContext
, optional: can be any value; is forwarded to the configuration file'sgetConfiguration({context})
method, ascontext
- returns a callback that can be used to shutdown the proxy, calling the
onExit
callback defined in configuration (if provided)