Request Matching

Overview

API Simulator inspects the HTTP requests it receives by applying matching rules defined in the simlets. Multi-key matching by URI (route), URI path pattern, any URI part, HTTP Method, HTTP Header fields, and values from the body provide for flexible and extensive request matching.

Various operations defined in the SuchThat class exist to perform value matching:

SuchThat.isEqualTo(String)
SuchThat.isEqualToIgnoringCase(String)
SuchThat.startsWith(String)
SuchThat.endsWith(String)
SuchThat.isLike(String regEx)
SuchThat.contains(String)
SuchThat.not(SuchThat)
SuchThat.exists()
SuchThat.isMissing()

Perhaps obvious but just for completeness: isMissing() is equivalent to not(exists()).

Method Matching

Matching requests by the HTTP Method (verb) can be specified in several ways:

httpRequest("GET")
httpRequest().whereMethod(SuchThat.isEqualTo("GET"))

httpRequest(HttpMethod.GET)
httpRequest().whereMethod(HttpMethod.GET)

// These require 'import static com.apisimulator.http.HttpMethod.*;'
httpRequest(GET)
httpRequest().whereMethod(GET)

To not match request’s HTTP Method simply do not specify whereMethod and do not pass HTTP Method as argument to httpRequest().

URI Matching

Here are examples for matching elements of the URI:

// Assuming the URI looks like this:
// https://admin:passW0rd@example.com:8090/api/places/json?radius=5&types=food&types=cafe&checked#ref1

.when(httpRequest()
      .whereUri(contains("api/places/json?")) // match the whole URI
      .whereUriPath(isEqualTo("/api/places/json"))
      .whereUriPathMatchesPattern("/api/{collection}/{format}")
      .whereUriScheme(isEqualToIgnoringCase("https"))
      .whereUriUserInfo(startsWith("admin:"))
      .whereUriUserInfo(endsWith(":passW0rd"))
      .whereUriHost(isEqualToIgnoringCase("example.com"))
      .whereUriPort(isEqualTo("8090"))
      .whereQueryParameter("radius", isEqualTo("5"))
      .whereQueryParameter("types", isEqualTo("food"))
      .whereQueryParameter("types", isEqualTo("cafe")) // multi-value parameter
      .whereQueryParameter("checked", exists())
      .whereQueryParameter("blah", not(exists()))
      .whereUriFragment(isEqualTo("ref1"))
)

Header Matching

The following methods are for matching HTTP Header values:

// Specify standard or custom header name
// Header name matching is case insensitive
.whereHeader(String name, SuchThat)

// Standard HTTP/1.1 and HTTP/1.0 header names in the "usual" mixed casing
// Header name matching is case insensitive
.whereHeader(Http1Header headerName, SuchThat)

// Standard HTTP/2 header names in all small letters
// Header name matching is case insensitive
.whereHeader(Http2Header headerName, SuchThat)

HTTP requests may contain cookies in an HTTP Header field called Cookie. A single header field can have multiple cookies separated by semi-colon ;.

API Simulator supports matching individual cookies so you don’t have to extract them yourself from the Cookie header field.

.whereCookie(String cookieName, SuchThat)

// Examples
.whereCookie("JSESSIONID", exists())
.whereCookie("lang", isEqualTo("en-CA"))

Body Content Matching

Currently there are two flavors of body matching methods: one using any SuchThat operation treating the body as unstructured content, and one that matches elements in structured text.

Unstructured Content

// The method signtature is 'whereBody(SuchThat)'
.whereBody(contains("\"id\":\"5678\""))

JSON

Currently (circa October 2018) there isn’t a standard for JSON similar to what XPath is for XML. There are few proposals and implementations out there for what is dubbed JSONPath, each with some "nuances". API Simulator v1.1+ uses an implementation close to the proposal in Stefan Goessner’s article.

JSONPath provides a way of identifying and navigating to parts of a JSON document. The parts can be individual fields or complex structures.

Below are the syntax elements of JSONPath expressions:

Syntax Element Description

.

A JSONPath expression must start with a dot . character.

@

Denotes the current object/element being processed by a filter (see below about filters).

*

Wildcard.

..

Recursive descent into all children of an object/element. The result is always an array, potentially with some null elements.

.<name>

Dot-notation reference to a child.

['<name>' (, '<name>')]

Bracket-notation reference to one or more children. Bracket-notation is required for fields that contain spaces.

[<number> (, <number>)]

One or more array elements referenced by their index.

[start:end]

Array slice operator.

[?(<filter>)]

Filter expression (see below). Must evaluate to a boolean value.

Filters are expressions used to select array elements. A typical filter would be [?(@.age > 18)] where @ represents the object/element item being processed. More complex filters can be created with logical operators && and ||. String literals must be enclosed by single ' or double quotes " - ([?(@.color == 'blue')] or [?(@.color == "blue")]).

The following table lists the supported filter operators:

Operator Description

==

