rexy 1.4.3 Documentation

A lightweight REST mock/proxy server

Rexy

Overview

Rexy is designed to allow dynamic testing of different scenarios during integration testing of REST endpoints. It is typically configured as a proxy server with the standard behaviour to simply forward requests on to the actual API endpoint. Rexy can then be configured to intercept certain requests and return specific responses. This can be helpful for testing error scenarios that may be difficult or impossible to trigger on an ad-hoc bases on the API endpoint itself.

Details of the different APIs to proxy as well as presets for responses are configured by a simple JSON file. Runtime configuration is performed via JMX or via the Wexy module as a web application.

Running

Start Rexy by running the com.github.samblake.rexy.Rexy class, optionally specifying a configuration file as an argument. Alternatively build via Maven and run the resulting jar.

FAQ

Can Rexy be used to test APIs without proxying?

Yes, it is not a requirement to specify an endpoint to proxy to. If a request is not intercepted a 502 response will be returned. This can be useful for testing APIs when there is only a production endpoint.

Can Rexy be configured without JMX?

Although it still uses JMX in the background the Wexy module provides a web interface for runtime configuration.

Can non-JSON response presets be configured?

Yes, they just need to be specified in their own files and referenced from the JSON configuration.

Configuration

On startup the configuration will be loaded from the classpath, falling back to the filesystem if that fails. The default path is `rexy.json`. A custom path can be supplied as a command line argument when running Rexy.

Rexy contains examples for two APIs, the metaweather API and the chucknorris.io API (see example-config.json).

Root

The root configures the general details of the Rexy application.

{
  "port": "8081",
  "baseUrl": "/",
  "scanPackages": ["com.github.samblake.rexy.module"]
  "modules": {
    "mock": {
      ...
    },
    "proxy": {
      ...
    }
  },
  "apis": [
    ...
  ],
  "imports": [
    ...
  ]
}
NameTypeDescription
baseUrlStringThe base path that all API endpoints run under. Defaults to <code>/</code>.
scanPackagesList of StringThe packages that should be scanned for modules.
modulesMap of Custom JSONA map of module name to module specific configurations. All module configurations have, as a minimum, an <code>enabled</code> element. All other values are defined by the module itself. Modules are disabled by default and must be explicitly set to be enabled.
apisList of ApiConfiguration details of the APIs that Rexy will handle.
importsList of StringA list of supplementary files, each containing a single API configuration.

Api

Models an API that Rexy can handle. Each will run under it's own path.

{
  "name": "metaweather",
  "baseUrl": "api",
  "contentType": "application/json",
  "proxy": "http://www.metaweather.com/api",
  "headers": {
    "Accept-Charset": "utf-8"
  },
  "endpoints": [
    ...
  ]
}
NameTypeDescription
nameStringThe name of the API.
baseUrlStringThe path the API will be deployed to.
contentTypeStringThe default content type to use for mocked API responses. This can be overridden by the endpoint or response.
proxyStringThe URL that requests to this API should be proxied to.
headersMap of StringThe default headers to use for mocked API responses. This can be overridden by the endpoint or response.
endpointsList of EndpointThe endpoints that are provided by the API.

Endpoint

Models an endpoint for an API.

{
  "name": "location",
  "method": "GET",
  "endpoint": "location/search/?query={query}",
  "matchers": {
     ...
  },
  "responses": [
    ...
  ]
}
NameTypeDescription
nameStringThe name of the endpoint.
methodMethodThe HTTP method of the endpoint.
endpointStringThe path of the endpoint. Named parameters can be specified in curly braces.
matchersMap of Custom JSONKey-value pairs of matcher name to custom JSON configuration.
responsesList of ResponseMock response presets.

Response

Models a mock response preset.

{
  "name": "Successful",
  "httpStatus": 200,
  "headers": {
    "abc": "Test 1",
    "xyz": "Test 2"
  }
  "body": {
    "title": "London",
    "location_type": "City",
    "woeid": 44418,
    "latt_long": "51.506321,-0.12714"
  }
}
NameTypeDescription
nameStringThe name of the preset.
headersMap of StringThe HTTP response headers to return.
bodyCustom JSONThe response body to return. If the response is JSON the the object can be supplied directly in the config, otherwise a reference to a text file containing the body should be supplied.

example config

example-config.json
{
  "port": "8081",
  "baseUrl": "/",
  "modules": {
    "delay": {
      "enabled": true
    },
    "mock": {
      "enabled": true,
      "interceptOnSet": true
    },
    "proxy": {
      "enabled": true
    }
  },
  "apis": [
    {
      "name": "metaweather",
      "baseUrl": "weather",
      "contentType": "application/json",
      "proxy": "http://www.metaweather.com/api",
      "endpoints": [
        {
          "name": "location",
          "method": "GET",
          "endpoint": "/location/search/?query={query}",
          "responses": [
            {
              "name": "Successful",
              "httpStatus": 200,
              "headers": {
                "Accept-Charset": "utf-8"
              },
              "body": "example-body.json"
            },
            {
              "name": "Invalid",
              "httpStatus": 401,
              "body": {
                "error": "invalid request"
              }
            }
          ]
        }
      ]
    }
  ],
  "imports": [
    "example-import.json"
  ]
}

