mountebank

mountebank - over the wire test doubles

Fork me on GitHub

the apothecary

Proxies

Proxies are one of the most powerful features of mountebank, rivaled only by the mighty injection. Each proxied response is recorded, either as an entirely new stub in the imposter's stubs array, or as a new response in the stub's responses array. Proxies allow you to define the fields which should be included in newly created predicates. The proxy will ensure that the proxied request data for those fields are filled in.

proxy response types take the following parameters:

Parameter Default Type Description
to required A URL without the path (e.g. http://someserver:3000 or tcp://someserver:3000) Defines the origin server that the stub should proxy to.
mode proxyOnce string, one of proxyOnce or proxyAlways. Defines the replay behavior of the proxy. proxyOnce always records the proxied call in the stubs array in front of itself, so the same call is never proxied twice. proxyAlways saves the proxied call after itself in the stubs array. This allows you to capture different responses for the same call. You can later replay proxyAlways stubs by issuing a GET or DELETE to the imposter with the removeProxies and replayable query params, and re-POSTing the imposter.
predicateGenerators [] array An array of objects that defines how the predicates for new stubs are created.
injectHeaders {} object Key-value pairs of headers to inject into the proxied request. This is useful when the behavior of the system being proxied can be altered with appropriate headers (e.g. pointing REST links back to mountebank for further proxying). You can also use this to pass along an 'Accept-Encoding: identity' header to prevent the proxy from sending back compressed data. Existing headers will be overwritten if they match the injected headers.
addWaitBehavior false boolean If true, mountebank will add a wait behavior to the response with the same latency that the proxied call took. This is useful in load testing scenarios where you want to simulate the actual latency of downstream services that you're virtualizing.
addDecorateBehavior null string, JavaScript If defined, mountebank will add a decorate behavior to the response.

http and https proxies add three additional optional parameters for situations where the origin server expects to use mutual authentication and will request a client certificate:

Parameter Default Type Description
cert null A PEM-formatted string The SSL client certificate
key null A PEM-formatted string The SSL client private key
ciphers ALL A valid cipher (see this page for formats) For older (and insecure) https servers, this field allows you to override the cipher used to commuicate

It is occasionally useful to capture how long the original proxied request takes. mountebank stores the number of milliseconds for the request in the _proxyResponseTime field in the response.

Note, if you use a corporate proxy, then setting the standard shell http_proxy or https_proxy environment variables will be honored.

Understanding proxy behavior

The mode and predicateGenerators parameters define the behavior of the proxy. The default proxyOnce mode is simpler; it always creates a new stub. Imagine the following stubs array, set by us when we create the imposter:


"stubs": [
  {
    "responses": [
      {
        "proxy": {
          "to": "http://origin-server.com",
          "mode": "proxyOnce",
          "predicateGenerators": [
            {
              "matches": {
                "method": true,
                "path": true,
                "query": true
              }
            }
          ]
        }
      }
    ]
  }
]

When we issue a GET to /test?q=mountebank, the stub will proxy to http://origin-server.com/test?q=mountebank, and save off the response in a new stub in front of the proxy response:


"stubs": [
  {
    "predicates": [{
      "deepEquals': {
        "method": "GET",
        "path": "/test",
        "query": { "q": "mountebank" }
      }
    }],
    "responses": [
      { "is": ...saved response }
    ]
  }
  {
    "responses": [
      {
        "proxy": {
          "to": "http://origin-server.com",
          "mode": "proxyOnce",
          "predicateGenerators": [
            {
              "matches": {
                "method": true,
                "path": true,
                "query": true
              }
            }
          ]
        }
      }
    ]
  }
]

Because of mountebank's first-match policy on stubs, the next time the imposter receives a GET /test?q=mountebank request, the saved predicates on the newly created stub will match, and the recorded response will be replayed. If the imposter receives a GET /test?q=mountebank&sort=descending, then it will proxy again, creating a new stub, because the querystring is different.

The proxyAlways mode saves stubs behind the proxy stub. This allows you to record a richer set of interactions with the origin server, but requires you to save off the imposter representation and remove or reorder the proxy to replay those interactions.

Let's say you had the following stubs array:


