README
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
- Supported Runtimes
- Starting the Server(s)
- Command-line Switches
- Endpoint Configuration
- Dynamic Token Interpolation
- The Admin Portal
- The Stubs Portal
- Programmatic API
- See Also
- TODO
- NOTES
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
- node.js - latest and currently supported LTS versions
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)
- is a full-fledged regular expression
- This is the only required property of an endpoint.
- signify the url after the base host and port (i.e. after
localhost:8882
). - any query parameters are stripped (so don't include them, that's what
query
is for)./url?some=value&another=value
becomes/url
- no checking is done for URI-encoding compliance.
- If it's invalid, it won't ever trigger a match.
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
- defaults to
GET
. - case-insensitive.
- can be any of the following:
- HEAD
- GET
- POST
- PUT
- DELETE
- etc.
- request:
url: /anything
method: GET
- it can also be an array of values.
- request:
url: /anything
method: [GET, HEAD]
- request:
url: ^/yonder
method:
- GET
- HEAD
- POST
query
values are full-fledged regular expressions
if omitted, stubby ignores query parameters for the given url.
a yaml hashmap of variable/value pairs.
allows the query parameters to appear in any order in a uri
The following will match either of these:
/with/parameters?search=search+terms&filter=month
/with/parameters?filter=month&search=search+terms
- 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
- is a full-fledged regular expression
- if omitted, any post data is ignored.
- the body contents of the server request, such as form data.
- request:
url: ^/post/form/data$
post: name=John&email=john@example.com
file
- if supplied, replaces
post
with the contents of the locally given file.- paths are relative from where the
--data
file is located
- paths are relative from where the
- if the file is not found when the request is made, falls back to
post
for matching. - allows you to split up stubby data across multiple files
- request:
url: ^/match/against/file$
file: postedData.json
post: '{"fallback":"data"}'
postedData.json
{"fileContents":"match against this if the file is here"}
- if
postedData.json
doesn't exist on the filesystem when/match/against/file
is requested, stubby will match post contents against{"fallback":"data"}
(frompost
) instead.
json
- not used if
post
orfile
are present. - will be parsed into a JavaScript object.
- allows you to specify a JSON string that will be deeply compared with a JSON request
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
- values are full-fledged regular expressions
- if omitted, stubby ignores headers for the given url.
- case-insensitive matching of header names.
- a hashmap of header/value pairs similar to
query
.
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
- the HTTP status code of the response.
- integer or integer-like string.
- defaults to
200
.
- request:
url: ^/im/a/teapot$
method: POST
response:
status: 420
body
- contents of the response body
- defaults to an empty content body
- request:
url: ^/give/me/a/smile$
response:
body: ':)'
file
- similar to
request.file
, but the contents of the file are used as thebody
.
- request:
url: /
response:
file: extremelyLongJsonFile.json
headers
- similar to
request.headers
except that these are sent back to the client.
- 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
- time to wait, in milliseconds, before sending back the response
- good for testing timeouts, or slow connections
- 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).
body
and file
Templating 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
- Make sure that the regex you used in your stubby configuration actually does what it supposed to do. Validate that it works via the node REPL (or similar) before using it in stubby
- Make sure that the regex has capturing groups for the parts of regex you want to capture as token values. In other words, make sure that you did not forget the parentheses within your regex if your token IDs start from
1
- Make sure that you are using token ID zero when wanting to use full regex match as the token value
- Make sure that the token names you used in your template are correct: check that property name is correct, capturing group IDs, token ID of the full match, the
<%
and%>
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:
request
: describes the client's call to the servermethod
: GET/POST/PUT/DELETE/etc.url
: the URI regex string.query
: a key/value map of query string parameters included with the requestheaders
: a key/value map of headers the server should respond topost
: a string matching the textual body of the response.file
: if specified, returns the contents of the given file as the request post. If the file cannot be found at request time, post is used instead
response
: describes the server's response to the clientheaders
: a key/value map of headers the server should use in it's responselatency
: the time in milliseconds the server should wait before responding. Useful for testing timeouts and latencyfile
: if specified, returns the contents of the given file as the response body. If the file cannot be found at request time, body is used insteadbody
: the textual body of the server's response to the clientstatus
: the numerical HTTP status code (200 for OK, 404 for NOT FOUND, etc.)
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