mrak / stubby4node

A configurable server for mocking/stubbing external systems during development.
266 stars 62 forks source link
javascript server stubbing testing

Build Status NPM version

stubby4node

A configurable server for mocking/stubbing external systems during development.

stubby takes endpoint descriptors in the form of a YAML or JSON file that tell it how to respond to incoming requests. For each incoming request, configured endpoints are checked in-order until a match is found.

Table of Contents

Installation

via npm

npm install -g stubby

This will install stubby as a command in your PATH. Leave off the -g flag if you'd like to use stubby as an embedded module in your project.

via source

git clone https://github.com/mrak/stubby4node.git
cd stubby4node
npm start -- <stubby args>

Supported Runtimes

Development is on x86-64 Linux.

Starting the Server(s)

Some systems require you to sudo before running services on certain ports (like 80)

[sudo] stubby

Command-line Switches

stubby [-a <port>] [-c <file>] [-d <file>] [-h] [-k <file>] [-l <hostname>] [-p <file>] [-q]
       [-s <port>] [-t <port>] [-v] [-w] [-H]

-a, --admin <port>             Port for admin portal. Defaults to 8889.
-c, --cert <file>              Certificate file. Use with --key.
-d, --data <file>              Data file to pre-load endoints. YAML or JSON format.
-h, --help                     This help text.
-k, --key <file>               Private key file. Use with --cert.
-l, --location <hostname>      Hostname at which to bind stubby.
-q, --quiet                    Prevent stubby from printing to the console.
-p, --pfx <file>               PFX file. Ignored if used with --key/--cert
-s, --stubs <port>             Port for stubs portal. Defaults to 8882.
-t, --tls <port>               Port for https stubs portal. Defaults to 7443.
-v, --version                  Prints stubby's version number.
-w, --watch                    Auto-reload data file when edits are made.
-H, --case-sensitive-headers   Do not normalize response headers to lower-case.

When used from the command-line, stubby responds to the SIGHUP signal to reload its configuration.

Endpoint Configuration

This section explains the usage, intent and behavior of each property on the request and response objects.

Here is a fully-populated, unrealistic endpoint:

-  request:
      url: ^/your/awesome/endpoint$
      method: POST
      query:
         exclamation: post requests can have query strings!
      headers:
         content-type: application/xml
      post: >
         <!xml blah="blah blah blah">
         <envelope>
            <unaryTag/>
         </envelope>
      file: tryMyFirst.xml
   response:
    - status: 200
      latency: 5000
      headers:
         content-type: application/xml
         server: stubbedServer/4.2
      body: >
         <!xml blah="blah blah blah">
         <responseXML>
            <content></content>
         </responseXML>
      file: responseData.xml
    - status: 200
      body: "Haha!"

request

This object is used to match an incoming request to stubby against the available endpoints that have been configured.

url (required)

This is the simplest you can get:

-  request:
      url: /

A demonstration using regular expressions:

-  request:
      url: ^/has/to/begin/with/this/

-  request:
      url: /has/to/end/with/this/$

-  request:
      url: ^/must/be/this/exactly/with/optional/trailing/slash/?$

method

-  request:
      url: /anything
      method: GET
-  request:
      url: /anything
      method: [GET, HEAD]

-  request:
     url: ^/yonder
     method:
       -  GET
       -  HEAD
       -  POST

query

-  request:
     url: ^/with/parameters$
     query:
       search: search terms
       filter: month

NOTE: repeated querystring keys (often array representations) will have their values converted to a comma-separated list.

/url?array=one&array=two

will be matched by:

- request:
    url: ^/url$
    query:
      array: one,two

post

-  request:
      url: ^/post/form/data$
      post: name=John&email=john@example.com

file

-  request:
      url: ^/match/against/file$
      file: postedData.json
      post: '{"fallback":"data"}'

postedData.json

{"fileContents":"match against this if the file is here"}

json

Although not required, it is recommended to also specify a application/json header requirement.

-  request:
      url: ^/match/against/jsonString$
      headers:
         content-type: application/json
      json: '{"key1":"value1","key2":"value2"}'

JSON strings may contain "key": "value" pairs in any order: {"key1":"value1", "key2":"value2"} is equivalent to {"key2":"value2", "key1":"value1"}

headers

The following endpoint only accepts requests with application/json post values:

-  request:
      url: /post/json
      method: post
      headers:
         content-type: application/json