"stubs": [
  {
    "responses": [
      {
        "proxy": {
          "to": "http://origin-server.com",
          "mode": "proxyAlways",
          "predicateGenerators": [
            { "matches": { "path": true } }
          ]
        }
      }
    ]
  },
  {
    "predicates": [
      { "equals": { "path": "/test" } }
    ],
    "responses": [
      { "is": { "body": "first response" } }
    ]
  }
]

The next time we send a request to /test, the request will still be proxied to http://origin-server.com/test. Since the predicate that we would normally create already exists after the proxy stub, the proxied response instead gets added to an existing stub's responses array.


"stubs": [
  {
    "responses": [
      {
        "proxy": {
          "to": "http://origin-server.com",
          "mode": "proxyAlways",
          "predicateGenerators": [
            { "matches": { "path": true } }
          ]
        }
      }
    ]
  },
  {
    "predicates": [
      { "deepEquals": { "path": "/test" } }
    ],
    "responses": [
      { "is": { "body": "first response" } },
      { "is": { ...saved response } }
    ]
  }
]

predicateGenerators accept the same predicate parameters as predicates: caseSensitive and except.

Examples

Our examples will proxy to the following origin server. To help us keep track of the imposters in the logs, we'll set the name field. We're using injection to return the number of times the proxy has been called to the user.


POST /imposters HTTP/1.1
Host: localhost:44762
Accept: application/json
Content-Type: application/json

{
  "port": 7575,
  "protocol": "http",
  "name": "origin",
  "stubs": [
    {
      "responses": [{
        "inject": "function (request, state) { state.calls = state.calls || 0; return { body: 'call ' + ++state.calls }; }"
      }]
    }
  ]
}

Select the behavior of the proxy below for a relevant example:

Let's create another imposter, this time using the proxyOnce mode to give us on-the-fly record and playback functionality.


POST /imposters HTTP/1.1
Host: localhost:44762
Accept: application/json
Content-Type: application/json

{
  "port": 6566,
  "protocol": "http",
  "name": "proxyOnce",
  "stubs": [
    {
      "responses": [
        {
          "proxy": {
            "to": "http://localhost:7575",
            "mode": "proxyOnce",
            "predicateGenerators": [
              {
                "matches": {
                  "method": true,
                  "path": true,
                  "query": true
                }
              }
            ]
          }
        }
      ]
    }
  ]
}

The first call will trigger a call to the origin server:


GET /test?first=1 HTTP/1.1
Host: localhost:6566

HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

call 1

The second call also triggers a call to the origin server, since it's on a different path:


GET /dir?first=1 HTTP/1.1
Host: localhost:6566

HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

call 2

The third call triggers a call to the origin server because it has a different querystring:


GET /test?first=1&second=2 HTTP/1.1
Host: localhost:6566

HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

call 3

If we send one of the requests already sent, the response will be replayed:


GET /test?first=1&second=2 HTTP/1.1
Host: localhost:6566

HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

call 3

First let's create the proxy:


POST /imposters HTTP/1.1
Host: localhost:44762
Accept: application/json
Content-Type: application/json

{
  "port": 6568,
  "protocol": "http",
  "name": "proxyAlways",
  "stubs": [
    {
      "responses": [
        {
          "proxy": {
            "to": "http://localhost:7575",
            "mode": "proxyAlways",
            "predicateGenerators": [
              {
                "matches": {
                  "method": true,
                  "path": true,
                  "query": true
                }
              }
            ]
          }
        }
      ]
    }
  ]
}

Let's record a response:


GET /test?first=1 HTTP/1.1
Host: localhost:6568

HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

call 4

We'll make the same request. Unlike the proxyOnce mode, this request will still be proxied:


GET /test?first=1 HTTP/1.1
Host: localhost:6568

HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

call 5

Now let's make on request to a different path:


GET /dir?first=1 HTTP/1.1
Host: localhost:6568

HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

call 6

Now we'll stop the origin server.


DELETE /imposters/7575 HTTP/1.1
Host: localhost:44762
Accept: application/json

And now we'll stop the proxy imposter. We'll use the removeProxies query parameter to remove the proxy responses and stubs, and the replayable query parameter to remove information not needed to recreate the imposter.

