bbyars/mountebank

View on GitHub
src/views/docs/api/xpath.ejs

Summary

Maintainability
Test Coverage
<%
title = 'xpath'
description = 'Using XPath in mountebank predicates'
%>

<%- include('../../_header') -%>

<h1>Using XPath</h1>

<p>It is common for mountebank to see XML documents in his line of business, and he suspects
the same may be true for you.  With the goal of making XML easier to work with, mountebank
accepts an <code>xpath</code> predicate parameter.  This parameter narrows the scope of the predicate
value to a value matched by the xpath selector, much like the <a href='/docs/api/predicates'>
<code>except</code> parameter</a>.  mountebank suggests avoiding using this parameter on fields
that are not XML documents.  He also counsels avoiding the temptation to use it with binary protocols.</p>

<p>mountebank uses the following fields with xpath:</p>

<table>
  <tr>
    <th>Field</th>
    <th>Required?</th>
    <th>Description</th>
  </tr>
  <tr>
    <td><code>selector</code></td>
    <td>Yes</td>
    <td>The XPath selector</td>
  </tr>
  <tr>
    <td><code>ns</code></td>
    <td>No</td>
    <td>The XPath namespace map, aliasing a prefix to a URL, which allows you to use the prefix in
    the <code>selector</code>.</td>
  </tr>
</table>

<p>You can send the parameter without namespaces...</p>

<pre><code>{
  "equals": { "body": "Harry Potter" },
  "xpath": { "selector": "//title" }
}</code></pre>

<p>...or with namespaces:</p>

<pre><code>{
  "equals": { "body": "Harry Potter" },
  "xpath": {
    "selector": "//a:title"
    "ns": {
      "a": "http://example.com/book"
    }
  }
}</code></pre>