example import

example-import.json
{
  "name": "chuck norris",
  "baseUrl": "chucknorris",
  "contentType": "application/json",
  "proxy": "https://api.chucknorris.io",
  "endpoints": [
    {
      "name": "joke",
      "endpoint": "/jokes/random",
      "method": "GET",
      "responses": [
        {
          "name": "Successful",
          "httpStatus": 200,
          "body": {
            "icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
            "id": "g6Tk9tCFQpeqqxY2Rh-sTw",
            "url": "https://api.chucknorris.io/jokes/g6Tk9tCFQpeqqxY2Rh-sTw",
            "value": "Phobias are afraid of Chuck Norris."
          }
        }
      ]
    },
    {
      "name": "category joke",
      "endpoint": "/jokes/random?category={category}",
      "method": "GET",
      "responses": [
        {
          "name": "Successful",
          "httpStatus": 200,
          "body":{
            "category": [
              "dev"
            ],
            "icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
            "id": "nmlts0sqrwifigqta-rv_g",
            "url": "https://api.chucknorris.io/jokes/nmlts0sqrwifigqta-rv_g",
            "value": "Chuck Norris's log statements are always at the FATAL level."
          }
        },
        {
          "name": "Error",
          "httpStatus": 404,
          "headers": {
            "content-type": "text/html; charset=UTF-8"
          }
        }
      ]
    },
    {
      "name": "categories",
      "endpoint": "/jokes/categories",
      "method": "GET",
      "responses": [
        {
          "name": "Successful",
          "httpStatus": 200,
          "body": [
            "explicit",
            "dev",
            "movie",
            "food",
            "celebrity",
            "science",
            "sport",
            "political",
            "religion",
            "animal",
            "history",
            "music",
            "travel",
            "career",
            "money",
            "fashion"
          ]
        }
      ]
    }
  ]
}

example body

example-body.json
{
  "title": "London",
  "location_type": "City",
  "woeid": 44418,
  "latt_long": "51.506321,-0.12714"
}

Modules

Rexy it built around pluggable chain of interceptors knows as modules. When Rexy receives a request it is passed to each module in order. Modifications can be made to the request as it is passed up the chain. When any of the modules generate a response the flow is instantly reversed and the response will be passed back down the chain in the opposite direction. Again modifications can be made to the response as it flows down the chain. When the initial module is reached the response will be returned to the originating server. All modules are configured to some extent by the Rexy JSON configuration file, however some (those that should be modified at runtime) can also be configured by JMX.

No modules are enabled by default. In order to enable a module it should be specified in the `modules` section in the configuration. Modules can be configured in any order but the final two modules in the chain should probably always be the mock module followed by the proxy module.

Removal Module

A module that removes a header from requests.
"removal": {
  "enabled": true,
  "headerName": "X-Requested-With"
}
Name Description Type Default
headerName The name of the header to removed. string X-Requested-With

Cors Module

A module that generates a preflight response for OPTIONS requests and adds/overrides the Access-Control-Allow-Headers header to the response of all other requests The module can be triggered conditionally based on a specified header value. The configuration is:

"cors": {
  "enabled": true,
  "allowOrigin": "*",
  "headerName": "X-Requested-With",
  "headerValue": "Wexy"
}
Name Description Type Default
allowOrigin The allowOrigin header value. string *
headerName The name of the header that should be used to conditionally trigger the module. string X-Requested-With
headerValue The value of the header that should be used to conditionally trigger the module. If no value is set then the module will trigger for all requests. string null

Proxy Module

A module that proxies a request to another URL. This will always write a response so will always be the last module in a chain.

To configure a proxy an API is needs to be defined configuration. The baseUrl of the API corresponds to the root path of the API on the Rexy server. The proxy value is the base URL that any request to that path will be forwarded to. The URL before, and including, the baseUrl value is swapped for the proxy value. This combined with the endpoint value forms the full URL that the request will be proxied to.

For example, in the following configuration, if Rexy was running on localhost, a request to http://localhost/metaweather-api would be converted to http://www.metaweather.com/api and a request to http://localhost/metaweather-api/location/search/?query=london would be proxied to http://www.metaweather.com/api/location/search/?query=london.

