Injection
mountebank allows JavaScript injection for predicates and response types for situations where the built-in ones are not sufficient
Though mountebank has gone to some trouble to make injections as hassle-free as
possible, there are a couple of things to be aware of. First, when validating stub creation,
mountebank does not validate any injected functions. Second, all injections have full access
to a node.js runtime environment, including the ability to require
in different
modules. Of course, such power comes with its own challenges, which leads us to our third point: with
injections, you can crash mountebank. mountebank has made a noble and valiant effort to be robust
in the face of errors, but he is not as clever as you are. If you find yourself coding injections
frequently because of missing functionality that you believe would be generally useful,
mountebank humbly requests you to add a
feature request.
There are five options for injecting JavaScript into mountebank. The first two,
predicate injection and response injection, are described below. The third and fourth,
post-processing responses through the decorate
behavior or through the
wait
behavior, are described
on the behaviors page. Finally, determining the end
of a TCP request is described on the tcp protocol page.
Predicate injection
Predicate injection allows you to pass a JavaScript function to decide whether the
stub should match or not. Unlike other predicates, predicate injections bind to the entire
request
object, which allows you to base the result on multiple fields. They
can return truthy or falsy to decide whether the predicate matches. mountebank doesn't know
what that means, so he always returns true
or false
.
The injected function should take a single parameter, which will contain the following fields:
Field | Description |
---|---|
request |
The entire request object, containing all request fields |
state |
An initially empty object that will be shared between predicate
and response injection functions as well as the decorate behavior.
You can use it to capture and mutate shared state. |
logger |
A logger object with debug , info , warn ,
and error functions to write to the mountebank logs. |
The following example uses injection to satisfy a complicated multi-field predicate for HTTP:
POST /imposters HTTP/1.1
Host: localhost:12382
Accept: application/json
Content-Type: application/json
{
"port": 4545,
"protocol": "http",
"stubs": [
{
"responses": [{ "is": { "statusCode": 400 } }],
"predicates": [{
"inject": "function (config) {\n\n function hasXMLProlog () {\n return config.request.body.indexOf('<?xml') === 0;\n }\n\n if (config.request.headers['Content-Type'] === 'application/xml') {\n return !hasXMLProlog();\n }\n else {\n return hasXMLProlog();\n }\n}"
}]
}
]
}
Injections certainly don't help the readability of the JSON. If you're loading imposters through a config file,
look at the stringify
function supported in file templating with the
--configfile
command line option to support storing the functions in a readable format.
Let's look at our injected function appropriately formatted:
function (config) {
function hasXMLProlog () {
return config.request.body.indexOf('<?xml') === 0;
}
if (config.request.headers['Content-Type'] === 'application/xml') {
return !hasXMLProlog();
}
else {
return hasXMLProlog();
}
}
It matches on XML content missing a prolog, or a prolog added to non-XML content. First we'll send a request that matches the first condition:
POST /test HTTP/1.1
Host: localhost:4545
Content-Type: application/xml
<customer>
<name>Test</name>
</customer>
HTTP/1.1 400 Bad Request
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked
Now we'll match on the second condition:
POST /test HTTP/1.1
Host: localhost:4545
Content-Type: application/json
<?xml>
<customer>
<name>Test</name>
</customer>
HTTP/1.1 400 Bad Request
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked
DELETE /imposters/4545 HTTP/1.1
Host: localhost:12382
Accept: application/json
Response injection
Response injection allows you to dynamically construct the response based on the request and previous requests. The response object should match the structure defined in the appropriate protocol page linked to on the sidebar. mountebank will use the default value documented on the protocol page for any response fields you leave empty. The predicate object takes a single parameter which will contain the following fields:
Field | Description |
---|---|
request |
The entire protocol-specific request object |
state |
mountebank maintains this variable for you
that will be empty on the first injection. The same variable will be passed into
every inject function, allowing you to remember state between calls,
even between stubs. It will be shared with predicate inject functions
and decorate behaviors. |
callback |
Asynchronous injection is supported through this parameter.
If your function returns a value, the imposter will consider it to be synchronous. If
it does not return a value, it must invoke the callback parameter with the
response object. The following injection takes advantage of the node.js environment
it will run in to proxy most of the request to localhost:5555. It records the request
and response to the state variable, and only proxies if certain request
parameters have not already been sent.
|
logger |
A logger object with debug , info , warn ,
and error functions to write to the mountebank logs. |
The example below will use two imposters, an origin server and a server that proxies to the origin server. It would be better implemented using a proxy response type, but has sufficient complexity to demonstrate many nuances of injection.
First we'll create the origin server at port 5555. To help us keep track of the
two imposters, we'll set the name
parameter, which will show up in the logs.
POST /imposters HTTP/1.1
Host: localhost:12382
Accept: application/json
Content-Type: application/json
{
"port": 5555,
"protocol": "http",
"name": "origin",
"stubs": [{
"responses": [{ "inject": "(config) => {\n config.logger.info('origin called');\n config.state.requests = config.state.requests || 0;\n config.state.requests += 1;\n return {\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ count: config.state.requests })\n };\n}" }]
}]
}
The injected function for the origin server imposter is formatted below. This
uses the state
parameter, and sets the requests
field on it to return how many times it's been called:
(config) => {
config.logger.info('origin called');
config.state.requests = config.state.requests || 0;
config.state.requests += 1;
return {
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ count: config.state.requests })
};
}
Now let's create the proxy imposter to try it out:
POST /imposters HTTP/1.1
Host: localhost:12382
Accept: application/json
Content-Type: application/json
{
"port": 4546,
"protocol": "http",
"name": "proxy",
"stubs": [
{
"predicates": [{ "equals": { "method": "GET", "path": "/counter" } }],
"responses": [{ "inject": "function (config) {\n var count = config.state.requests ? Object.keys(config.state.requests).length : 0;\n\n return {\n body: `There have been ${count} proxied calls`\n };\n}" }]
},
{
"responses": [{ "inject": "function (config) {\n var cacheKey = config.request.method + ' ' + config.request.path;\n\n if (typeof config.state.requests === 'undefined') {\n config.state.requests = {};\n }\n\n if (config.state.requests[cacheKey]) {\n config.logger.info('Using previous response');\n config.callback(config.state.requests[cacheKey]);\n }\n\n var http = require('http'),\n options = {\n method: config.request.method,\n hostname: 'localhost',\n port: 5555,\n path: config.request.path,\n headers: config.request.headers\n },\n httpRequest = http.request(options, response => {\n var body = '';\n response.setEncoding('utf8');\n response.on('data', chunk => {\n body += chunk;\n });\n response.on('end', () => {\n var stubResponse = {\n statusCode: response.statusCode,\n headers: response.headers,\n body: body\n };\n config.logger.info('Successfully proxied: ' + JSON.stringify(stubResponse));\n config.state.requests[cacheKey] = stubResponse;\n config.callback(stubResponse);\n });\n });\n httpRequest.end();\n}" }]
}
]
}
The first stub uses the following function. It depends on the second stub to
set a variable on the state
parameter each time it proxies, and returns
a count of proxied calls. Note that it uses the exact same requests
field that the origin server injection uses on the state
parameter,
but for a different purpose. This is OK - state
is shared between
stubs on one imposter, but not shared between imposters. This function
require
s in node's util
module, showing an example
that takes advantage of the runtime environment.
function (config) {
var count = config.state.requests ? Object.keys(config.state.requests).length : 0;
return {
body: `There have been ${count} proxied calls`
};
}
The second stub uses an asynchronous proxy with a cache:
function (config) {
var cacheKey = config.request.method + ' ' + config.request.path;
if (typeof config.state.requests === 'undefined') {
config.state.requests = {};
}
if (config.state.requests[cacheKey]) {
config.logger.info('Using previous response');
config.callback(config.state.requests[cacheKey]);
}
var http = require('http'),
options = {
method: config.request.method,
hostname: 'localhost',
port: 5555,
path: config.request.path,
headers: config.request.headers
},
httpRequest = http.request(options, response => {
var body = '';
response.setEncoding('utf8');
response.on('data', chunk => {
body += chunk;
});
response.on('end', () => {
var stubResponse = {
statusCode: response.statusCode,
headers: response.headers,
body
};
config.logger.info('Successfully proxied: ' + JSON.stringify(stubResponse));
config.state.requests[cacheKey] = stubResponse;
config.callback(stubResponse);
});
});
httpRequest.end();
}
Our first request should trigger a proxy call:
GET /first HTTP/1.1
Host: localhost:4546
HTTP/1.1 200 OK
Content-Type: application/json
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked
{ "count": 1 }
A second call also proxies because the path is different.
GET /second HTTP/1.1
Host: localhost:4546
HTTP/1.1 200 OK
Content-Type: application/json
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked
{ "count": 2 }
The third request should be served from the cache of the proxy imposter:
GET /first HTTP/1.1
Host: localhost:4546
HTTP/1.1 200 OK
Content-Type: application/json
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked
{ "count": 1 }
Finally, let's query the other stub on the proxy imposter:
GET /counter HTTP/1.1
Host: localhost:4546
HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked
There have been 2 proxied calls
This is the most complicated example in the documentation, and took
some time to get it working (these examples in the docs are executed
as part of the test suite). This is appropriate since injection is the most
complicated thing you can do in the tool. When injection errors are detected,
mountebank logs the full eval
'd code, including the bits it's added
on top of your code. It also logs JSON representation of the parameter.
As always, if you get stuck, ask for help.
DELETE /imposters/5555 HTTP/1.1
Host: localhost:12382
Accept: application/json
DELETE /imposters/4546 HTTP/1.1
Host: localhost:12382
Accept: application/json
Including other npm modules
It is entirely possible to include other npm modules in your injection scripts.
However, you will need to install the module globally to ensure your injection function
can access it. For example, if you wanted to use the underscore
module,
you'd first have to install it on the machine running mountebank:
npm install -g underscore
At that point, you can simply require
it into your function.