<p>The xpath parameter follows the same semantics as those obeyed for multi-valued keys described on the
<a href='/docs/api/predicates'>main predicates page</a>, like those observed when a querystring has
the same key multiple times.  Since the xpath selector can potentially match multiple nodes in the XML
document, this is an important point to make.  <code>deepEquals</code> will require all the values to match
(although the order isn't important).  All other predicates will match if any node value matches.  The examples below
explore these semantics.</p>

<h2>Examples</h2>

<p>Let's create an HTTP imposter with multiple stubs.  We'll use redundant predicates simply to
demonstrate various ways to use the <code>xpath</code> parameter.  Where applicable, a
<code>comment</code> field is added to the JSON to explain what's going on.  Like all superfluous
fields, mountebank will ignore it.</p>

<testScenario name='xpath'>
    <step type='http'>
<pre><code>POST /imposters HTTP/1.1
Host: localhost:<%= port %>
Accept: application/json
Content-Type: application/json

{
  "port": 4545,
  "protocol": "http",
  "stubs": [<strong class='highlight1'>
    {
      "responses": [{ "is": { "body": "Basic xpath usage" } }],
      "predicates": [
        {
          "equals": { "body": "Harry Potter" },
          "xpath": { "selector": "//title" },
          "caseSensitive": true,
          "comment": "case sensitivity applies to the selector as well as the value"
        },
        {
          "equals": { "body": "POTTER" },
          "xpath": { "selector": "//TITLE" },
          "except": "HARRY ",
          "comment": "The except regular expression is removed from the value before matching"
        },
        {
          "matches": { "body": "^Harry" },
          "xpath": { "selector": "//title" }
        },
        {
          "exists": { "body": true },
          "xpath": { "selector": "//title" },
          "comment": "body must exist (e.g. be empty) AND the xpath must match at least one node"
        },
        {
          "exists": { "body": false },
          "xpath": { "selector": "//title/@first" },
          "comment": "body must not exist (e.g. be empty) OR the xpath must not match any nodes"
        },
        {
          "equals": { "body": 3 },
          "xpath": { "selector": "count(//title)" },
          "comment": "number results from xpath selectors are fine..."
        },
        {
          "equals": { "body": true },
          "xpath": { "selector": "boolean(//title)" },
          "comment": "...as are boolean results"
        }
      ]
    }</strong>,<strong class='highlight2'>
    {
      "responses": [{ "is": { "body": "xpath with namespaces" } }],
      "predicates": [
        {
          "contains": { "body": "dragons" },
          "xpath": {
            "selector": "//isbn:summary",
            "ns": {
              "isbn": "http://schemas.isbn.org/ns/1999/basic.dtd"
            }
          }
        },
        {
          "contains": { "body": "dragons" },
          "xpath": {
            "selector": "//*[local-name(.)='summary' and namespace-uri(.)='http://schemas.isbn.org/ns/1999/basic.dtd']"
          },
          "comment": "You can use XPath without namespace aliases if you're so inclined"
        },
        {
          "exists": { "body": false },
          "xpath": { "selector": "//title/@first" },
          "comment": "This lets us test out the third stub without running into a conflict"
        }
      ]
    }</strong>,<strong class='highlight3'>
    {
      "responses": [{ "is": { "body": "xpath with attributes and the quirks of deepEquals" } }],
      "predicates": [
        {
          "deepEquals": { "body": "3" },
          "xpath": { "selector": "//books/@count" },
          "comment": "deepEquals expects a string when there is only one match"
        },
        {
          "deepEquals": { "body": ["false", "false", "true"] },
          "xpath": { "selector": "//title/@first" },
          "comment": "Use an array for deepEquals when there are multiple matches (same for a repeating querystring key)"
        }
      ]
    }</strong>
  ]
}</code></pre>
    </step>

<p>The first predicate uses an xpath selector to navigate to all <code>title</code> elements
within the XML document within the <code>body</code> field.  If <code>body</code> turns out not
to be an XML document, none of them will match.  The <code>caseSensitive</code> parameter applies
both to the predicate value in <code>body</code> and to the <code>selector</code> field in
<code>xpath</code>.  The <code>exists</code> predicate behavior is identical to its behavior with
other fields, except now the xpath selector must also match if <code>exists</code> is <code>true</code>,
or not match if <code>exists</code> is false.<p>

<p>Note that in the example below, there are multiple nodes matching the xpath selector.  With all
predicates except <code>deepEquals</code>, it's sufficient that <i>any</i> of them match.</p>

    <step type='http'>
<pre><code>POST / HTTP/1.1
Host: localhost:4545

&lt;books xmlns:isbn=&quot;http://schemas.isbn.org/ns/1999/basic.dtd&quot;&gt;
  &lt;book&gt;
    &lt;title&gt;Game of Thrones&lt;/title&gt;
    &lt;isbn:summary&gt;Dragons and political intrigue&lt;/isbn:summary&gt;
  &lt;/book&gt;
  &lt;book&gt;
    <strong class='highlight1'>&lt;title&gt;Harry Potter&lt;/title&gt;</strong>
    &lt;isbn:summary&gt;Dragons and a boy wizard&lt;/isbn:summary&gt;
  &lt;/book&gt;
  &lt;book&gt;
    &lt;title&gt;The Hobbit&lt;/title&gt;
    &lt;isbn:summary&gt;A dragon and short people&lt;/isbn:summary&gt;
  &lt;/book&gt;
&lt;/books&gt;</code></pre>

        <assertResponse>
<pre><code>HTTP/1.1 200 OK
Connection: close
Date: <volatile>Thu, 09 Jan 2014 02:30:31 GMT</volatile>
Transfer-Encoding: chunked

<strong class='highlight1'>Basic xpath usage</strong></code></pre>
        </assertResponse>
    </step>

<p>The XML document above also would match the second stub's predicates, but mountebank
always uses the first stub that matches.  To test the second stub, we'll namespace the
<code>title</code> nodes to get past the first stub.</p>

    <step type='http'>
<pre><code>POST / HTTP/1.1
Host: localhost:4545

&lt;books xmlns:isbn=&quot;http://schemas.isbn.org/ns/1999/basic.dtd&quot;&gt;
  &lt;book&gt;
    &lt;isbn:title&gt;Game of Thrones&lt;/isbn:title&gt;
    <strong class='highlight2'>&lt;isbn:summary&gt;Dragons and political intrigue&lt;/isbn:summary&gt;</strong>
  &lt;/book&gt;
  &lt;book&gt;
    &lt;isbn:title&gt;Harry Potter&lt;/isbn:title&gt;
    <strong class='highlight2'>&lt;isbn:summary&gt;Dragons and a boy wizard&lt;/isbn:summary&gt;</strong>
  &lt;/book&gt;
  &lt;book&gt;
    &lt;isbn:title&gt;The Hobbit&lt;/isbn:title&gt;
    &lt;isbn:summary&gt;A dragon and short people&lt;/isbn:summary&gt;
  &lt;/book&gt;
&lt;/books&gt;</code></pre>

        <assertResponse>
<pre><code>HTTP/1.1 200 OK
Connection: close
Date: <volatile>Thu, 09 Jan 2014 02:30:31 GMT</volatile>
Transfer-Encoding: chunked

<strong class='highlight2'>xpath with namespaces</strong></code></pre>
        </assertResponse>
    </step>

<p>The third stub will only match on a <code>first</code> attribute, and expects one
to be set to true and two to be set to false.  In the interests of providing an unbiased
presentation, we'll simply denote which book was first published with this attribute.</p>

    <step type='http'>
<pre><code>POST / HTTP/1.1
Host: localhost:4545

&lt;books count=&quot;3&quot; xmlns:isbn=&quot;http://schemas.isbn.org/ns/1999/basic.dtd&quot;&gt;
  &lt;book&gt;
    &lt;title <strong class='highlight3'>first=&quot;false&quot;</strong>&gt;Game of Thrones&lt;/title&gt;
    &lt;isbn:summary&gt;Dragons and political intrigue&lt;/isbn:summary&gt;
  &lt;/book&gt;
  &lt;book&gt;
    &lt;title <strong class='highlight3'>first=&quot;false&quot;</strong>&gt;Harry Potter&lt;/title&gt;
    &lt;isbn:summary&gt;Dragons and a boy wizard&lt;/isbn:summary&gt;
  &lt;/book&gt;
  &lt;book&gt;
    &lt;title <strong class='highlight3'>first=&quot;true&quot;</strong>&gt;The Hobbit&lt;/title&gt;
    &lt;isbn:summary&gt;A dragon and short people&lt;/isbn:summary&gt;
  &lt;/book&gt;
&lt;/books&gt;</code></pre>

        <assertResponse>
<pre><code>HTTP/1.1 200 OK
Connection: close
Date: <volatile>Thu, 09 Jan 2014 02:30:31 GMT</volatile>
Transfer-Encoding: chunked

<strong class='highlight3'>xpath with attributes and the quirks of deepEquals</strong></code></pre>
        </assertResponse>
    </step>

    <step type='http'>
<code class='hidden'>DELETE /imposters/4545 HTTP/1.1
Host: localhost:<%= port %>
Accept: application/json</code>
    </step>
</testScenario>

<%- include('../../_footer') -%>