@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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/bin/mb +136 -0
  4. package/package.json +71 -0
  5. package/releases.json +52 -0
  6. package/src/cli/api.js +112 -0
  7. package/src/cli/cli.js +420 -0
  8. package/src/controllers/configController.js +64 -0
  9. package/src/controllers/feedController.js +115 -0
  10. package/src/controllers/homeController.js +58 -0
  11. package/src/controllers/imposterController.js +328 -0
  12. package/src/controllers/impostersController.js +215 -0
  13. package/src/controllers/logsController.js +52 -0
  14. package/src/models/behaviors.js +553 -0
  15. package/src/models/behaviorsValidator.js +186 -0
  16. package/src/models/compatibility.js +133 -0
  17. package/src/models/dryRunValidator.js +261 -0
  18. package/src/models/filesystemBackedImpostersRepository.js +908 -0
  19. package/src/models/http/baseHttpServer.js +207 -0
  20. package/src/models/http/headersMap.js +87 -0
  21. package/src/models/http/httpProxy.js +230 -0
  22. package/src/models/http/httpRequest.js +82 -0
  23. package/src/models/http/httpServer.js +18 -0
  24. package/src/models/http/index.js +18 -0
  25. package/src/models/https/cert/mb-cert.pem +20 -0
  26. package/src/models/https/cert/mb-csr.pem +16 -0
  27. package/src/models/https/cert/mb-key.pem +27 -0
  28. package/src/models/https/httpsServer.js +42 -0
  29. package/src/models/https/index.js +18 -0
  30. package/src/models/imposter.js +243 -0
  31. package/src/models/imposterPrinter.js +120 -0
  32. package/src/models/impostersRepository.js +49 -0
  33. package/src/models/inMemoryImpostersRepository.js +418 -0
  34. package/src/models/jsonpath.js +44 -0
  35. package/src/models/mbConnection.js +107 -0
  36. package/src/models/predicates.js +438 -0
  37. package/src/models/protocols.js +242 -0
  38. package/src/models/responseResolver.js +398 -0
  39. package/src/models/smtp/index.js +16 -0
  40. package/src/models/smtp/smtpRequest.js +60 -0
  41. package/src/models/smtp/smtpServer.js +109 -0
  42. package/src/models/tcp/index.js +18 -0
  43. package/src/models/tcp/tcpProxy.js +110 -0
  44. package/src/models/tcp/tcpRequest.js +23 -0
  45. package/src/models/tcp/tcpServer.js +156 -0
  46. package/src/models/tcp/tcpValidator.js +19 -0
  47. package/src/models/xpath.js +95 -0
  48. package/src/mountebank.js +245 -0
  49. package/src/public/images/arrow_down.png +0 -0
  50. package/src/public/images/arrow_up.png +0 -0
  51. package/src/public/images/book.jpg +0 -0
  52. package/src/public/images/dataflow.png +0 -0
  53. package/src/public/images/favicon.ico +0 -0
  54. package/src/public/images/forkme_right_orange_ff7600.png +0 -0
  55. package/src/public/images/mountebank.png +0 -0
  56. package/src/public/images/overview.gif +0 -0
  57. package/src/public/images/quote.png +0 -0
  58. package/src/public/images/tw-logo.png +0 -0
  59. package/src/public/scripts/jquery/jquery-3.6.1.min.js +2 -0
  60. package/src/public/scripts/urlHashHandler.js +31 -0
  61. package/src/public/stylesheets/application.css +424 -0
  62. package/src/public/stylesheets/ie.css +14 -0
  63. package/src/public/stylesheets/imposters.css +121 -0
  64. package/src/public/stylesheets/jqueryui/1.10.4/themes/smoothness/jquery-ui.css +1178 -0
  65. package/src/util/combinators.js +68 -0
  66. package/src/util/date.js +51 -0
  67. package/src/util/errors.js +55 -0
  68. package/src/util/helpers.js +131 -0
  69. package/src/util/inherit.js +28 -0
  70. package/src/util/ip.js +54 -0
  71. package/src/util/logger.js +83 -0
  72. package/src/util/middleware.js +256 -0
  73. package/src/util/scopedLogger.js +47 -0
  74. package/src/views/_footer.ejs +20 -0
  75. package/src/views/_header.ejs +113 -0
  76. package/src/views/_imposter.ejs +8 -0
  77. package/src/views/config.ejs +71 -0
  78. package/src/views/docs/api/behaviors/copy.ejs +427 -0
  79. package/src/views/docs/api/behaviors/decorate.ejs +182 -0
  80. package/src/views/docs/api/behaviors/lookup.ejs +220 -0
  81. package/src/views/docs/api/behaviors/shellTransform.ejs +153 -0
  82. package/src/views/docs/api/behaviors/wait.ejs +121 -0
  83. package/src/views/docs/api/behaviors.ejs +141 -0
  84. package/src/views/docs/api/contracts/addStub-description.ejs +10 -0
  85. package/src/views/docs/api/contracts/addStub.ejs +10 -0
  86. package/src/views/docs/api/contracts/config-description.ejs +32 -0
  87. package/src/views/docs/api/contracts/config.ejs +23 -0
  88. package/src/views/docs/api/contracts/home-description.ejs +18 -0
  89. package/src/views/docs/api/contracts/home.ejs +13 -0
  90. package/src/views/docs/api/contracts/imposter-description.ejs +439 -0
  91. package/src/views/docs/api/contracts/imposter.ejs +182 -0
  92. package/src/views/docs/api/contracts/imposters-description.ejs +13 -0
  93. package/src/views/docs/api/contracts/imposters.ejs +13 -0
  94. package/src/views/docs/api/contracts/logs-description.ejs +3 -0
  95. package/src/views/docs/api/contracts/logs.ejs +14 -0
  96. package/src/views/docs/api/contracts/stub-description.ejs +4 -0
  97. package/src/views/docs/api/contracts/stub.ejs +7 -0
  98. package/src/views/docs/api/contracts/stubs-description.ejs +4 -0
  99. package/src/views/docs/api/contracts/stubs.ejs +11 -0
  100. package/src/views/docs/api/contracts.ejs +133 -0
  101. package/src/views/docs/api/errors.ejs +64 -0
  102. package/src/views/docs/api/fault/connectionReset.ejs +31 -0
  103. package/src/views/docs/api/fault/randomDataThenClose.ejs +31 -0
  104. package/src/views/docs/api/faults.ejs +57 -0
  105. package/src/views/docs/api/injection.ejs +426 -0
  106. package/src/views/docs/api/json.ejs +205 -0
  107. package/src/views/docs/api/jsonpath.ejs +210 -0
  108. package/src/views/docs/api/mocks.ejs +130 -0
  109. package/src/views/docs/api/overview.ejs +968 -0
  110. package/src/views/docs/api/predicates/and.ejs +62 -0
  111. package/src/views/docs/api/predicates/contains.ejs +64 -0
  112. package/src/views/docs/api/predicates/deepEquals.ejs +114 -0
  113. package/src/views/docs/api/predicates/endsWith.ejs +66 -0
  114. package/src/views/docs/api/predicates/equals.ejs +125 -0
  115. package/src/views/docs/api/predicates/exists.ejs +118 -0
  116. package/src/views/docs/api/predicates/inject.ejs +67 -0
  117. package/src/views/docs/api/predicates/matches.ejs +66 -0
  118. package/src/views/docs/api/predicates/not.ejs +52 -0
  119. package/src/views/docs/api/predicates/or.ejs +79 -0
  120. package/src/views/docs/api/predicates/startsWith.ejs +62 -0
  121. package/src/views/docs/api/predicates.ejs +382 -0
  122. package/src/views/docs/api/proxies.ejs +191 -0
  123. package/src/views/docs/api/proxy/addDecorateBehavior.ejs +115 -0
  124. package/src/views/docs/api/proxy/addWaitBehavior.ejs +96 -0
  125. package/src/views/docs/api/proxy/injectHeaders.ejs +91 -0
  126. package/src/views/docs/api/proxy/predicateGenerators.ejs +600 -0
  127. package/src/views/docs/api/proxy/proxyModes.ejs +495 -0
  128. package/src/views/docs/api/stubs.ejs +391 -0
  129. package/src/views/docs/api/xpath.ejs +281 -0
  130. package/src/views/docs/cli/configFiles.ejs +133 -0
  131. package/src/views/docs/cli/customFormatters.ejs +53 -0
  132. package/src/views/docs/cli/help.ejs +6 -0
  133. package/src/views/docs/cli/replay.ejs +42 -0
  134. package/src/views/docs/cli/restart.ejs +10 -0
  135. package/src/views/docs/cli/save.ejs +68 -0
  136. package/src/views/docs/cli/start.ejs +234 -0
  137. package/src/views/docs/cli/stop.ejs +32 -0
  138. package/src/views/docs/commandLine.ejs +93 -0
  139. package/src/views/docs/communityExtensions.ejs +233 -0
  140. package/src/views/docs/gettingStarted.ejs +146 -0
  141. package/src/views/docs/mentalModel.ejs +51 -0
  142. package/src/views/docs/protocols/custom.ejs +231 -0
  143. package/src/views/docs/protocols/http.ejs +238 -0
  144. package/src/views/docs/protocols/https.ejs +246 -0
  145. package/src/views/docs/protocols/smtp.ejs +142 -0
  146. package/src/views/docs/protocols/tcp.ejs +431 -0
  147. package/src/views/docs/security.ejs +38 -0
  148. package/src/views/faqs.ejs +65 -0
  149. package/src/views/feed.ejs +33 -0
  150. package/src/views/imposter.ejs +22 -0
  151. package/src/views/imposters.ejs +33 -0
  152. package/src/views/index.ejs +89 -0
  153. package/src/views/license.ejs +30 -0
  154. package/src/views/logs.ejs +77 -0
  155. package/src/views/releases/v1.1.0.ejs +55 -0
  156. package/src/views/releases/v1.1.36.ejs +84 -0
  157. package/src/views/releases/v1.1.72.ejs +92 -0
  158. package/src/views/releases/v1.10.0.ejs +108 -0
  159. package/src/views/releases/v1.11.0.ejs +109 -0
  160. package/src/views/releases/v1.12.0.ejs +96 -0
  161. package/src/views/releases/v1.13.0.ejs +118 -0
  162. package/src/views/releases/v1.14.0.ejs +107 -0
  163. package/src/views/releases/v1.14.1.ejs +94 -0
  164. package/src/views/releases/v1.15.0.ejs +113 -0
  165. package/src/views/releases/v1.16.0.ejs +104 -0
  166. package/src/views/releases/v1.2.0.ejs +78 -0
  167. package/src/views/releases/v1.2.103.ejs +86 -0
  168. package/src/views/releases/v1.2.122.ejs +86 -0
  169. package/src/views/releases/v1.2.30.ejs +84 -0
  170. package/src/views/releases/v1.2.45.ejs +84 -0
  171. package/src/views/releases/v1.2.56.ejs +79 -0
  172. package/src/views/releases/v1.3.0.ejs +86 -0
  173. package/src/views/releases/v1.3.1.ejs +100 -0
  174. package/src/views/releases/v1.4.0.ejs +96 -0
  175. package/src/views/releases/v1.4.1.ejs +103 -0
  176. package/src/views/releases/v1.4.2.ejs +100 -0
  177. package/src/views/releases/v1.4.3.ejs +113 -0
  178. package/src/views/releases/v1.5.0.ejs +104 -0
  179. package/src/views/releases/v1.5.1.ejs +91 -0
  180. package/src/views/releases/v1.6.0.ejs +109 -0
  181. package/src/views/releases/v1.7.0.ejs +113 -0
  182. package/src/views/releases/v1.7.1.ejs +90 -0
  183. package/src/views/releases/v1.7.2.ejs +96 -0
  184. package/src/views/releases/v1.8.0.ejs +121 -0
  185. package/src/views/releases/v1.9.0.ejs +111 -0
  186. package/src/views/releases/v2.0.0.ejs +159 -0
  187. package/src/views/releases/v2.1.0.ejs +121 -0
  188. package/src/views/releases/v2.1.1.ejs +106 -0
  189. package/src/views/releases/v2.1.2.ejs +84 -0
  190. package/src/views/releases/v2.2.0.ejs +115 -0
  191. package/src/views/releases/v2.2.1.ejs +102 -0
  192. package/src/views/releases/v2.3.0.ejs +121 -0
  193. package/src/views/releases/v2.3.1.ejs +100 -0
  194. package/src/views/releases/v2.3.2.ejs +102 -0
  195. package/src/views/releases/v2.3.3.ejs +97 -0
  196. package/src/views/releases/v2.4.0.ejs +114 -0
  197. package/src/views/releases/v2.5.0.ejs +51 -0
  198. package/src/views/releases/v2.6.0.ejs +35 -0
  199. package/src/views/releases/v2.7.0.ejs +32 -0
  200. package/src/views/releases/v2.8.0.ejs +36 -0
  201. package/src/views/releases/v2.8.1.ejs +7 -0
  202. package/src/views/releases/v2.8.2.ejs +26 -0
  203. package/src/views/releases/v2.9.0.ejs +32 -0
  204. package/src/views/releases/v2.9.1.ejs +10 -0
  205. package/src/views/releases.ejs +26 -0
  206. package/src/views/sitemap.ejs +36 -0
  207. 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('&lt;?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('&lt;?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
+ &lt;customer&gt;
121
+ &lt;name&gt;Test&lt;/name&gt;
122
+ &lt;/customer&gt;</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'>&lt;?xml&gt;</strong>
140
+ &lt;customer&gt;
141
+ &lt;name&gt;Test&lt;/name&gt;
142
+ &lt;/customer&gt;</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') -%>