@mbtest/mountebank 2.9.2-beta.9050
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/bin/mb +136 -0
- package/package.json +71 -0
- package/releases.json +52 -0
- package/src/cli/api.js +112 -0
- package/src/cli/cli.js +420 -0
- package/src/controllers/configController.js +64 -0
- package/src/controllers/feedController.js +115 -0
- package/src/controllers/homeController.js +58 -0
- package/src/controllers/imposterController.js +328 -0
- package/src/controllers/impostersController.js +215 -0
- package/src/controllers/logsController.js +52 -0
- package/src/models/behaviors.js +553 -0
- package/src/models/behaviorsValidator.js +186 -0
- package/src/models/compatibility.js +133 -0
- package/src/models/dryRunValidator.js +261 -0
- package/src/models/filesystemBackedImpostersRepository.js +908 -0
- package/src/models/http/baseHttpServer.js +207 -0
- package/src/models/http/headersMap.js +87 -0
- package/src/models/http/httpProxy.js +230 -0
- package/src/models/http/httpRequest.js +82 -0
- package/src/models/http/httpServer.js +18 -0
- package/src/models/http/index.js +18 -0
- package/src/models/https/cert/mb-cert.pem +20 -0
- package/src/models/https/cert/mb-csr.pem +16 -0
- package/src/models/https/cert/mb-key.pem +27 -0
- package/src/models/https/httpsServer.js +42 -0
- package/src/models/https/index.js +18 -0
- package/src/models/imposter.js +243 -0
- package/src/models/imposterPrinter.js +120 -0
- package/src/models/impostersRepository.js +49 -0
- package/src/models/inMemoryImpostersRepository.js +418 -0
- package/src/models/jsonpath.js +44 -0
- package/src/models/mbConnection.js +107 -0
- package/src/models/predicates.js +438 -0
- package/src/models/protocols.js +242 -0
- package/src/models/responseResolver.js +398 -0
- package/src/models/smtp/index.js +16 -0
- package/src/models/smtp/smtpRequest.js +60 -0
- package/src/models/smtp/smtpServer.js +109 -0
- package/src/models/tcp/index.js +18 -0
- package/src/models/tcp/tcpProxy.js +110 -0
- package/src/models/tcp/tcpRequest.js +23 -0
- package/src/models/tcp/tcpServer.js +156 -0
- package/src/models/tcp/tcpValidator.js +19 -0
- package/src/models/xpath.js +95 -0
- package/src/mountebank.js +245 -0
- package/src/public/images/arrow_down.png +0 -0
- package/src/public/images/arrow_up.png +0 -0
- package/src/public/images/book.jpg +0 -0
- package/src/public/images/dataflow.png +0 -0
- package/src/public/images/favicon.ico +0 -0
- package/src/public/images/forkme_right_orange_ff7600.png +0 -0
- package/src/public/images/mountebank.png +0 -0
- package/src/public/images/overview.gif +0 -0
- package/src/public/images/quote.png +0 -0
- package/src/public/images/tw-logo.png +0 -0
- package/src/public/scripts/jquery/jquery-3.6.1.min.js +2 -0
- package/src/public/scripts/urlHashHandler.js +31 -0
- package/src/public/stylesheets/application.css +424 -0
- package/src/public/stylesheets/ie.css +14 -0
- package/src/public/stylesheets/imposters.css +121 -0
- package/src/public/stylesheets/jqueryui/1.10.4/themes/smoothness/jquery-ui.css +1178 -0
- package/src/util/combinators.js +68 -0
- package/src/util/date.js +51 -0
- package/src/util/errors.js +55 -0
- package/src/util/helpers.js +131 -0
- package/src/util/inherit.js +28 -0
- package/src/util/ip.js +54 -0
- package/src/util/logger.js +83 -0
- package/src/util/middleware.js +256 -0
- package/src/util/scopedLogger.js +47 -0
- package/src/views/_footer.ejs +20 -0
- package/src/views/_header.ejs +113 -0
- package/src/views/_imposter.ejs +8 -0
- package/src/views/config.ejs +71 -0
- package/src/views/docs/api/behaviors/copy.ejs +427 -0
- package/src/views/docs/api/behaviors/decorate.ejs +182 -0
- package/src/views/docs/api/behaviors/lookup.ejs +220 -0
- package/src/views/docs/api/behaviors/shellTransform.ejs +153 -0
- package/src/views/docs/api/behaviors/wait.ejs +121 -0
- package/src/views/docs/api/behaviors.ejs +141 -0
- package/src/views/docs/api/contracts/addStub-description.ejs +10 -0
- package/src/views/docs/api/contracts/addStub.ejs +10 -0
- package/src/views/docs/api/contracts/config-description.ejs +32 -0
- package/src/views/docs/api/contracts/config.ejs +23 -0
- package/src/views/docs/api/contracts/home-description.ejs +18 -0
- package/src/views/docs/api/contracts/home.ejs +13 -0
- package/src/views/docs/api/contracts/imposter-description.ejs +439 -0
- package/src/views/docs/api/contracts/imposter.ejs +182 -0
- package/src/views/docs/api/contracts/imposters-description.ejs +13 -0
- package/src/views/docs/api/contracts/imposters.ejs +13 -0
- package/src/views/docs/api/contracts/logs-description.ejs +3 -0
- package/src/views/docs/api/contracts/logs.ejs +14 -0
- package/src/views/docs/api/contracts/stub-description.ejs +4 -0
- package/src/views/docs/api/contracts/stub.ejs +7 -0
- package/src/views/docs/api/contracts/stubs-description.ejs +4 -0
- package/src/views/docs/api/contracts/stubs.ejs +11 -0
- package/src/views/docs/api/contracts.ejs +133 -0
- package/src/views/docs/api/errors.ejs +64 -0
- package/src/views/docs/api/fault/connectionReset.ejs +31 -0
- package/src/views/docs/api/fault/randomDataThenClose.ejs +31 -0
- package/src/views/docs/api/faults.ejs +57 -0
- package/src/views/docs/api/injection.ejs +426 -0
- package/src/views/docs/api/json.ejs +205 -0
- package/src/views/docs/api/jsonpath.ejs +210 -0
- package/src/views/docs/api/mocks.ejs +130 -0
- package/src/views/docs/api/overview.ejs +968 -0
- package/src/views/docs/api/predicates/and.ejs +62 -0
- package/src/views/docs/api/predicates/contains.ejs +64 -0
- package/src/views/docs/api/predicates/deepEquals.ejs +114 -0
- package/src/views/docs/api/predicates/endsWith.ejs +66 -0
- package/src/views/docs/api/predicates/equals.ejs +125 -0
- package/src/views/docs/api/predicates/exists.ejs +118 -0
- package/src/views/docs/api/predicates/inject.ejs +67 -0
- package/src/views/docs/api/predicates/matches.ejs +66 -0
- package/src/views/docs/api/predicates/not.ejs +52 -0
- package/src/views/docs/api/predicates/or.ejs +79 -0
- package/src/views/docs/api/predicates/startsWith.ejs +62 -0
- package/src/views/docs/api/predicates.ejs +382 -0
- package/src/views/docs/api/proxies.ejs +191 -0
- package/src/views/docs/api/proxy/addDecorateBehavior.ejs +115 -0
- package/src/views/docs/api/proxy/addWaitBehavior.ejs +96 -0
- package/src/views/docs/api/proxy/injectHeaders.ejs +91 -0
- package/src/views/docs/api/proxy/predicateGenerators.ejs +600 -0
- package/src/views/docs/api/proxy/proxyModes.ejs +495 -0
- package/src/views/docs/api/stubs.ejs +391 -0
- package/src/views/docs/api/xpath.ejs +281 -0
- package/src/views/docs/cli/configFiles.ejs +133 -0
- package/src/views/docs/cli/customFormatters.ejs +53 -0
- package/src/views/docs/cli/help.ejs +6 -0
- package/src/views/docs/cli/replay.ejs +42 -0
- package/src/views/docs/cli/restart.ejs +10 -0
- package/src/views/docs/cli/save.ejs +68 -0
- package/src/views/docs/cli/start.ejs +234 -0
- package/src/views/docs/cli/stop.ejs +32 -0
- package/src/views/docs/commandLine.ejs +93 -0
- package/src/views/docs/communityExtensions.ejs +233 -0
- package/src/views/docs/gettingStarted.ejs +146 -0
- package/src/views/docs/mentalModel.ejs +51 -0
- package/src/views/docs/protocols/custom.ejs +231 -0
- package/src/views/docs/protocols/http.ejs +238 -0
- package/src/views/docs/protocols/https.ejs +246 -0
- package/src/views/docs/protocols/smtp.ejs +142 -0
- package/src/views/docs/protocols/tcp.ejs +431 -0
- package/src/views/docs/security.ejs +38 -0
- package/src/views/faqs.ejs +65 -0
- package/src/views/feed.ejs +33 -0
- package/src/views/imposter.ejs +22 -0
- package/src/views/imposters.ejs +33 -0
- package/src/views/index.ejs +89 -0
- package/src/views/license.ejs +30 -0
- package/src/views/logs.ejs +77 -0
- package/src/views/releases/v1.1.0.ejs +55 -0
- package/src/views/releases/v1.1.36.ejs +84 -0
- package/src/views/releases/v1.1.72.ejs +92 -0
- package/src/views/releases/v1.10.0.ejs +108 -0
- package/src/views/releases/v1.11.0.ejs +109 -0
- package/src/views/releases/v1.12.0.ejs +96 -0
- package/src/views/releases/v1.13.0.ejs +118 -0
- package/src/views/releases/v1.14.0.ejs +107 -0
- package/src/views/releases/v1.14.1.ejs +94 -0
- package/src/views/releases/v1.15.0.ejs +113 -0
- package/src/views/releases/v1.16.0.ejs +104 -0
- package/src/views/releases/v1.2.0.ejs +78 -0
- package/src/views/releases/v1.2.103.ejs +86 -0
- package/src/views/releases/v1.2.122.ejs +86 -0
- package/src/views/releases/v1.2.30.ejs +84 -0
- package/src/views/releases/v1.2.45.ejs +84 -0
- package/src/views/releases/v1.2.56.ejs +79 -0
- package/src/views/releases/v1.3.0.ejs +86 -0
- package/src/views/releases/v1.3.1.ejs +100 -0
- package/src/views/releases/v1.4.0.ejs +96 -0
- package/src/views/releases/v1.4.1.ejs +103 -0
- package/src/views/releases/v1.4.2.ejs +100 -0
- package/src/views/releases/v1.4.3.ejs +113 -0
- package/src/views/releases/v1.5.0.ejs +104 -0
- package/src/views/releases/v1.5.1.ejs +91 -0
- package/src/views/releases/v1.6.0.ejs +109 -0
- package/src/views/releases/v1.7.0.ejs +113 -0
- package/src/views/releases/v1.7.1.ejs +90 -0
- package/src/views/releases/v1.7.2.ejs +96 -0
- package/src/views/releases/v1.8.0.ejs +121 -0
- package/src/views/releases/v1.9.0.ejs +111 -0
- package/src/views/releases/v2.0.0.ejs +159 -0
- package/src/views/releases/v2.1.0.ejs +121 -0
- package/src/views/releases/v2.1.1.ejs +106 -0
- package/src/views/releases/v2.1.2.ejs +84 -0
- package/src/views/releases/v2.2.0.ejs +115 -0
- package/src/views/releases/v2.2.1.ejs +102 -0
- package/src/views/releases/v2.3.0.ejs +121 -0
- package/src/views/releases/v2.3.1.ejs +100 -0
- package/src/views/releases/v2.3.2.ejs +102 -0
- package/src/views/releases/v2.3.3.ejs +97 -0
- package/src/views/releases/v2.4.0.ejs +114 -0
- package/src/views/releases/v2.5.0.ejs +51 -0
- package/src/views/releases/v2.6.0.ejs +35 -0
- package/src/views/releases/v2.7.0.ejs +32 -0
- package/src/views/releases/v2.8.0.ejs +36 -0
- package/src/views/releases/v2.8.1.ejs +7 -0
- package/src/views/releases/v2.8.2.ejs +26 -0
- package/src/views/releases/v2.9.0.ejs +32 -0
- package/src/views/releases/v2.9.1.ejs +10 -0
- package/src/views/releases.ejs +26 -0
- package/src/views/sitemap.ejs +36 -0
- package/src/views/support.ejs +14 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
<%
|
|
2
|
+
title = 'injection'
|
|
3
|
+
description = 'Extending mountebank with custom JavaScript scripting'
|
|
4
|
+
%>
|
|
5
|
+
|
|
6
|
+
<%- include('../../_header') -%>
|
|
7
|
+
|
|
8
|
+
<h1>Injection</h1>
|
|
9
|
+
|
|
10
|
+
<p>mountebank allows JavaScript injection for predicates and response types for situations
|
|
11
|
+
where the built-in ones are not sufficient</p>
|
|
12
|
+
|
|
13
|
+
<p class='warning-icon'>Injection only works if <code>mb</code> is
|
|
14
|
+
run with the <code>--allowInjection</code> flag.</p>
|
|
15
|
+
|
|
16
|
+
<p class='warning-icon'>If you have injection enabled, you should set either the
|
|
17
|
+
<code>--localOnly</code> or <code>--ipWhitelist</code> flags as well. See the
|
|
18
|
+
<a href='/docs/security'>security page</a> for more details.</p>
|
|
19
|
+
|
|
20
|
+
<p>Though mountebank has gone to some trouble to make injections as hassle-free as
|
|
21
|
+
possible, there are a couple of things to be aware of. First, when validating stub creation,
|
|
22
|
+
mountebank does not validate any injected functions. Second, all injections have full access
|
|
23
|
+
to a node.js runtime environment, including the ability to <code>require</code> in different
|
|
24
|
+
modules. Of course, such power comes with its own challenges, which leads us to our third point: with
|
|
25
|
+
injections, you can crash mountebank. mountebank has made a noble and valiant effort to be robust
|
|
26
|
+
in the face of errors, but he is not as clever as you are. If you find yourself coding injections
|
|
27
|
+
frequently because of missing functionality that you believe would be generally useful,
|
|
28
|
+
mountebank humbly requests you to add a
|
|
29
|
+
<a href='https://github.com/mountebank-testing/mountebank/issues'>feature request.</a></p>
|
|
30
|
+
|
|
31
|
+
<p>There are five options for injecting JavaScript into mountebank. The first two,
|
|
32
|
+
predicate injection and response injection, are described below. The third and fourth,
|
|
33
|
+
post-processing responses through the <code>decorate</code> behavior or through the
|
|
34
|
+
<code>wait</code> behavior, are described
|
|
35
|
+
on the <a href='/docs/api/behaviors'>behaviors</a> page. Finally, determining the end
|
|
36
|
+
of a TCP request is described on the <a href='/docs/protocols/tcp'>tcp</a> protocol page.</p>
|
|
37
|
+
|
|
38
|
+
<h2 id='predicate-injection'>Predicate injection</h2>
|
|
39
|
+
|
|
40
|
+
<p>Predicate injection allows you to pass a JavaScript function to decide whether the
|
|
41
|
+
stub should match or not. Unlike other predicates, predicate injections bind to the entire
|
|
42
|
+
<code>request</code> object, which allows you to base the result on multiple fields. They
|
|
43
|
+
can return truthy or falsy to decide whether the predicate matches. mountebank doesn't know
|
|
44
|
+
what that means, so he always returns <code>true</code> or <code>false</code>.</p>
|
|
45
|
+
|
|
46
|
+
<p>The injected function should take a single parameter, which will contain the following fields:</p>
|
|
47
|
+
|
|
48
|
+
<table>
|
|
49
|
+
<tr>
|
|
50
|
+
<th>Field</th>
|
|
51
|
+
<th>Description</th>
|
|
52
|
+
</tr>
|
|
53
|
+
<tr>
|
|
54
|
+
<td><code>request</code></td>
|
|
55
|
+
<td>The entire request object, containing all request fields</td>
|
|
56
|
+
</tr>
|
|
57
|
+
<tr>
|
|
58
|
+
<td><code>state</code></td>
|
|
59
|
+
<td>An initially empty object that will be shared between predicate
|
|
60
|
+
and response injection functions as well as the <code>decorate</code> behavior.
|
|
61
|
+
You can use it to capture and mutate shared state.</td>
|
|
62
|
+
</tr>
|
|
63
|
+
<tr>
|
|
64
|
+
<td><code>logger</code></td>
|
|
65
|
+
<td>A logger object with <code>debug</code>, <code>info</code>, <code>warn</code>,
|
|
66
|
+
and <code>error</code> functions to write to the mountebank logs.</td>
|
|
67
|
+
</tr>
|
|
68
|
+
</table>
|
|
69
|
+
|
|
70
|
+
<p>The following example uses injection to satisfy a complicated multi-field predicate for HTTP:</p>
|
|
71
|
+
|
|
72
|
+
<testScenario name='predicate injection'>
|
|
73
|
+
<step type='http'>
|
|
74
|
+
<pre><code>POST /imposters HTTP/1.1
|
|
75
|
+
Host: localhost:<%= port %>
|
|
76
|
+
Accept: application/json
|
|
77
|
+
Content-Type: application/json
|
|
78
|
+
|
|
79
|
+
{
|
|
80
|
+
"port": 4545,
|
|
81
|
+
"protocol": "http",
|
|
82
|
+
"stubs": [
|
|
83
|
+
{
|
|
84
|
+
"responses": [{ "is": { "statusCode": 400 } }],
|
|
85
|
+
"predicates": [{
|
|
86
|
+
"inject": "function (config) {\n\n function hasXMLProlog () {\n return config.request.body.indexOf('<?xml') === 0;\n }\n\n if (config.request.headers['Content-Type'] === 'application/xml') {\n return !hasXMLProlog();\n }\n else {\n return hasXMLProlog();\n }\n}"
|
|
87
|
+
}]
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}</code></pre>
|
|
91
|
+
</step>
|
|
92
|
+
|
|
93
|
+
<p>Injections certainly don't help the readability of the JSON. If you're loading imposters through a config file,
|
|
94
|
+
look at the <code>stringify</code> function supported in file templating with the <a href='/docs/commandLine#config-file'>
|
|
95
|
+
<code>--configfile</code></a> command line option to support storing the functions in a readable format.
|
|
96
|
+
Let's look at our injected function appropriately formatted:</p>
|
|
97
|
+
|
|
98
|
+
<pre><code>function (config) {
|
|
99
|
+
|
|
100
|
+
function hasXMLProlog () {
|
|
101
|
+
return config.request.body.indexOf('<?xml') === 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (config.request.headers['Content-Type'] === 'application/xml') {
|
|
105
|
+
return !hasXMLProlog();
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
return hasXMLProlog();
|
|
109
|
+
}
|
|
110
|
+
}</code></pre>
|
|
111
|
+
|
|
112
|
+
<p>It matches on XML content missing a prolog, or a prolog added to non-XML content. First we'll
|
|
113
|
+
send a request that matches the first condition:</p>
|
|
114
|
+
|
|
115
|
+
<step type='http'>
|
|
116
|
+
<pre><code>POST /test HTTP/1.1
|
|
117
|
+
Host: localhost:4545
|
|
118
|
+
Content-Type: <strong class='highlight1'>application/xml</strong>
|
|
119
|
+
|
|
120
|
+
<customer>
|
|
121
|
+
<name>Test</name>
|
|
122
|
+
</customer></code></pre>
|
|
123
|
+
|
|
124
|
+
<assertResponse>
|
|
125
|
+
<pre><code>HTTP/1.1 <strong class='highlight1'>400</strong> Bad Request
|
|
126
|
+
Connection: close
|
|
127
|
+
Date: <volatile>Thu, 09 Jan 2014 02:30:31 GMT</volatile>
|
|
128
|
+
Transfer-Encoding: chunked</code></pre>
|
|
129
|
+
</assertResponse>
|
|
130
|
+
</step>
|
|
131
|
+
|
|
132
|
+
<p>Now we'll match on the second condition:</p>
|
|
133
|
+
|
|
134
|
+
<step type='http'>
|
|
135
|
+
<pre><code>POST /test HTTP/1.1
|
|
136
|
+
Host: localhost:4545
|
|
137
|
+
Content-Type: <strong class='highlight1'>application/json</strong>
|
|
138
|
+
|
|
139
|
+
<strong class='highlight1'><?xml></strong>
|
|
140
|
+
<customer>
|
|
141
|
+
<name>Test</name>
|
|
142
|
+
</customer></code></pre>
|
|
143
|
+
|
|
144
|
+
<assertResponse>
|
|
145
|
+
<pre><code>HTTP/1.1 <strong class='highlight1'>400</strong> Bad Request
|
|
146
|
+
Connection: close
|
|
147
|
+
Date: <volatile>Thu, 09 Jan 2014 02:30:31 GMT</volatile>
|
|
148
|
+
Transfer-Encoding: chunked</code></pre>
|
|
149
|
+
</assertResponse>
|
|
150
|
+
</step>
|
|
151
|
+
|
|
152
|
+
<step type='http'>
|
|
153
|
+
<code class='hidden'>DELETE /imposters/4545 HTTP/1.1
|
|
154
|
+
Host: localhost:<%= port %>
|
|
155
|
+
Accept: application/json</code>
|
|
156
|
+
</step>
|
|
157
|
+
</testScenario>
|
|
158
|
+
|
|
159
|
+
<h2 id='response-injection'>Response injection</h2>
|
|
160
|
+
|
|
161
|
+
<p>Response injection allows you to dynamically construct the response based on the
|
|
162
|
+
request and previous requests. The response object should match the structure defined in
|
|
163
|
+
the appropriate protocol page linked to on the sidebar. mountebank will use the default
|
|
164
|
+
value documented on the protocol page for any response fields you leave empty. The predicate
|
|
165
|
+
object takes a single parameter which will contain the following fields:</p>
|
|
166
|
+
|
|
167
|
+
<table>
|
|
168
|
+
<tr>
|
|
169
|
+
<th>Field</th>
|
|
170
|
+
<th>Description</th>
|
|
171
|
+
</tr>
|
|
172
|
+
<tr>
|
|
173
|
+
<td><code>request</code></td>
|
|
174
|
+
<td>The entire protocol-specific request object</td>
|
|
175
|
+
</tr>
|
|
176
|
+
<tr>
|
|
177
|
+
<td><code>state</code></td>
|
|
178
|
+
<td>mountebank maintains this variable for you
|
|
179
|
+
that will be empty on the first injection. The same variable will be passed into
|
|
180
|
+
every <code>inject</code> function, allowing you to remember state between calls,
|
|
181
|
+
even between stubs. It will be shared with predicate <code>inject</code> functions
|
|
182
|
+
and <code>decorate</code> behaviors.</td>
|
|
183
|
+
</tr>
|
|
184
|
+
<tr>
|
|
185
|
+
<td><code>callback</code></td>
|
|
186
|
+
<td>Asynchronous injection is supported through this parameter.
|
|
187
|
+
If your function returns a value, the imposter will consider it to be synchronous. If
|
|
188
|
+
it does not return a value, it must invoke the <code>callback</code> parameter with the
|
|
189
|
+
response object. The following injection takes advantage of the node.js environment
|
|
190
|
+
it will run in to proxy most of the request to localhost:5555. It records the request
|
|
191
|
+
and response to the <code>state</code> variable, and only proxies if certain request
|
|
192
|
+
parameters have not already been sent.
|
|
193
|
+
</td>
|
|
194
|
+
</tr>
|
|
195
|
+
<tr>
|
|
196
|
+
<td><code>logger</code></td>
|
|
197
|
+
<td>A logger object with <code>debug</code>, <code>info</code>, <code>warn</code>,
|
|
198
|
+
and <code>error</code> functions to write to the mountebank logs.</td>
|
|
199
|
+
</tr>
|
|
200
|
+
</table>
|
|
201
|
+
|
|
202
|
+
<p>The example below will use two imposters, an origin server and a server that proxies
|
|
203
|
+
to the origin server. It would be better implemented using
|
|
204
|
+
<a href='/docs/api/proxies'>a proxy response type</a>, but has sufficient complexity to
|
|
205
|
+
demonstrate many nuances of injection.</p>
|
|
206
|
+
|
|
207
|
+
<p>First we'll create the origin server at port 5555. To help us keep track of the
|
|
208
|
+
two imposters, we'll set the <code>name</code> parameter, which will show up in the logs.</p>
|
|
209
|
+
|
|
210
|
+
<testScenario name='predicate response'>
|
|
211
|
+
<step type='http'>
|
|
212
|
+
<pre><code>POST /imposters HTTP/1.1
|
|
213
|
+
Host: localhost:<%= port %>
|
|
214
|
+
Accept: application/json
|
|
215
|
+
Content-Type: application/json
|
|
216
|
+
|
|
217
|
+
{
|
|
218
|
+
"port": 5555,
|
|
219
|
+
"protocol": "http",
|
|
220
|
+
"name": "origin",
|
|
221
|
+
"stubs": [{
|
|
222
|
+
"responses": [{ "inject": "(config) => {\n config.logger.info('origin called');\n config.state.requests = config.state.requests || 0;\n config.state.requests += 1;\n return {\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ count: config.state.requests })\n };\n}" }]
|
|
223
|
+
}]
|
|
224
|
+
}</code></pre>
|
|
225
|
+
</step>
|
|
226
|
+
|
|
227
|
+
<p>The injected function for the origin server imposter is formatted below. This
|
|
228
|
+
uses the <code>state</code> parameter, and sets the <code>requests</code>
|
|
229
|
+
field on it to return how many times it's been called:</p>
|
|
230
|
+
|
|
231
|
+
<pre><code>(config) => {
|
|
232
|
+
config.logger.info('origin called');
|
|
233
|
+
config.state.requests = config.state.requests || 0;
|
|
234
|
+
config.state.requests += 1;
|
|
235
|
+
return {
|
|
236
|
+
headers: {
|
|
237
|
+
'Content-Type': 'application/json'
|
|
238
|
+
},
|
|
239
|
+
<strong class='highlight3'>body: JSON.stringify({ count: config.state.requests })</strong>
|
|
240
|
+
};
|
|
241
|
+
}</code></pre>
|
|
242
|
+
|
|
243
|
+
<p>Now let's create the proxy imposter to try it out:</p>
|
|
244
|
+
|
|
245
|
+
<step type='http'>
|
|
246
|
+
<pre><code>POST /imposters HTTP/1.1
|
|
247
|
+
Host: localhost:<%= port %>
|
|
248
|
+
Accept: application/json
|
|
249
|
+
Content-Type: application/json
|
|
250
|
+
|
|
251
|
+
{
|
|
252
|
+
"port": 4546,
|
|
253
|
+
"protocol": "http",
|
|
254
|
+
"name": "proxy",
|
|
255
|
+
"stubs": [
|
|
256
|
+
{
|
|
257
|
+
"predicates": [<strong class='highlight1'>{ "equals": { "method": "GET", "path": "/counter" } }</strong>],
|
|
258
|
+
"responses": [{ "inject": "function (config) {\n var count = config.state.requests ? Object.keys(config.state.requests).length : 0;\n\n return {\n body: `There have been ${count} proxied calls`\n };\n}" }]
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"responses": [{ "inject": "function (config) {\n var cacheKey = config.request.method + ' ' + config.request.path;\n\n if (typeof config.state.requests === 'undefined') {\n config.state.requests = {};\n }\n\n if (config.state.requests[cacheKey]) {\n config.logger.info('Using previous response');\n config.callback(config.state.requests[cacheKey]);\n }\n\n var http = require('http'),\n options = {\n method: config.request.method,\n hostname: 'localhost',\n port: 5555,\n path: config.request.path,\n headers: config.request.headers\n },\n httpRequest = http.request(options, response => {\n var body = '';\n response.setEncoding('utf8');\n response.on('data', chunk => {\n body += chunk;\n });\n response.on('end', () => {\n var stubResponse = {\n statusCode: response.statusCode,\n headers: response.headers,\n body: body\n };\n config.logger.info('Successfully proxied: ' + JSON.stringify(stubResponse));\n config.state.requests[cacheKey] = stubResponse;\n config.callback(stubResponse);\n });\n });\n httpRequest.end();\n}" }]
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
}</code></pre>
|
|
265
|
+
</step>
|
|
266
|
+
|
|
267
|
+
<p>The first stub uses the following function. It depends on the second stub to
|
|
268
|
+
set a variable on the <code>state</code> parameter each time it proxies, and returns
|
|
269
|
+
a count of proxied calls. Note that it uses the exact same <code>requests</code>
|
|
270
|
+
field that the origin server injection uses on the <code>state</code> parameter,
|
|
271
|
+
but for a different purpose. This is OK - <code>state</code> is shared between
|
|
272
|
+
stubs on one imposter, but not shared between imposters. This function
|
|
273
|
+
<code>require</code>s in node's <code>util</code> module, showing an example
|
|
274
|
+
that takes advantage of the runtime environment.</p>
|
|
275
|
+
|
|
276
|
+
<pre><code>function (config) {
|
|
277
|
+
var count = config.state.requests ? Object.keys(config.state.requests).length : 0;
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
<strong class='highlight2'>body: `There have been ${count} proxied calls`</strong>
|
|
281
|
+
};
|
|
282
|
+
}</code></pre>
|
|
283
|
+
|
|
284
|
+
<p>The second stub uses an asynchronous proxy with a cache:</p>
|
|
285
|
+
|
|
286
|
+
<pre><code>function (config) {
|
|
287
|
+
var cacheKey = config.request.method + ' ' + config.request.path;
|
|
288
|
+
|
|
289
|
+
if (typeof config.state.requests === 'undefined') {
|
|
290
|
+
config.state.requests = {};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (config.state.requests[cacheKey]) {
|
|
294
|
+
config.logger.info('Using previous response');
|
|
295
|
+
config.callback(config.state.requests[cacheKey]);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
var http = require('http'),
|
|
299
|
+
options = {
|
|
300
|
+
method: config.request.method,
|
|
301
|
+
hostname: 'localhost',
|
|
302
|
+
port: 5555,
|
|
303
|
+
path: config.request.path,
|
|
304
|
+
headers: config.request.headers
|
|
305
|
+
},
|
|
306
|
+
httpRequest = http.request(options, response => {
|
|
307
|
+
var body = '';
|
|
308
|
+
response.setEncoding('utf8');
|
|
309
|
+
response.on('data', chunk => {
|
|
310
|
+
body += chunk;
|
|
311
|
+
});
|
|
312
|
+
response.on('end', () => {
|
|
313
|
+
var stubResponse = {
|
|
314
|
+
statusCode: response.statusCode,
|
|
315
|
+
headers: response.headers,
|
|
316
|
+
body
|
|
317
|
+
};
|
|
318
|
+
config.logger.info('Successfully proxied: ' + JSON.stringify(stubResponse));
|
|
319
|
+
config.state.requests[cacheKey] = stubResponse;
|
|
320
|
+
config.callback(stubResponse);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
httpRequest.end();
|
|
324
|
+
}</code></pre>
|
|
325
|
+
|
|
326
|
+
<p>Our first request should trigger a proxy call:</p>
|
|
327
|
+
|
|
328
|
+
<step type='http'>
|
|
329
|
+
<pre><code>GET /first HTTP/1.1
|
|
330
|
+
Host: localhost:4546</code></pre>
|
|
331
|
+
|
|
332
|
+
<assertResponse>
|
|
333
|
+
<pre><code>HTTP/1.1 200 OK
|
|
334
|
+
Content-Type: application/json
|
|
335
|
+
Connection: close
|
|
336
|
+
Date: <volatile>Thu, 09 Jan 2014 02:30:31 GMT</volatile>
|
|
337
|
+
Transfer-Encoding: chunked
|
|
338
|
+
|
|
339
|
+
<strong class='highlight3'>{ "count": 1 }</strong></code></pre>
|
|
340
|
+
</assertResponse>
|
|
341
|
+
</step>
|
|
342
|
+
|
|
343
|
+
<p>A second call also proxies because the path is different.</p>
|
|
344
|
+
|
|
345
|
+
<step type='http'>
|
|
346
|
+
<pre><code>GET /second HTTP/1.1
|
|
347
|
+
Host: localhost:4546</code></pre>
|
|
348
|
+
|
|
349
|
+
<assertResponse>
|
|
350
|
+
<pre><code>HTTP/1.1 200 OK
|
|
351
|
+
Content-Type: application/json
|
|
352
|
+
Connection: close
|
|
353
|
+
Date: <volatile>Thu, 09 Jan 2014 02:30:31 GMT</volatile>
|
|
354
|
+
Transfer-Encoding: chunked
|
|
355
|
+
|
|
356
|
+
<strong class='highlight3'>{ "count": 2 }</strong></code></pre>
|
|
357
|
+
</assertResponse>
|
|
358
|
+
</step>
|
|
359
|
+
|
|
360
|
+
<p>The third request should be served from the cache of the proxy imposter:</p>
|
|
361
|
+
|
|
362
|
+
<step type='http'>
|
|
363
|
+
<pre><code>GET /first HTTP/1.1
|
|
364
|
+
Host: localhost:4546</code></pre>
|
|
365
|
+
|
|
366
|
+
<assertResponse>
|
|
367
|
+
<pre><code>HTTP/1.1 200 OK
|
|
368
|
+
Content-Type: application/json
|
|
369
|
+
Connection: close
|
|
370
|
+
Date: <volatile>Thu, 09 Jan 2014 02:30:31 GMT</volatile>
|
|
371
|
+
Transfer-Encoding: chunked
|
|
372
|
+
|
|
373
|
+
<strong class='highlight3'>{ "count": 1 }</strong></code></pre>
|
|
374
|
+
</assertResponse>
|
|
375
|
+
</step>
|
|
376
|
+
|
|
377
|
+
<p>Finally, let's query the other stub on the proxy imposter:</p>
|
|
378
|
+
|
|
379
|
+
<step type='http'>
|
|
380
|
+
<pre><code><strong class='highlight1'>GET /counter</strong> HTTP/1.1
|
|
381
|
+
Host: localhost:4546</code></pre>
|
|
382
|
+
|
|
383
|
+
<assertResponse>
|
|
384
|
+
<pre><code>HTTP/1.1 200 OK
|
|
385
|
+
Connection: close
|
|
386
|
+
Date: <volatile>Thu, 09 Jan 2014 02:30:31 GMT</volatile>
|
|
387
|
+
Transfer-Encoding: chunked
|
|
388
|
+
|
|
389
|
+
<strong class='highlight2'>There have been 2 proxied calls</strong></code></pre>
|
|
390
|
+
</assertResponse>
|
|
391
|
+
</step>
|
|
392
|
+
|
|
393
|
+
<p>This is the most complicated example in the documentation, and took
|
|
394
|
+
some time to get it working (these examples in the docs are executed
|
|
395
|
+
as part of the test suite). This is appropriate since injection is the most
|
|
396
|
+
complicated thing you can do in the tool. When injection errors are detected,
|
|
397
|
+
mountebank logs the full <code>eval</code>'d code, including the bits it's added
|
|
398
|
+
on top of your code. It also logs JSON representation of the parameter.</p>
|
|
399
|
+
|
|
400
|
+
<p>As always, if you get stuck, <a href='/support'>ask for help.</a></p>
|
|
401
|
+
|
|
402
|
+
<step type='http'>
|
|
403
|
+
<code class='hidden'>DELETE /imposters/5555 HTTP/1.1
|
|
404
|
+
Host: localhost:<%= port %>
|
|
405
|
+
Accept: application/json</code>
|
|
406
|
+
</step>
|
|
407
|
+
|
|
408
|
+
<step type='http'>
|
|
409
|
+
<code class='hidden'>DELETE /imposters/4546 HTTP/1.1
|
|
410
|
+
Host: localhost:<%= port %>
|
|
411
|
+
Accept: application/json</code>
|
|
412
|
+
</step>
|
|
413
|
+
</testScenario>
|
|
414
|
+
|
|
415
|
+
<h2 id='including-modules'>Including other npm modules</h2>
|
|
416
|
+
|
|
417
|
+
<p>It is entirely possible to include other npm modules in your injection scripts.
|
|
418
|
+
However, you will need to install the module globally to ensure your injection function
|
|
419
|
+
can access it. For example, if you wanted to use the <code>underscore</code> module,
|
|
420
|
+
you'd first have to install it on the machine running mountebank:</p>
|
|
421
|
+
|
|
422
|
+
<pre><code>npm install -g underscore</code></pre>
|
|
423
|
+
|
|
424
|
+
<p>At that point, you can simply <code>require</code> it into your function.</p>
|
|
425
|
+
|
|
426
|
+
<%- include('../../_footer') -%>
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<%
|
|
2
|
+
title = 'json'
|
|
3
|
+
description = 'Using JSON in mountebank predicates'
|
|
4
|
+
%>
|
|
5
|
+
|
|
6
|
+
<%- include('../../_header') -%>
|
|
7
|
+
|
|
8
|
+
<h1>Using JSON Predicates</h1>
|
|
9
|
+
|
|
10
|
+
<p>It is possible to match <code>string</code> JSON fields using string operators, but mountebank finds
|
|
11
|
+
it clumsy, as you have to either exactly match the whitespace or use a regular expression. Given
|
|
12
|
+
mountebank's desire for elegance above all else, he supports predicates that treat JSON strings as
|
|
13
|
+
objects, allowing a fuller range of predicate matching.</p>
|
|
14
|
+
|
|
15
|
+
<p>JSON predicates follow the same semantics as those obeyed for multi-valued keys described on the
|
|
16
|
+
<a href='/docs/api/predicates'>main predicates page</a>, like those observed when a querystring has
|
|
17
|
+
the same key multiple times. Since the selected JSON field can potentially represent an array, most predicates
|
|
18
|
+
match if <em>any</em> array element matches. <code>deepEquals</code> will require all the values to match
|
|
19
|
+
(although the order isn't important).</p>
|
|
20
|
+
|
|
21
|
+
<h2>Examples</h2>
|
|
22
|
+
|
|
23
|
+
<p>Let's create an HTTP imposter. To show mountebank's first class support for JSON, we'll also
|
|
24
|
+
demonstrate <a href='/docs/protocols/http#inline-json-response-bodies'>passing a JSON object
|
|
25
|
+
as the <code>http body</code></a> in the response. We've added a <code>comment</code> field
|
|
26
|
+
to help explain each predicate. Like all unrecognized fields passed in, mountebank will
|
|
27
|
+
simply ignore it.</p>
|
|
28
|
+
|
|
29
|
+
<testScenario name='json'>
|
|
30
|
+
<step type='http'>
|
|
31
|
+
<pre><code>POST /imposters HTTP/1.1
|
|
32
|
+
Host: localhost:<%= port %>
|
|
33
|
+
Accept: application/json
|
|
34
|
+
Content-Type: application/json
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
"port": 4545,
|
|
38
|
+
"protocol": "http",
|
|
39
|
+
"stubs": [
|
|
40
|
+
{
|
|
41
|
+
"responses": [{
|
|
42
|
+
"is": {
|
|
43
|
+
"body": <strong class='highlight1'>{
|
|
44
|
+
"code": "SUCCESS",
|
|
45
|
+
"author": "J.K. Rowling"
|
|
46
|
+
}</strong>
|
|
47
|
+
}
|
|
48
|
+
}],
|
|
49
|
+
"predicates": [
|
|
50
|
+
{
|
|
51
|
+
"equals": { "body": { "title": "Harry Potter" } },
|
|
52
|
+
"caseSensitive": true,
|
|
53
|
+
"comment": "case sensitivity applies to the key as well as the value"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"equals": { "body": { "title": "POTTER" } },
|
|
57
|
+
"except": "HARRY ",
|
|
58
|
+
"comment": "The except regular expression is removed from the value before matching"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"matches": { "body": { "title": "^Harry" } }
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"exists": { "body": { "title": true } },
|
|
65
|
+
"comment": "The given JSON key must exist"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"exists": { "body": { "name": false } },
|
|
69
|
+
"comment": "The given JSON key must NOT exist"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}</code></pre>
|
|
75
|
+
</step>
|
|
76
|
+
|
|
77
|
+
<p>We'll pass the following HTTP request to test the predicates and confirm we get the
|
|
78
|
+
expected response:</p>
|
|
79
|
+
|
|
80
|
+
<step type='http'>
|
|
81
|
+
<pre><code>POST / HTTP/1.1
|
|
82
|
+
Host: localhost:4545
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
"title": "Harry Potter",
|
|
86
|
+
"summary": "Dragons and a boy wizard"
|
|
87
|
+
}</code></pre>
|
|
88
|
+
|
|
89
|
+
<assertResponse>
|
|
90
|
+
<pre><code>HTTP/1.1 200 OK
|
|
91
|
+
Connection: close
|
|
92
|
+
Date: <volatile>Thu, 09 Jan 2014 02:30:31 GMT</volatile>
|
|
93
|
+
Transfer-Encoding: chunked
|
|
94
|
+
|
|
95
|
+
<strong class='highlight1'>{
|
|
96
|
+
"code": "SUCCESS",
|
|
97
|
+
"author": "J.K. Rowling"
|
|
98
|
+
}</strong></code></pre>
|
|
99
|
+
</assertResponse>
|
|
100
|
+
</step>
|
|
101
|
+
|
|
102
|
+
<h3>Embedded Arrays</h3>
|
|
103
|
+
|
|
104
|
+
<p>mountebank uses the same logic to <a href='/docs/api/predicates#array-match'>process arrays</a> as he
|
|
105
|
+
uses in other predicate operations, which can be summarized as follows:</p>
|
|
106
|
+
|
|
107
|
+
<ul class='bullet-list'>
|
|
108
|
+
<li>For all operators except <code>deepEquals</code>, at least one element in the array must match.
|
|
109
|
+
The array syntax can be left off.</li>
|
|
110
|
+
<li>For <code>deepEquals</code>, all elements of the array have to match, in any order.</li>
|
|
111
|
+
<li>If you put an array in the predicate definition, all fields must match the array fields in the
|
|
112
|
+
request, in any order. For <code>deepEquals</code> predicates, the array lengths must also match.</li>
|
|
113
|
+
</ul>
|
|
114
|
+
|
|
115
|
+
<p>These rules can be explored through the following example:</p>
|
|
116
|
+
|
|
117
|
+
<step type='http'>
|
|
118
|
+
<pre><code>POST /imposters HTTP/1.1
|
|
119
|
+
Host: localhost:<%= port %>
|
|
120
|
+
Accept: application/json
|
|
121
|
+
Content-Type: application/json
|
|
122
|
+
|
|
123
|
+
{
|
|
124
|
+
"port": 4546,
|
|
125
|
+
"protocol": "http",
|
|
126
|
+
"stubs": [
|
|
127
|
+
{
|
|
128
|
+
"responses": [{
|
|
129
|
+
"is": {
|
|
130
|
+
"body": "<strong class='highlight1'>Matched all elements exactly</strong>"
|
|
131
|
+
}
|
|
132
|
+
}],
|
|
133
|
+
"predicates": [
|
|
134
|
+
{
|
|
135
|
+
"deepEquals": {
|
|
136
|
+
"body": {
|
|
137
|
+
"books": [
|
|
138
|
+
{ "title": "The Hobbit" },
|
|
139
|
+
{ "title": "Game of Thrones" }
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"responses": [{
|
|
148
|
+
"is": {
|
|
149
|
+
"body": "<strong class='highlight2'>Matched all elements listed</strong>"
|
|
150
|
+
}
|
|
151
|
+
}],
|
|
152
|
+
"predicates": [
|
|
153
|
+
{
|
|
154
|
+
"equals": {
|
|
155
|
+
"body": {
|
|
156
|
+
"books": {
|
|
157
|
+
"title": "The Hobbit"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}</code></pre>
|
|
166
|
+
</step>
|
|
167
|
+
|
|
168
|
+
<p>We'll pass the following HTTP request, leaving off <em>Harry Potter</em> so that all elements match the
|
|
169
|
+
<code>deepEquals</code> predicate:</p>
|
|
170
|
+
|
|
171
|
+
<step type='http'>
|
|
172
|
+
<pre><code>POST / HTTP/1.1
|
|
173
|
+
Host: localhost:4546
|
|
174
|
+
|
|
175
|
+
{
|
|
176
|
+
"books": [
|
|
177
|
+
{ "title": "Game of Thrones" },
|
|
178
|
+
{ "title": "The Hobbit" }
|
|
179
|
+
]
|
|
180
|
+
}</code></pre>
|
|
181
|
+
|
|
182
|
+
<assertResponse>
|
|
183
|
+
<pre><code>HTTP/1.1 200 OK
|
|
184
|
+
Connection: close
|
|
185
|
+
Date: <volatile>Thu, 09 Jan 2014 02:30:31 GMT</volatile>
|
|
186
|
+
Transfer-Encoding: chunked
|
|
187
|
+
|
|
188
|
+
<strong class='highlight1'>Matched all elements exactly</strong></code></pre>
|
|
189
|
+
</assertResponse>
|
|
190
|
+
</step>
|
|
191
|
+
|
|
192
|
+
<step type='http'>
|
|
193
|
+
<code class='hidden'>DELETE /imposters/4545 HTTP/1.1
|
|
194
|
+
Host: localhost:<%= port %>
|
|
195
|
+
Accept: application/json</code>
|
|
196
|
+
</step>
|
|
197
|
+
|
|
198
|
+
<step type='http'>
|
|
199
|
+
<code class='hidden'>DELETE /imposters/4546 HTTP/1.1
|
|
200
|
+
Host: localhost:<%= port %>
|
|
201
|
+
Accept: application/json</code>
|
|
202
|
+
</step>
|
|
203
|
+
</testScenario>
|
|
204
|
+
|
|
205
|
+
<%- include('../../_footer') -%>
|