mountebank

mountebank - over the wire test doubles


the apothecary

Injection

mountebank allows JavaScript injection for predicates and response types for situations where the built-in ones are not sufficient

Injection only works if mb is run with the --allowInjection flag.

If you have injection enabled, you should set either the --localOnly or --ipWhitelist flags as well. See the security page for more details.

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:48451
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

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:48451
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:48451
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 requires 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.

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.