response

Assuming a match has been made against the given request object, data from response is used to build the stubbed response back to the client.

ALSO: The response property can also be a yaml sequence of responses that cycle as each request is made.

ALSO: The response property can also be a url (string) or sequence of object/urls. The url will be used to record a response object to be used in calls to stubby. When used this way, data from the request portion of the endpoint will be used to assemble a request to the url given as the response.

- request:
    url: /single/object
  response:
    status: 204

- request:
    url: /single/url/to/record
  response: http://example.com

- request:
    url: /object/and/url/in/sequence
  response:
  - http://google.com
  - status: 200
    body: 'second hit'

status

-  request:
      url: ^/im/a/teapot$
      method: POST
   response:
      status: 420

body

-  request:
      url: ^/give/me/a/smile$
   response:
      body: ':)'

file

-  request:
      url: /
   response:
      file: extremelyLongJsonFile.json

headers

-  request:
      url: ^/give/me/some/json$
   response:
      headers:
         content-type: application/json
      body: >
         [{
            "name":"John",
            "email":"john@example.com"
         },{
            "name":"Jane",
            "email":"jane@example.com"
         }]

latency

-  request:
      url: ^/hello/to/jupiter$
   response:
      latency: 800000
      body: Hello, World!

Dynamic Token Interpolation

While stubby is matching request data against configured endpoints, it is keeping a hash of all regular expression capture groups along the way. These capture groups can be referenced in response data. Here's an example

-  request:
      method: [GET]
      url: ^/account/(\d{5})/category/([a-zA-Z]+)
      query:
         date: "([a-zA-Z]+)"
      headers:
         custom-header: "[0-9]+"

   response:
      status: 200
      body: Returned invoice number# <% url[1] %> in category '<% url[2] %>' on the date '<% query.date[1] %>', using header custom-header <% headers.custom-header[0] %>

The url regex ^/account/(\d{5})/category/([a-zA-Z]+) has two defined capturing groups: (\d{5}) and ([a-zA-Z]+). The query regex has one defined capturing group: ([a-zA-Z]+).

Although the headers do not have capturing groups defined explicitly (no regex sections within parenthesis), the individual headers' fully-matched value is still accessible in a template (see Capture group IDs).

Templating body and file

