Rogue is a simple service for writing tests for webhooks and other outbound HTTP+JSON requests.
Rogue allows you to write tests that ensure
- your code makes HTTP+JSON requests to external services with the correct properties
- your code behaves correctly when external services return responses with particular sets of properties
Rogue provides a simple API that tests can call to
- create apps that can receive pre-defined HTTP+JSON requests, log the requests, and send pre-defined responses
- retrieve the logs from the apps
Before you use Rogue, you should consider using Mockbin, a similar service written by Mashape. Mockbin is the more mature service and may be the better choice depending on what you're testing and how you write your tests.
To install Rogue from source:
git clone git@github.com:uberflip/rogue-webhooks.git
cd rogue-webhooks
npm install
npm test
Rogue stores its configuration settings in package.json
's config
property.
By default, Rogue is configured to listen for HTTP traffic on port 5000. To change the HTTP port, edit package.json
and set httpPort
appropriately.
Other configuration settings are described below. Most configuration is done through Rogue's API when the service is running.
npm start
Rogue lets your tests create apps made up of handlers that match requests and return responses. For example, this is a simple app with a single handler that matches requests to POST /users
and returns a 201 Created
response with a Location
header and a JSON string body:
{
"handlers": [
{
"id": "on-create-user",
"request": {
"method": "POST",
"url": "/users"
},
"response": {
"status": 201,
"headers": {
"Location": "/users/123"
},
"body": "/users/123"
}
}
]
}
Your test can create an app with POST /apps
:
curl -X POST http://localhost:8080/apps -d @samples/matchEverything.json
"/apps/40f7f6db-7e19-451f-8cd9-a6376cd9378b"
Your test can then send requests with any method to any URL under /apps/:app_id/test
:
curl -X POST http://localhost:8080/apps/40f7f6db-7e19-451f-8cd9-a6376cd9378b/test/users -d ...
curl -X GET http://localhost:8080/apps/40f7f6db-7e19-451f-8cd9-a6376cd9378b/test/users/123
curl -X DELETE http://localhost:8080/apps/40f7f6db-7e19-451f-8cd9-a6376cd9378b/test/users/123
...
If a handler matches the request, the request will be logged, and the handler's response will be returned. Otherwise, 500 Internal Server Error
will be returned:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json; charset=utf-8
Content-Length: 45
...
{
"message": "No matching handler found."
}
Your test can retrieve the app's logs with GET /apps/:app_id/logs
:
curl http://localhost:8080/apps/40f7f6db-7e19-451f-8cd9-a6376cd9378b/logs
{
"data": [
{
"date": "2016-03-11T22:24:37.144Z",
"request": {
"protocol": "http",
"method": "POST",
"fullUrl": "/apps/d08a8a40-cd3a-4825-99e8-bf494280cdaa/test/users",
"relativeUrl": "/users",
"headers": {
"content-type": "application/json",
"host": "localhost:9300",
"accept": "application/json",
"content-length": "26",
"connection": "keep-alive"
},
"body": {
"username": "colincoller"
}
},
"handler": "on-create-user",
"response": {
"status": 201,
"headers": {
"Location": "/users/123"
},
"body": "/users/123"
}
},
{
"date": "2016-03-11T22:24:37.154Z",
"request": {
"protocol": "http",
"method": "GET",
"fullUrl": "/apps/d08a8a40-cd3a-4825-99e8-bf494280cdaa/test/users/123",
"relativeUrl": "/users/123",
"headers": {
"content-type": "application/json",
"host": "localhost:9300",
"connection": "keep-alive"
},
"body": {}
},
"handler": "on-get-user",
"response": {
"status": 200,
"body": {
"username": "colincoller"
}
}
}
]
}
Finally, your test can delete the app with DELETE /apps/:app_id
:
curl -X DELETE http://localhost:8080/apps/40f7f6db-7e19-451f-8cd9-a6376cd9378b
Handlers can match requests on protocol, method, URL, parameters, and headers:
{
"handlers": [
{
"id": "on-get-user",
"request": {
"protocol": "https",
"method": "GET",
"url": "/users/:id"
url
and headers
values are matched using the url-pattern package and support parameters. headers
keys and all other request properties are matched exactly.
Each of these request properties is optional. If you don't supply a request property, the handler will ignore that property when matching requests. If you don't supply any request properties, the handler will match every request. For example, matchEverything.json defines an app that matches every request, regardless of protocol, method, URL, etc, and sends 200 OK
:
{
"handlers": [
{
"id": "on-anything",
"response": {
"status": 200,
"body": "Everything is going to be 200 OK"
}
}
]
}
When a handler matches a request, the handler will send a response. The response includes a HTTP status coode, optional headers, and an optional body.
{
"handlers": [
{
"id": "on-create-user",
"response": {
"status": 201,
"headers": {
"Location": "/users/123"
},
"body": "/users/123"
The response will automatically include the Content-Type
and Content-Length
headers when appropriate.
Apps can have multiple handlers matching different requests and sending different responses. Handlers are invoked one at a time, in the order they are defined in the app, until a handler matches the request. For example, multipleHandlers.json defines an app with three handlers:
- Match
POST .../users
and send201 Created
with aLocation
header and a JSON string body - Match
GET .../users/:id
and send200 OK
with a JSON object body - Match
DELETE .../users/:id
and send204 No Content
{
"handlers": [
{
"id": "on-create-user",
"request": {
"method": "POST",
"url": "/users"
},
"response": {
"status": 201,
"headers": {
"Location": "/users/123"
},
"body": "/users/123"
}
},
{
"id": "on-get-user",
"request": {
"method": "GET",
"url": "/users/:id"
},
"response": {
"status": 200,
"body": {
"username": "colincoller"
}
}
},
{
"id": "on-delete-user",
"request": {
"method": "DELETE",
"url": "/users/:id"
},
"response": {
"status": 204
}
}
]
}
Handlers can delay responses by an arbitrary amount of time by setting the delay
property on the handler's response object. delay
is in milliseconds.
{
"handlers": [
{
"id": "on-get-user-1",
"response": {
"delay": 3000
This is useful when testing how webhooks deal with timeouts.
Handlers can match requests a maximum number of times before falling through to subsequent handlers by setting the maxMatches
property on the handler object.
{
"handlers": [
{
"id": "on-get-user-1",
"maxMatches": 1
This is useful when testing retries due to errors. For example, errorRetries.json defines an app with two handlers that match GET .../users/:id
:
- On the first request, send
500 Internal Server Error
- On the second request (when retrying), send
200 OK
with a JSON object body
{
"handlers": [
{
"id": "on-get-user-1",
"maxMatches": 1,
"request": {
"method": "GET",
"url": "/users/:id"
},
"response": {
"status": 500
}
},
{
"id": "on-get-user-2",
"maxMatches": 1,
"request": {
"method": "GET",
"url": "/users/:id"
},
"response": {
"status": 200,
"body": {
"username": "colincoller"
}
}
}
]
}
This is also useful when testing retries due to timeouts. For example, timeoutRetries.json defines an app with two handlers that match GET .../users/:id
:
- On the first request, delay the response for longer than the caller will wait
- On the second request (when retrying), send
200 OK
with a JSON object body
{
"handlers": [
{
"id": "on-get-user-1",
"maxMatches": 1,
"request": {
"method": "GET",
"url": "/users/:id"
},
"response": {
"delay": 3000,
"status": 200,
"body": {
"username": "The caller should have timed out in less than 3 seconds and so should never see this."
}
}
},
{
"id": "on-get-user-2",
"maxMatches": 1,
"request": {
"method": "GET",
"url": "/users/:id"
},
"response": {
"status": 200,
"body": {
"username": "colincoller"
}
}
}
]
}
This is also useful when testing complex sequences of webhook calls where the same request is made multiple times and the test requires different responses for each. For example, differentResponses.json defines an app with three handlers that all match GET .../users/:id
:
- On the first request (before the user would have been created), send
404 Not Found
- On the second request (after the user has been created), send
200 OK
with a JSON object body - On the third and subsequent requests (after the user has been deleted), send
404 Not Found
{
"handlers": [
{
"id": "on-get-user-1",
"maxMatches": 1,
"request": {
"method": "GET",
"url": "/users/:id"
},
"response": {
"status": 404
}
},
{
"id": "on-get-user-2",
"maxMatches": 1,
"request": {
"method": "GET",
"url": "/users/:id"
},
"response": {
"status": 200,
"body": {
"username": "colincoller"
}
}
},
{
"id": "on-get-user-3",
"request": {
"method": "GET",
"url": "/users/:id"
},
"response": {
"status": 404
}
}
]
}
Rogue logs all requests to an app. Each log entry includes the date the request was received, key request properties, the identifier of the handler that matched the request, and key response properties.
{
"data": [
{
"date": "2016-03-11T22:24:37.144Z",
"request": {
"protocol": "http",
"method": "POST",
"fullUrl": "/apps/d08a8a40-cd3a-4825-99e8-bf494280cdaa/test/users",
"relativeUrl": "/users",
"headers": {
"content-type": "application/json",
"host": "localhost:9300",
"accept": "application/json",
"content-length": "26",
"connection": "keep-alive"
},
"body": {
"username": "colincoller"
}
},
"handler": "on-create-user",
"response": {
"status": 201,
"headers": {
"Location": "/users/123"
},
"body": "/users/123"
}
},
{
"date": "2016-03-11T22:24:37.154Z",
"request": {
"protocol": "http",
"method": "GET",
"fullUrl": "/apps/d08a8a40-cd3a-4825-99e8-bf494280cdaa/test/users/123",
"relativeUrl": "/users/123",
"headers": {
"content-type": "application/json",
"host": "localhost:9300",
"connection": "keep-alive"
},
"body": {}
},
"handler": "on-get-user",
"response": {
"status": 200,
"body": {
"username": "colincoller"
}
}
}
]
}
Rogue logs in a single log for the app rather than a log for each handler. This simplifies test scenarios where you want to assert that multiple webhook calls have been made in a particular order.
Rogue can listen both for HTTP and HTTPS without the use of a proxy. To enable HTTPS, edit package.json
, set https
to true
, and set httpsPort
, httpsKey
, and httpsCert
appropriately.
{
"config": {
"https": true,
"httpsPort": 8443,
"httpsCert": "/path/to/cert",
"httpsKey": "/path/to/key"
Rogue can require basic authentication to call its static API without the use of a proxy. To enable basic authentication, edit package.json
and set username
and password
appropriately.
{
"config": {
"username": "colincoller",
"password": "HighlandPark21!",
Rogue supports pluggable persistence. The only persistence currently included is memory
. If you want to build your own persistence (e.g. MySQL), create a new class in src/persistence/
(src/persistence/mysql.js
), and set persistence
to the name of the file without the .js suffix (mysql
). The tests in test/persistence.js
are written to be executed against any persistence implementation.
{
"config": {
"persistence": "mysql
This is a list of key features that Mockbin has that Rogue does not:
- HAR: Rogue's logs don't use the HAR format.
- Content negotiation: Rogue only supports JSON.
- JSONP: Rogue doesn't support JSONP.
- User interface: Rogue doesn't have a user interface for creating apps, testing handlers, or viewing logs. If you're looking for visual testing and debugging tools, see John Sheehan's excellent, if slightly-outdated, list of API and webhook testing tools.
If you'd like to contribute one of these features, or any features or bug fixes, please submit a pull request!