Holoserve - Simple faking of HTTP APIs

This tool can be used to fake HTTP web APIs. It’s meant to be used in a testing environment, to make the test suite run faster and be independent from other API and network problems.

Concept

HoloServe runs a Goliath application server, that matches any incoming request to a list of request/response pairs. If a match is found, the corresponding response is returned. To do that the response is assembeled by a selection of responses. The selection is made by conditions on the current state. The name of the matched request/response pair is saved in a request history. If no match is found, a 404 is returned and the request data is stored in the bucket for unhandeled requests. These informations can be used to extend the server layout with missing request handlers.

To avoid too much duplication in the definition of the request/response-pairs, it is possible reference some fixture data that is shared between all pair definitions.

The pairs, state, history and bucket can be accessed via control routes, which are described below.

Installation

Assuming that ruby and gem are installed, simply type…

gem install holoserve

Run from the command line

To start up an empty Holoserve instance, type…

holoserve

To load the server with a couple of pairs, fixtures and define a state during start up, use these parameters.

holoserve -f 'path/to/fixtures/*.yaml' -p 'path/to/pairs/*.yaml' -s user_one=missing -s car_one=existing

Notice, that the files must have either the .yaml or the .json extension.

A full description of all options can be displayed with holoserve --help

-P, --port PORT                  The port holoserve should listen to.
-f, --fixture-files PATTERN      Load the specified fixture files on startup.
-p, --pair-files PATTERN         Load the specified pair files on startup.
-s, --state SETTING              Set a specific state. Use the pattern key=value. Can be applied multiple times.
-h, --help                       Shows the help message.

Control routes

If you’re using Ruby, you can control Holoserve via the Holoserve Connector gem.

GET /_control/pairs

Returns all pair definitions.

GET /_control/pairs/:id

Returns the specified pair definition.

PUT /_control/state

Sets the current state with the transmitted parameters.

GET /_control/state

Returns the current state.

Response example

{
  "user_one": "missing",
  "car_one": "existing"
}

GET /_control/bucket

Returns a list of all requests that has been received, but couldn’t be handled.

Response example

[
  {
    "method": "GET",
    "path": "/test",
    "headers": {
      "SERVER_SOFTWARE": "Goliath",
      "SERVER_NAME": "localhost",
      "SERVER_PORT": "4250",
      "REMOTE_ADDR": "127.0.0.1",
      "HTTP_ACCEPT": "*/*",
      "HTTP_USER_AGENT": "Ruby",
      "HTTP_HOST": "localhost:4250",
      "HTTP_VERSION": "1.1",
      "SCRIPT_NAME": "/test"
    }
  }
]

GET /_control/history

Returns a list of hashes, which include the names/id of pairs that has been triggered, with a request variant and a list of response variants.

Response example

[{"id":"test_request","request_variant":"default","response_variants":["default","alternative"]}]

DELETE /_control/history

Removes all entries from the history.

Pair file format

The request/response pair file should have the following format.

requests:
  default:
    imports:
      - path: "test_fixtures.users.0"
        as: "parameters"
        only: [ "username", "password" ]
    method: "POST"
    path: "/session"
    headers:
      HTTP_USER_AGENT: "Ruby"
    oauth:
      oauth_token: "12345"
responses:
  default:
    status: 200
  found:
    condition: "username == :existing"
    imports:
      - path: "test_fixtures.users.0"
        as: "json.user"
    transitions:
      session_one: "existing"
  not_found:
    condition: "username == :missing"
    json:
      message: "user not found"

The fixture data where this example pair definition relies on, could look like the following.

users:
  - email: "[email protected]"
    username: "one"
    password: "valid"

This example defines a request/response pair situation. It uses username=one and password=valid as parameters.

If the state includes condition: "user_one == :existing", the response would return the complete user object encoded as json. If the state includes user_one == :missing, the request would be replied with the message “user not found”.

The transitions part of the response allows you set the successor state. If a response is selected, the corresponding transitions will be evaluated.

requests:
  default:
    imports:
      - path: "test_fixtures.users.0.email"
        as: "parameters"
    method: "POST"
    path: "/valid_email"
responses:
  default:
    status: 200
  found:
    condition: "email == :existing"
    imports:
      - path: "test_fixtures.users.0.username"
        as: "json.user.username"
    transitions:
      session_one: "existing"
  not_found:
    condition: "email == :missing"
    json:
      message: "email address not found"

This example relies on the same fixture file used above.

This time it uses [email protected] as a parameter. If the state includes condition: "email == :existing", the response would return a username of the email address owner as json.

If the state includes email == :missing, the response would be a message “email address not found” in json.