"apis": [
  {
    "name": "metaweather",
    "baseUrl": "metaweather-api/",
    "contentType": "application/json",
    "proxy": "http://www.metaweather.com/api",
    "endpoints": [
    {
      "name": "location",
      "endpoint": "location/search/?query={query}",
    }
  }
]

Template Module

A module that applies substitutions to a response body.

"template": {
  "enabled": true,
}

There are five different types of substitution: param, header, xpath, jsonpath and regex. Each can be inserted into a response body with the following syntax:

${[name]:[expression]}

Param

The param substitution's expression should be the name of a request parameter. The template will be substituted for the value of the specified parameter. If there are multiple values for the specified name the first value found will be used. An example template is:

${param:id}

Header

The header substitution's expression should be the name of a request header. The template will be substituted for the value of the specified header. If there are multiple values for the specified name the first value found will be used. An example template is:

${header:user-agent}

XPath

The xpath substitution's expression should be an XPath expression. The template will be substituted for the result of evaluating the XPath expression against the request body. As such it should only be used when the entire body is valid XML. An example template is:

${xpath:/bookstore/book[1]/title}

JSONPath

The jsonpath substitution's expression should be a JSONPath expression. The template will be substituted for the result of evaluating the JSONPath expression against the request body. As such it should only be used when the entire body is valid JSON. An example template is:

${jsonpath:$['bookstore']['book'][1]['title']}

Regex

The regex substitution's expression should be a regular expression. The template will be substituted for first match found when evaluating the regular expression against the request body. If the expression contains group the first group will be supplied as the result rather than the entire match. An example template is:

${regex:(?:&|^)id=(.*?)(&|$)}

Mock Module

A module that intercepts a request and optionally returns a mock response.

To configure a delay an API is created in the configuration. Each endpoint has an an MBean associated with it under the name Rexy/[API name]/[Endpoint name]/mock. The MBean has an intercept property which is set to false by default. When it is set to true no mock response is returned and the module chain continues, when it is set to true a mock response is written and no further MBeans in the chain will be called.

The MBean has other properties: content type, HTTP status and body. These values dictate what is returned as the mock response.

For example, in the following configuration an MBean would be registered under Rexy/metaweather/location/mock.

"apis": [
  {
    "name": "metaweather",
    "baseUrl": "api/",
    "contentType": "application/json",
    "proxy": "http://www.metaweather.com/api",
    "endpoints": [{
      "name": "location",
      "endpoint": "/location/search/?query={query}",
      "responses": [{
          "name": "Successful",
          "httpStatus": 200,
          "body": {
            "title": "London",
            "location_type": "City",
            "woeid": 44418,
            "latt_long": "51.506321,-0.12714"
          }
        },
        {
          "name": "Invalid",
          "httpStatus": 401,
          "body": {
            "error": "invalid request"
          }
        }
      ]
    }]
  }
]

It is also possible to create presets that can be set to be returned. These can be seen in the above example under the responses array. Each response will have bean an MBean associated with it under Rexy/metaweather/location/preset-[name] where name is the name of the response. If no name is given the index of the response in the array is used instead. The http status, headers and body to return can be specified in the response. The body value can be any unstructured JSON.

The module has two configuration values - interceptOnSet and prettyPrint. If interceptOnSet is set to true, when the set operator is called on an example response, the mock bean will have it's intercept value set to true. If prettyPrint is set to true then mocked responses will be pretty printed.

Delay Module

A module that intercepts a request and optionally adds a delay before the next module in the chain.

To configure a delay an API is created in the configuration. Each endpoint has an an MBean associated with it under the name Rexy/[API name]/[Endpoint name]/delay. The MBean has a single property - delay. If the value of delay is greater then zero then a delay of that number of seconds will be introduced.

For example, in the following configuration an MBean would be registered under Rexy/metaweather/location/delay.

"apis": [
  {
    "name": "metaweather",
    ...
    "endpoints": [
    {
      "name": "location",
      ...
    }
  }
]

Matchers

Matchers are used to see if an MBean should handle a HTTP request. A base matcher is used to match all requests against the endpoint URL and HTTP method. Currently this is a simple regular expression based matcher. In the future other matchers may be supplied to address the limitations of this approach.

Additional matchers can be supplied as part of the matchers JSON. The keys in the matchers object are the name of the matcher and the values are the configuration required by that matcher. If additional matchers are supplied all matchers, including the base matcher, must match for the MBean to process the request.

Xpath Body Matcher

Matches requests with bodies that match a given XPath expression. As such it should be used with requests that contain an XML body. The expression should be supplied as the configuration. For example:

"matchers": {
  "xpath": "//elements/element"
},

Json Path Body Matcher

Matches requests with bodies that match a given JsonPath expression. As such it should be used with requests that contain a JSON body. The expression should be supplied as the configuration. For example:

"matchers": {
  "jsonpath": "$['object']['value']"
},

Not Matcher

Inverts a request matcher to match when a request does not match the enclosed matcher. For example:

"matchers": {
  "not": {
    "header": {
       "Accept": "application/xhtml+xml"
    }
  }
},

This would match a request that doesn't contain the header specified in the request matcher.

Regex Body Matcher

Matches requests with bodies that match a given regular expression. The expression should be supplied as the configuration. For example:

"matchers": {
  "regex": "^error"
},

Regex Request Matcher

The base matcher that is applied to every request. Associates an MBean with a HTTP method and a regular expression pattern that is matched against the requested URL.

Header Matcher

Matches requests that contain a specific header. The header name and value should be supplied as a key-value pair in the configuration. For example:

"matchers": {
  "header": {
     "Accept": "application/xhtml+xml"
  }
},