Tip: We could also use the mb save command with the --removeProxies parameter to save the imposters without the proxies, and restart using the --configfile command line option. Everything in mountebank is API-first, and this example will demonstrate using the API.


DELETE /imposters/6568?removeProxies=true&replayable=true HTTP/1.1
Host: localhost:44762
Accept: application/json

Now for the coup de grâce. We saved off the response we received from the DELETE call and are re-POSTing it.


POST /imposters HTTP/1.1
Host: localhost:44762
Accept: application/json
Content-Type: application/json

{
  "protocol": "http",
  "port": 6568,
  "name": "proxyAlways",
  "stubs": [
    {
      "predicates": [
        {
          "deepEquals": {
            "method": "GET"
          }
        },
        {
          "deepEquals": {
            "path": "/test"
          }
        },
        {
          "deepEquals": {
            "query": {
              "first": "1"
            }
          }
        }
      ],
      "responses": [
        {
          "is": {
            "statusCode": 200,
            "headers": {
              "Connection": "close",
              "Date": "Mon, 27 Jan 2014 04:38:44 GMT",
              "Transfer-Encoding": "chunked"
            },
            "body": "call 4"
          }
        },
        {
          "is": {
            "statusCode": 200,
            "headers": {
              "Connection": "close",
              "Date": "Mon, 27 Jan 2014 04:38:44 GMT",
              "Transfer-Encoding": "chunked"
            },
            "body": "call 5"
          }
        }
      ]
    },
    {
      "predicates": [
        {
          "deepEquals": {
            "method": "GET"
          }
        },
        {
          "deepEquals": {
            "path": "/dir"
          }
        },
        {
          "deepEquals": {
            "query": {
              "first": "1"
            }
          }
        }
      ],
      "responses": [
        {
          "is": {
            "statusCode": 200,
            "headers": {
              "Connection": "close",
              "Date": "Mon, 27 Jan 2014 04:38:44 GMT",
              "Transfer-Encoding": "chunked"
            },
            "body": "call 6"
          }
        }
      ]
    }
  ]
}

Notice the two responses on the first stub. Let's trigger the first one:


GET /test?first=1 HTTP/1.1
Host: localhost:6568

HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

call 4

Since the responses array represents a circular buffer, the second call triggers the next response in order:


GET /test?first=1 HTTP/1.1
Host: localhost:6568

HTTP/1.1 200 OK
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
Transfer-Encoding: chunked

call 5

First let's create a mirror imposter so we can see what headers are sent. This imposter just takes the request headers it receives and sends them back in the response:


POST /imposters HTTP/1.1
Host: localhost:44762
        Accept: application/json
Content-Type: application/json

{
  "port": 7002,
  "protocol": "http",
  "name": "Mirror",
  "stubs": [
    {
      "responses": [
        {
          "is": {
            "body": "The body."
          },
          "_behaviors": {
            "decorate": "function (req, res) { res.headers = req.headers; }"
          }
        }
      ]
    }
  ]
}

Now, let's set up another imposter that will proxy requests to the mirror imposter. We'll also set up the inject headers field to insert some custom headers in the outgoing request:


POST /imposters HTTP/1.1
Host: localhost:44762
        Accept: application/json
Content-Type: application/json

{
  "port": 7001,
  "protocol": "http",
  "name": "Inject Headers",
  "stubs": [
    {
      "responses": [
        {
          "proxy": {
            "to": "http://localhost:7002",
            "mode": "proxyAlways",
            "injectHeaders": {
               "X-My-Custom-Header-One": "my first value",
               "X-My-Custom-Header-Two": "my second value"
            }
          }
        }
      ]
    }
  ]
}

Then we send a request to our proxy imposter:


GET / HTTP/1.1
Host: localhost:7001

HTTP/1.1 200 OK
Accept: application/json
Host: localhost:7002
Connection: close
Date: Thu, 09 Jan 2014 02:30:31 GMT
X-My-Custom-Header-One: my first value
X-My-Custom-Header-Two: my second value
Transfer-Encoding: chunked

The body.

Now we can see that the X-My-Custom-Header-One and Two headers were returned back to us, reflected back from the mirror imposter after being injected into the outgoing request.