The response.body can have token interpolations following the format of < %PROPERTY_NAME[CAPTURING_GROUP_ID] %>. If it is a token that corresponds to headers or query member matches, then the token structure would be `<% HEADERS_OR_QUERY.[KEY_NAME][CAPTURING_GROUP_ID] %>.

  response:
    body: The "content-type" header value was <% headers.content-type[0] %>.

NOTE: If you are using the file property for your responses, keep in mind that the both the file name and contents are interpolated. In other words, the <% ... %> will appear in the files' contents as well as on the line in your configuration that has response.file

Capture group IDs

The CAPTURING_GROUP_ID is determined by the regular expression used. The index of 0 will be the full-text that matches the regular expression.

Capture groups start at index 1 and correspond to the usage of parentheses.

Let's demonstrate with the example from above:

- request:
    url: ^/account/(\d{5})/category/([a-zA-Z]+)

If the incoming url is /account/54/category/users, the following would be the capture groups:

<% url[0] %> -> /account/54/categroy/users
<% url[1] %> -> 54
<% url[2] %> -> users

Let's take a more complicated example with sub-groups as captures:

- request:
    url: ^/resource/(([a-z]{3})-([0-9]{3}))$

If the incoming url is /resource/abc-123, the capture groups would be:

<% url[0] %> -> /resource/abc-123
<% url[1] %> -> abc-123
<% url[2] %> -> abc
<% url[3] %> -> 123

Troubleshooting

The Admin Portal

The admin portal is a RESTful(ish) endpoint running on localhost:8889. Or wherever you described through stubby's options.

Supplying Endpoints to Stubby

Submit POST requests to localhost:8889, PUT requests to localhost:8889/:id*, or load a data-file (-d) with the following structure for each endpoint:

YAML

-  request:
      url: ^/path/to/something$
      method: POST
      headers:
         authorization: "Basic usernamez:passwordinBase64"
      post: this is some post data in textual format
   response:
      headers:
         Content-Type: application/json
      latency: 1000
      status: 200
      body: Your request was successfully processed!

-  request:
      url: ^/path/to/anotherThing
      query:
         a: anything
         b: more
      method: GET
      headers:
         Content-Type: application/json
      post:
   response:
      headers:
         Content-Type: application/json
         Access-Control-Allow-Origin: "*"
      status: 204
      file: path/to/page.html

-  request:
      url: ^/path/to/thing$
      method: POST
      headers:
         Content-Type: application/json
      post: this is some post data in textual format
   response:
      headers:
         Content-Type: application/json
      status: 304

JSON

[
  {
    "request": {
      "url": "^/path/to/something$",
      "post": "this is some post data in textual format",
      "headers": {
         "authorization": "Basic usernamez:passwordinBase64"
      },
      "method": "POST"
    },
    "response": {
      "status": 200,
      "headers": {
        "Content-Type": "application/json"
      },
      "latency": 1000,
      "body": "Your request was successfully processed!"
    }
  },
  {
    "request": {
      "url": "^/path/to/anotherThing",
      "query": {
         "a": "anything",
         "b": "more"
      },
      "headers": {
        "Content-Type": "application/json"
      },
      "post": null,
      "method": "GET"
    },
    "response": {
      "status": 204,
      "headers": {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*"
      },
      "file": "path/to/page.html"
    }
  },
  {
    "request": {
      "url": "^/path/to/thing$",
      "headers": {
        "Content-Type": "application/json"
      },
      "post": "this is some post data in textual format",
      "method": "POST"
    },
    "response": {
      "status": 304,
      "headers": {
        "Content-Type": "application/json"
      }
    }
  }
]

If you want to load more than one endpoint via file, use either a JSON array or YAML list (-) syntax. On success, the response will contain Location in the header with the newly created resources' location

Getting the ID of a Loaded Endpoint

Stubby adds the response-header X-Stubby-Resource-ID to outgoing responses. This ID can be referenced for use with the Admin portal.

Getting the Current List of Stubbed Endpoints

Performing a GET request on localhost:8889 will return a JSON array of all currently saved responses. It will reply with 204 : No Content if there are none saved.

Performing a GET request on localhost:8889/<id> will return the JSON object representing the response with the supplied id.

The Status Page

You can also view the currently configured endpoints by going to localhost:8889/status

Changing Existing Endpoints

Perform PUT requests in the same format as using POST, only this time supply the id in the path. For instance, to update the response with id 4 you would PUT to localhost:8889/4.

Deleting Endpoints

Send a DELETE request to localhost:8889/<id>

The Stubs Portal

Requests sent to any url at localhost:8882 (or wherever you told stubby to run) will search through the available endpoints and, if a match is found, respond with that endpoint's response data

How Endpoints Are Matched

For a given endpoint, stubby only cares about matching the properties of the request that have been defined in the YAML. The exception to this rule is method; if it is omitted it is defaulted to GET.

For instance, the following will match any POST request to the root url:

-  request:
      url: /
      method: POST
   response: {}

The request could have any headers and any post body it wants. It will match the above.

Pseudocode:

for each <endpoint> of stored endpoints {

   for each <property> of <endpoint> {
      if <endpoint>.<property> != <incoming request>.<property>
         next endpoint
   }

   return <endpoint>
}

Programmatic API

The Stubby module

Add stubby as a module within your project's directory:

    npm install stubby

Then within your project files you can do something like:

    var Stubby = require('stubby').Stubby;
    var mockService = new Stubby();

    mockService.start();

What can I do with it, you ask? Read on!

start(options, [callback])

start([callback])

Identical to previous signature, only all options are assumed to be defaults.

stop([callback])

closes the connections and ports being used by stubby's stubs and admin portals. Executes callback afterward.

get(id, callback)

Simulates a GET request to the admin portal, with the callback receiving the resultant data.

get(callback)

Simulates a GET request to the admin portal, with the callback receiving the resultant data.

post(data, [callback])

put(id, data, [callback])

delete([id], callback)

Example

var Stubby = require('stubby').Stubby;

var stubby1 = new Stubby();
var stubby2 = new Stubby();

stubby1.start({
  stubs: 80,
  admin: 81,
  location: 'localhost',
  data: [{
    request: { url: "/anywhere" }
  },{
    request: { url: "/but/here" }
  }]
});

stubby2.start({
  stubs: 82,
  admin: 83,
  location: '127.0.0.2'
});

See Also

TODO

Non-breaking changes

Breaking changes

NOTES