The left side is equal to the right side (note that the number 1 is not equal to the String '1').

!=

The left side is not equal to the right side.

<

The left side is less than the right side.

<=

The left side is less than or equal to the right side.

>

The left side is greater than the right side.

>=

The left side is greater than or equal to right side.

=~

The left side matches the regular expression on the right.

The following example should help understand how it works. Let’s consider this JSON document:

{
  "product": {
    "id": "1234",
    "name": "The Jumpers",
    "category": [
      "Shoes",
      "Kids"
    ],
    "subCategory": "Basketball",
    "color": "white"
  }
}

Below are examples of matching HTTP body elements if the JSON document was in the HTTP body of a request:

.whereBody(element(".product.id"), isEqualTo("1234"))
.whereBody(element(".product['id']"), isEqualTo("1234"))
.whereBody(element(".['product']['id']"), isEqualTo("1234"))

// The first array element
.whereBody(element(".product.category[0]"), isEqualToIgnoringCase("shoes"))

// The second array element
.whereBody(element(".product.category[1]"), isEqualTo("Kids"))
.whereBody(element(".product[?(@.category[1] == 'Kids')]"), exists())

.whereBody(element(".product.category[2]"), not(exists()))

// Any of the array elements' value is equal to Kids (case-sensitive matching)
.whereBody(element(".product.category[?(@ == 'Kids')]"), exists())

// Any of the array elements' value is equal to shoes (case-insensitive regEx matching)
.whereBody(element(".product.category[?(@ =~ /shoes/i)]"), exists())

// Any of the array elements' value is equal to kids (case-insensitive regEx matching)
.whereBody(element(".product.category[?(@ =~ /kids/i)]"), exists())

.whereBody(element(".product.color"), exists())
.whereBody(element(".product.size"), not(exists()))

XML without Namespaces

Assuming the following XML body:

<ValidateAddress>
  <ValidateAddressInput>
    <Address>
      <Line1>123 Main Street</Line1>
        <City>Anycity</City>
        <StateProvinceCode>ZZ</StateProvinceCode>
        <PostalCode>12345</PostalCode>
        <CountryCode>USA</CountryCode>
    </Address>
  </ValidateAddressInput>
</ValidateAddress>

These can be used to match elements in the XML body:

.whereBody(
   element("ValidateAddress/ValidateAddressInput/Address/Line1"),
   isEqualTo("123 Main Street")
)
.whereBody(
   element("//Address/PostalCode"),
   isEqualTo("12345")
)

XML with Namespaces

Often time XML uses namespaces. This is an example of how to match XML elements when namespaces are in play:

Assuming the following XML body:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
   xmlns:ws="http://ws.v1_0.avs.example.com/">
<s:Header />
<s:Body>
  <ws:ValidateAddress>
    <ValidateAddressInput>
      <Address>
        <Line1>123 Main Street</Line1>
        <City>Anycity</City>
        <StateProvinceCode>ZZ</StateProvinceCode>
        <PostalCode>12345</PostalCode>
        <CountryCode>USA</CountryCode>
      </Address>
    </ValidateAddressInput>
  </ws:ValidateAddress>
</s:Body>
</s:Envelope>

These can be used to match elements in the XML body:

.whereBody(
   element("s:Envelope/s:Body/ws:ValidateAddress/ValidateAddressInput/Address/Line1",
      namespace("s", "http://schemas.xmlsoap.org/soap/envelope/"),
      namespace("ws", "http://ws.v1_0.avs.example.com/")
   ),
   isEqualTo("123 Main Street")
)

// The namespace prefixes in the path (ns1, ns2) do not have to match the
// prefixes in the request (s, ws) as longs as they refer to the same URI
.whereBody(
   element("ns1:Envelope/ns1:Body/ns2:ValidateAddress/ValidateAddressInput/Address/PostalCode",
      namespace("ns1", "http://schemas.xmlsoap.org/soap/envelope/"),
      namespace("ns2", "http://ws.v1_0.avs.example.com/")
   ),
   isEqualTo("12345")
)

Matching Rank

Sometimes, simlet matching has to be applied in a certain order. This is where matching rank comes into play. Here is an example:

apiSim.add(simlet("access-forbidden").withRank(0)
   .when(httpRequest(GET)
      .whereUriPath(isLike("/admin/.*"))
      .whereHeader(AUTHORIZATION, not(exists()))
   )
   .then(httpResponse(FORBIDDEN))
);

apiSim.add(simlet("get-health").withRank(100)
   .when(httpRequest(GET)
      .whereUriPath(isEqualTo("/admin/healthz"))
   )
   .then(httpResponse(OK))
);

API Simulator tries to match simlets with higher matching ranks before simlets with lower ranks; simlets with the same matching rank are tried in no particular order.

The default matching rank is 0 (zero).


We would love to hear your feedback! Shoot us a quick email to [feedback at APISimulator.com] to let us know what you think.

Happy API Simulating!