@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,220 @@
|
|
|
1
|
+
<table>
|
|
2
|
+
<tr>
|
|
3
|
+
<th>Parameter</th>
|
|
4
|
+
<th>Type</th>
|
|
5
|
+
<th>Description</th>
|
|
6
|
+
</tr>
|
|
7
|
+
<tr>
|
|
8
|
+
<td><code>lookup</code></td>
|
|
9
|
+
<td>An object</td>
|
|
10
|
+
<td>An object specifying the key (copied from a request field), the data source,
|
|
11
|
+
and the response token</td>
|
|
12
|
+
</tr>
|
|
13
|
+
<tr>
|
|
14
|
+
<td><code>lookup.key</code></td>
|
|
15
|
+
<td>An object</td>
|
|
16
|
+
<td>The information on how to select the key from the request.</td>
|
|
17
|
+
</tr>
|
|
18
|
+
<tr>
|
|
19
|
+
<td><code>lookup.key.from</code></td>
|
|
20
|
+
<td>A string or an object</td>
|
|
21
|
+
<td>The name of the request field to select from, or, if the request field is an object,
|
|
22
|
+
then an object specifying the path to the request field. For example,
|
|
23
|
+
<pre><code>{ "from": "body" }</code></pre> and <pre><code>{ "from": { "query": "q" } }</code></pre>
|
|
24
|
+
are both valid.</td>
|
|
25
|
+
</tr>
|
|
26
|
+
<tr>
|
|
27
|
+
<td><code>lookup.key.using</code></td>
|
|
28
|
+
<td>An object</td>
|
|
29
|
+
<td>The configuration needed to select the key from the response</td>
|
|
30
|
+
</tr>
|
|
31
|
+
<tr>
|
|
32
|
+
<td><code>lookup.key.using.method</code></td>
|
|
33
|
+
<td>A string</td>
|
|
34
|
+
<td>The method used to select the key from the request. Allowed values are <code>regex</code>,
|
|
35
|
+
<code>xpath</code>, and <code>jsonpath</code>.</td>
|
|
36
|
+
</tr>
|
|
37
|
+
<tr>
|
|
38
|
+
<td><code>lookup.key.using.selector</code></td>
|
|
39
|
+
<td>A string</td>
|
|
40
|
+
<td>The selector used to select the key from the request. For a <code>regex</code>, this would
|
|
41
|
+
be the pattern, and the replacement value will be the entire match by default.
|
|
42
|
+
<code>xpath</code> and <code>jsonpath</code> selectors work on XML and JSON documents. If the request
|
|
43
|
+
value does not match the selector (including through XML or JSON parsing errors), nothing is replaced.</td>
|
|
44
|
+
</tr>
|
|
45
|
+
<tr>
|
|
46
|
+
<td><code>lookup.key.using.ns</code></td>
|
|
47
|
+
<td>An object</td>
|
|
48
|
+
<td>For <code>xpath</code> selectors, the <code>ns</code> object maps namespace aliases to URLs</td>
|
|
49
|
+
</tr>
|
|
50
|
+
<tr>
|
|
51
|
+
<td><code>lookup.key.using.options</code></td>
|
|
52
|
+
<td>An object</td>
|
|
53
|
+
<td>For <code>regex</code> selectors, the <code>options</code> object describes the regular expression options</td>
|
|
54
|
+
</tr>
|
|
55
|
+
<tr>
|
|
56
|
+
<td><code>lookup.key.using.options.ignoreCase</code></td>
|
|
57
|
+
<td>A boolean</td>
|
|
58
|
+
<td>Uses a <a href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/ignoreCase'>case-insensitive</a>
|
|
59
|
+
regular expression</td>
|
|
60
|
+
</tr>
|
|
61
|
+
<tr>
|
|
62
|
+
<td><code>lookup.key.using.options.multiline</code></td>
|
|
63
|
+
<td>A boolean</td>
|
|
64
|
+
<td>Uses a <a href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/multiline'>multiline</a>
|
|
65
|
+
regular expression</td>
|
|
66
|
+
</tr>
|
|
67
|
+
<tr>
|
|
68
|
+
<td><code>lookup.key.index</code></td>
|
|
69
|
+
<td>An int (defaults to 0)</td>
|
|
70
|
+
<td>Each of the selection options returns an array: <code>regex</code> returns an array of parenthesized
|
|
71
|
+
gropus (with the entire match in the 0th index), and <code>jsonpath</code> and <code>xpath</code>
|
|
72
|
+
return an array of matches. This field selects the appropriate value from the array to use as
|
|
73
|
+
the lookup key into the data source.</td>
|
|
74
|
+
</tr>
|
|
75
|
+
<tr>
|
|
76
|
+
<td><code>lookup.fromDataSource</code></td>
|
|
77
|
+
<td>An object</td>
|
|
78
|
+
<td>Configuration for the external data source to lookup data based on the key. Each
|
|
79
|
+
<code>lookup</code> configuration may only specify one data source.</td>
|
|
80
|
+
</tr>
|
|
81
|
+
<tr>
|
|
82
|
+
<td><code>lookup.fromDataSource.csv</code></td>
|
|
83
|
+
<td>An object</td>
|
|
84
|
+
<td>Configuration for using a CSV file as the data source</td>
|
|
85
|
+
</tr>
|
|
86
|
+
<tr>
|
|
87
|
+
<td><code>lookup.fromDataSource.csv.path</code></td>
|
|
88
|
+
<td>A string</td>
|
|
89
|
+
<td>The path to the CSV file, which must be readable by the <code>mb</code> process</td>
|
|
90
|
+
</tr>
|
|
91
|
+
<tr>
|
|
92
|
+
<td><code>lookup.fromDataSource.csv.keyColumn</code></td>
|
|
93
|
+
<td>A string</td>
|
|
94
|
+
<td>The header of the column to scan for a match against the key. If a match is
|
|
95
|
+
found, the entire row will be returned.</td>
|
|
96
|
+
</tr>
|
|
97
|
+
<tr>
|
|
98
|
+
<td><code>lookup.fromDataSource.csv.delimiter</code></td>
|
|
99
|
+
<td>A string(default to comma ,)</td>
|
|
100
|
+
<td>The delimiter separated colums in CSV file.</td>
|
|
101
|
+
</tr>
|
|
102
|
+
<tr>
|
|
103
|
+
<td><code>lookup.into</code></td>
|
|
104
|
+
<td>A string</td>
|
|
105
|
+
<td>The token to replace in the response with the selected request value. There is
|
|
106
|
+
no need to specify which field in the response the token will be in; all response
|
|
107
|
+
tokens will be replaced in all response fields. A successful match will return a
|
|
108
|
+
hashmap or dictionary type object, with named indexes. For example, if you specify
|
|
109
|
+
<pre><code>{ "into": "${NAME}" }</code></pre> as your token configuration, and the
|
|
110
|
+
data source returned a row containing "first" and "last" fields, then
|
|
111
|
+
<code>${NAME}["first"]</code> and <code>${NAME}["last"]</code> will be replaced by
|
|
112
|
+
the appropriate data. You can quote the field name with double quotes, single quotes,
|
|
113
|
+
or no quotes at all.</td>
|
|
114
|
+
</tr>
|
|
115
|
+
</table>
|
|
116
|
+
|
|
117
|
+
<p>The <code>lookup</code> behavior supports dynamically replacing values in the response with something that
|
|
118
|
+
comes from an external data source. Looking up the values from the data source requires first selecting
|
|
119
|
+
the key value from the request. The key selection and replacement behavior in the response mirrors the
|
|
120
|
+
functionality for the <code>copy</code> behavior. We'll look at the following examples:</p>
|
|
121
|
+
|
|
122
|
+
<ul class='bullet-list'>
|
|
123
|
+
<li><a href='#lookup-regex-replacement'>Lookup up from a CSV file based on a key selected with a regular expression</a></li>
|
|
124
|
+
</ul>
|
|
125
|
+
|
|
126
|
+
<p class='info-icon'>Look at the <code>copy</code> examples to see how to do advanced key
|
|
127
|
+
selection using jsonpath and xpath.</p>
|
|
128
|
+
|
|
129
|
+
<testScenario name='lookup'>
|
|
130
|
+
|
|
131
|
+
<p>We'll use the following CSV file for these examples, saved as "values.csv" in the
|
|
132
|
+
current working directory of the <code>mb</code> process (you can always use an absolute path):</p>
|
|
133
|
+
|
|
134
|
+
<step type='file' filename='<%= process.cwd() %>/values.csv'>
|
|
135
|
+
<pre><code>State_ID,code,Name,price,tree,jobs
|
|
136
|
+
1111111,400,liquid,235651,mango,farmer
|
|
137
|
+
9856543,404,solid,54564564,orange,miner
|
|
138
|
+
2222222,500,water,12564,pine,shepherd
|
|
139
|
+
1234564,200,plasma,2656,guava,lumberjack
|
|
140
|
+
9999999,200,lovers,9999,dogwood,steel worker</code></pre>
|
|
141
|
+
</step>
|
|
142
|
+
|
|
143
|
+
<h3 id='lookup-regex-replacement'>Lookup up from a CSV file based on a key selected with a regular expression</h3>
|
|
144
|
+
|
|
145
|
+
<p>The following example shows selecting the key using a regular expression to match against the path. We're
|
|
146
|
+
using the <code>index</code> field to select the first parenthesized group in the regular expression.</p>
|
|
147
|
+
|
|
148
|
+
<step type='http'>
|
|
149
|
+
<pre><code>POST /imposters HTTP/1.1
|
|
150
|
+
Host: localhost:<%= port %>
|
|
151
|
+
Accept: application/json
|
|
152
|
+
Content-Type: application/json
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
"port": 9595,
|
|
156
|
+
"protocol": "http",
|
|
157
|
+
"stubs": [
|
|
158
|
+
{
|
|
159
|
+
"responses": [
|
|
160
|
+
{
|
|
161
|
+
"is": {
|
|
162
|
+
"statusCode": "<strong class='highlight1'>${row}['code']</strong>",
|
|
163
|
+
"headers": {
|
|
164
|
+
"X-Tree": "<strong class='highlight1'>${row}['tree']</strong>"
|
|
165
|
+
},
|
|
166
|
+
"body": "Hello <strong class='highlight1'>${row}['Name']</strong>, have you done your <strong class='highlight1'>${row}['jobs']</strong> today?"
|
|
167
|
+
},
|
|
168
|
+
"behaviors": [
|
|
169
|
+
{
|
|
170
|
+
"lookup": {
|
|
171
|
+
"key": <strong class='highlight2'>{
|
|
172
|
+
"from": "path",
|
|
173
|
+
"using": { "method": "regex", "selector": "/(.*)$" },
|
|
174
|
+
"index": 1
|
|
175
|
+
}</strong>,
|
|
176
|
+
"fromDataSource": {
|
|
177
|
+
"csv": {
|
|
178
|
+
"path": "<%= process.cwd() %>/values.csv",
|
|
179
|
+
"keyColumn": "Name",
|
|
180
|
+
"delimiter": ","
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"into": "<strong class='highlight1'>${row}</strong>"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
}</code></pre>
|
|
192
|
+
</step>
|
|
193
|
+
|
|
194
|
+
<p>As with the <code>copy</code> behavior, we can plug tokens into any of the response fields
|
|
195
|
+
(including the <code>statusCode</code>). Let's see what happens when we craft a request to match
|
|
196
|
+
all of those selectors:</p>
|
|
197
|
+
|
|
198
|
+
<step type='http'>
|
|
199
|
+
<pre><code>GET /<strong class='highlight2'>liquid</strong> HTTP/1.1
|
|
200
|
+
Host: localhost:9595</code></pre>
|
|
201
|
+
|
|
202
|
+
<assertResponse>
|
|
203
|
+
<pre><code>HTTP/1.1 <strong class='highlight1'>400</strong> Bad Request
|
|
204
|
+
X-Tree: <strong class='highlight1'>mango</strong>
|
|
205
|
+
Connection: close
|
|
206
|
+
Date: <volatile>Thu, 28 Dec 2016 11:37:31 GMT</volatile>
|
|
207
|
+
Transfer-Encoding: chunked
|
|
208
|
+
|
|
209
|
+
Hello <strong class='highlight1'>liquid</strong>, have you done your <strong class='highlight1'>farmer</strong> today?</code></pre>
|
|
210
|
+
</assertResponse>
|
|
211
|
+
</step>
|
|
212
|
+
|
|
213
|
+
<step type='http'>
|
|
214
|
+
<code class='hidden'>DELETE /imposters/9595 HTTP/1.1
|
|
215
|
+
Host: localhost:<%= port %></code>
|
|
216
|
+
</step>
|
|
217
|
+
|
|
218
|
+
<step type='file' filename='<%= process.cwd() %>/values.csv' delete='true'><code>
|
|
219
|
+
</code></step>
|
|
220
|
+
</testScenario>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<table>
|
|
2
|
+
<tr>
|
|
3
|
+
<th>Parameter</th>
|
|
4
|
+
<th>Type</th>
|
|
5
|
+
<th>Description</th>
|
|
6
|
+
</tr>
|
|
7
|
+
<tr>
|
|
8
|
+
<td><code>shellTransform</code></td>
|
|
9
|
+
<td>A string</td>
|
|
10
|
+
<td>Represents the path to a command line application. The application should
|
|
11
|
+
retrieve the JSON-encoded <code>request</code> and <code>response</code> from the environment
|
|
12
|
+
and print out the transformed <code>response</code> to <code>stdout</code>.
|
|
13
|
+
|
|
14
|
+
<p class='warning-icon'>The <code><a href='/docs/commandLine'>--allowInjection</a></code> command
|
|
15
|
+
line flag must be set to support this behavior.</p>
|
|
16
|
+
</td>
|
|
17
|
+
</tr>
|
|
18
|
+
</table>
|
|
19
|
+
|
|
20
|
+
<p>The <code>shellTransform</code> behavior plays a similar role as the <code>decorate</code> behavior,
|
|
21
|
+
enabling a programmatic transformation of the response. However, you don't have to write the transformation
|
|
22
|
+
logic in JavaScript -- it can be in the language of your choice.</p>
|
|
23
|
+
|
|
24
|
+
<p class='warning-icon'>Since it allows shelling out to another application,
|
|
25
|
+
the <a href='/docs/commandLine'>--allowInjection</a> flag must be passed in to <code>mb</code> on startup.</p>
|
|
26
|
+
|
|
27
|
+
<p>mountebank will expose the following environment variables to your shell application:</p>
|
|
28
|
+
|
|
29
|
+
<ul class='bullet-list'>
|
|
30
|
+
<li><code>MB_REQUEST</code> which contains the JSON request as a string, and</li>
|
|
31
|
+
<li><code>MB_RESPONSE</code> which contains the current JSON response as a string</li>
|
|
32
|
+
</ul>
|
|
33
|
+
|
|
34
|
+
<p>The application should write to stdout a JSON representation of
|
|
35
|
+
the transformed response.</p>
|
|
36
|
+
|
|
37
|
+
<p>We'll show a simple example of shelling out to plug in response values based on an external data source.</p>
|
|
38
|
+
|
|
39
|
+
<h3 id='external-datasource'>Using an external data source</h3>
|
|
40
|
+
|
|
41
|
+
<p>At times you may find it convenient to use an external data store to fill in dynamic values based on
|
|
42
|
+
data coming in from the request. You can combine an <code>is</code> canned response with a
|
|
43
|
+
<code>shellTransform</code> behavior to achieve this, regardless of what the external data source is.
|
|
44
|
+
For this example, we'll assume it's a simple pipe-delimited file mapping customer ids to names. We'll
|
|
45
|
+
save it as <code>names.csv</code> in the directory we run <code>mb</code> from.</p>
|
|
46
|
+
|
|
47
|
+
<p class='info-icon'>Note that, by reformatting the file, this example would more easily be satisfied
|
|
48
|
+
by the <code>lookup</code> behavior.</p>
|
|
49
|
+
|
|
50
|
+
<testScenario name='shellTransform'>
|
|
51
|
+
<step type='file' filename='<%= process.cwd() %>/names.txt'>
|
|
52
|
+
<pre><code>123|Frodo Baggins
|
|
53
|
+
234|Samwise Gamgee
|
|
54
|
+
345|Gandalf the White
|
|
55
|
+
456|Smeagol</code></pre>
|
|
56
|
+
</step>
|
|
57
|
+
|
|
58
|
+
<p>We expect the incoming http request to specify the id in the URL, and we want to represent the name
|
|
59
|
+
in the response body. We'll set up our imposter like this:</p>
|
|
60
|
+
|
|
61
|
+
<step type='http'>
|
|
62
|
+
<pre><code>POST /imposters HTTP/1.1
|
|
63
|
+
Host: localhost:<%= port %>
|
|
64
|
+
Accept: application/json
|
|
65
|
+
Content-Type: application/json
|
|
66
|
+
|
|
67
|
+
{
|
|
68
|
+
"port": 5555,
|
|
69
|
+
"protocol": "http",
|
|
70
|
+
"stubs": [
|
|
71
|
+
{
|
|
72
|
+
"predicates": [{ "matches": { "path": "/accounts/\\d+" } }],
|
|
73
|
+
"responses": [
|
|
74
|
+
{
|
|
75
|
+
"is": { "body": "Hello, <strong class='highlight1'>${YOU}!</strong>" },
|
|
76
|
+
"behaviors": [
|
|
77
|
+
{ "shellTransform": "node <%= process.cwd() %>/addName.js" }
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
</code></pre>
|
|
85
|
+
</step>
|
|
86
|
+
|
|
87
|
+
<p>In this example, we're shelling out to a node.js application, which isn't much different than
|
|
88
|
+
using a <code>decorate</code> function. However, that's just to keep it simple; the application
|
|
89
|
+
could be written in any language.</p>
|
|
90
|
+
|
|
91
|
+
<p>Let's create the <code>addName.js</code> file..</p>
|
|
92
|
+
|
|
93
|
+
<step type='file' filename='<%= process.cwd() %>/addName.js'>
|
|
94
|
+
<pre><code>var request = JSON.parse(process.env.MB_REQUEST),
|
|
95
|
+
response = JSON.parse(process.env.MB_RESPONSE),
|
|
96
|
+
requestId = request.path.replace('/accounts/', ''),
|
|
97
|
+
fs = require('fs'),
|
|
98
|
+
mappings = fs.readFileSync('<%= process.cwd() %>/names.txt', { encoding: 'utf8' }),
|
|
99
|
+
lines = mappings.split(/\r?\n/);
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
102
|
+
var fields = lines[i].split('|'),
|
|
103
|
+
id = fields[0],
|
|
104
|
+
name = fields[1];
|
|
105
|
+
|
|
106
|
+
if (requestId === id) {
|
|
107
|
+
<strong class='highlight1'>response.body = response.body.replace('${YOU}', name);</strong>
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(JSON.stringify(response));</code></pre>
|
|
112
|
+
</step>
|
|
113
|
+
|
|
114
|
+
<p>Now we can test it out:</p>
|
|
115
|
+
|
|
116
|
+
<step type='http'>
|
|
117
|
+
<pre><code>GET /accounts/234 HTTP/1.1
|
|
118
|
+
Host: localhost:5555</code></pre>
|
|
119
|
+
|
|
120
|
+
<assertResponse>
|
|
121
|
+
<pre><code>HTTP/1.1 200 OK
|
|
122
|
+
Connection: close
|
|
123
|
+
Date: <volatile>Wed, 07 Jan 2016 21:27:14 GMT</volatile>
|
|
124
|
+
Transfer-Encoding: chunked
|
|
125
|
+
|
|
126
|
+
Hello, <strong class='highlight1'>Samwise Gamgee!</strong></code></pre>
|
|
127
|
+
</assertResponse>
|
|
128
|
+
</step>
|
|
129
|
+
|
|
130
|
+
<p>And again...</p>
|
|
131
|
+
|
|
132
|
+
<step type='http'>
|
|
133
|
+
<pre><code>GET /accounts/456 HTTP/1.1
|
|
134
|
+
Host: localhost:5555</code></pre>
|
|
135
|
+
|
|
136
|
+
<assertResponse>
|
|
137
|
+
<pre><code>HTTP/1.1 200 OK
|
|
138
|
+
Connection: close
|
|
139
|
+
Date: <volatile>Wed, 07 Jan 2016 21:27:14 GMT</volatile>
|
|
140
|
+
Transfer-Encoding: chunked
|
|
141
|
+
|
|
142
|
+
Hello, <strong class='highlight1'>Smeagol!</strong></code></pre>
|
|
143
|
+
</assertResponse>
|
|
144
|
+
</step>
|
|
145
|
+
|
|
146
|
+
<step type='http'>
|
|
147
|
+
<code class='hidden'>DELETE /imposters/5555 HTTP/1.1
|
|
148
|
+
Host: localhost:<%= port %></code>
|
|
149
|
+
</step>
|
|
150
|
+
|
|
151
|
+
<step type='file' filename='<%= process.cwd() %>/names.txt' delete='true'><code></code></step>
|
|
152
|
+
<step type='file' filename='<%= process.cwd() %>/addName.js' delete='true'><code></code></step>
|
|
153
|
+
</testScenario>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<table>
|
|
2
|
+
<tr>
|
|
3
|
+
<th>Parameter</th>
|
|
4
|
+
<th>Type</th>
|
|
5
|
+
<th>Description</th>
|
|
6
|
+
</tr>
|
|
7
|
+
<tr>
|
|
8
|
+
<td><code>wait</code></td>
|
|
9
|
+
<td>A positive integer, or a string</td>
|
|
10
|
+
<td>If a number is passed in, mountebank will wait that number of milliseconds before returning.
|
|
11
|
+
If a string is passed in, it is expected to be a parameterless JavaScript function that returns the number
|
|
12
|
+
of milliseconds to wait.
|
|
13
|
+
|
|
14
|
+
<p class='warning-icon'>The <code><a href='/docs/commandLine'>--allowInjection</a></code> command
|
|
15
|
+
line flag must be set to support passing in a JavaScript function</p>
|
|
16
|
+
</td>
|
|
17
|
+
</tr>
|
|
18
|
+
</table>
|
|
19
|
+
|
|
20
|
+
<p>The <code>wait</code> behavior is conceptually quite simple. Just pass in a number of milliseconds
|
|
21
|
+
to wait into the behavior:</p>
|
|
22
|
+
|
|
23
|
+
<testScenario name='wait'>
|
|
24
|
+
<step type='http'>
|
|
25
|
+
<pre><code>POST /imposters HTTP/1.1
|
|
26
|
+
Host: localhost:<%= port %>
|
|
27
|
+
Accept: application/json
|
|
28
|
+
Content-Type: application/json
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
"port": 4545,
|
|
32
|
+
"protocol": "http",
|
|
33
|
+
"stubs": [
|
|
34
|
+
{
|
|
35
|
+
"responses": [
|
|
36
|
+
{
|
|
37
|
+
"is": { "body": "This took at least half a second to send" },
|
|
38
|
+
"behaviors": [
|
|
39
|
+
{ "wait": 500 }
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}</code></pre>
|
|
46
|
+
</step>
|
|
47
|
+
|
|
48
|
+
<p>Now we can call the imposter. But have some patience, it'll take the better portion of a second
|
|
49
|
+
before you'll get your response...</p>
|
|
50
|
+
|
|
51
|
+
<step type='http'>
|
|
52
|
+
<pre><code>GET / HTTP/1.1
|
|
53
|
+
Host: localhost:4545</code></pre>
|
|
54
|
+
|
|
55
|
+
<assertResponse>
|
|
56
|
+
<pre><code>HTTP/1.1 200 OK
|
|
57
|
+
Connection: close
|
|
58
|
+
Date: <volatile>Thu, 01 Jan 2015 02:30:31 GMT</volatile>
|
|
59
|
+
Transfer-Encoding: chunked
|
|
60
|
+
|
|
61
|
+
This took at least half a second to send</code></pre>
|
|
62
|
+
</assertResponse>
|
|
63
|
+
</step>
|
|
64
|
+
|
|
65
|
+
<step type='http'>
|
|
66
|
+
<code class='hidden'>DELETE /imposters/4545 HTTP/1.1
|
|
67
|
+
Host: localhost:<%= port %></code>
|
|
68
|
+
</step>
|
|
69
|
+
|
|
70
|
+
<p>If a more advanced wait strategy is needed, you can also specify a function body as the wait variable.
|
|
71
|
+
As long as the function returns a number, mountebank can be configured to wait a variable
|
|
72
|
+
amount of time.</p>
|
|
73
|
+
|
|
74
|
+
<p class='warning-icon'>The <code><a href='/docs/commandLine'>--allowInjection</a></code> command
|
|
75
|
+
line flag must be set to support using a function to determine the wait time.</p>
|
|
76
|
+
|
|
77
|
+
<step type='http'>
|
|
78
|
+
<pre><code>POST /imposters HTTP/1.1
|
|
79
|
+
Host: localhost:<%= port %>
|
|
80
|
+
Accept: application/json
|
|
81
|
+
Content-Type: application/json
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
"port": 4545,
|
|
85
|
+
"protocol": "http",
|
|
86
|
+
"stubs": [
|
|
87
|
+
{
|
|
88
|
+
"responses": [
|
|
89
|
+
{
|
|
90
|
+
"is": { "body": "This took at least 100 to 1000 ms to send" },
|
|
91
|
+
"behaviors": [
|
|
92
|
+
{ "wait": "function() { return Math.floor(Math.random() * 901) + 100; }" }
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}</code></pre>
|
|
99
|
+
</step>
|
|
100
|
+
|
|
101
|
+
<p>The result of the function determines the wait time:</p>
|
|
102
|
+
|
|
103
|
+
<step type='http'>
|
|
104
|
+
<pre><code>GET / HTTP/1.1
|
|
105
|
+
Host: localhost:4545</code></pre>
|
|
106
|
+
|
|
107
|
+
<assertResponse>
|
|
108
|
+
<pre><code>HTTP/1.1 200 OK
|
|
109
|
+
Connection: close
|
|
110
|
+
Date: <volatile>Thu, 01 Jan 2015 02:30:31 GMT</volatile>
|
|
111
|
+
Transfer-Encoding: chunked
|
|
112
|
+
|
|
113
|
+
This took at least 100 to 1000 ms to send</code></pre>
|
|
114
|
+
</assertResponse>
|
|
115
|
+
</step>
|
|
116
|
+
|
|
117
|
+
<step type='http'>
|
|
118
|
+
<code class='hidden'>DELETE /imposters/4545 HTTP/1.1
|
|
119
|
+
Host: localhost:<%= port %></code>
|
|
120
|
+
</step>
|
|
121
|
+
</testScenario>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<%
|
|
2
|
+
title = 'behaviors'
|
|
3
|
+
description = 'Additional behaviors supported by all mountebank responses'
|
|
4
|
+
%>
|
|
5
|
+
|
|
6
|
+
<%- include('../../_header') -%>
|
|
7
|
+
|
|
8
|
+
<h1>Stub Behaviors</h1>
|
|
9
|
+
|
|
10
|
+
<p>You can alter the response created by adding to the <code>behaviors</code> array, which acts as a
|
|
11
|
+
middleware pipeline of transformations to the response. At the moment, mountebank accepts
|
|
12
|
+
the following behaviors:</p>
|
|
13
|
+
|
|
14
|
+
<table id='behaviors'>
|
|
15
|
+
<tr>
|
|
16
|
+
<th>Behavior</th>
|
|
17
|
+
<th>Description</th>
|
|
18
|
+
</tr>
|
|
19
|
+
<tr>
|
|
20
|
+
<td><code>wait</code></td>
|
|
21
|
+
<td>Adds latency to a response by waiting a specified number of milliseconds before sending
|
|
22
|
+
the response.
|
|
23
|
+
|
|
24
|
+
<p class='info-icon'>Tip: Setting the <a href='/docs/api/proxies'><code>addWaitBehavior</code></a>
|
|
25
|
+
flag on proxies will automatically add this behavior with the actual time it took to call the
|
|
26
|
+
downstream service</p>
|
|
27
|
+
</td>
|
|
28
|
+
</tr>
|
|
29
|
+
<tr>
|
|
30
|
+
<td><code>copy</code></td>
|
|
31
|
+
<td>Copies one or more values from request fields into the response. You can tokenize the response
|
|
32
|
+
and select values from request fields using regular expressions, xpath, or jsonpath.</td>
|
|
33
|
+
</tr>
|
|
34
|
+
<tr>
|
|
35
|
+
<td><code>lookup</code></td>
|
|
36
|
+
<td>Queries an external data source for data based on a key selected from the request. Like the
|
|
37
|
+
<code>copy</code> behavior, you can tokenize the response and select the key from the request
|
|
38
|
+
using regular expressions, xpath, or jsonpath.</td>
|
|
39
|
+
</tr>
|
|
40
|
+
<tr>
|
|
41
|
+
<td><code>decorate</code></td>
|
|
42
|
+
<td>Post-processes the response using JavaScript injection before sending it. Post-processing opens
|
|
43
|
+
up a world of opportunities - you can use a <code>decorate</code> behavior to add data to a proxied
|
|
44
|
+
response or substitute data from the request into the response, for example. The value passed into
|
|
45
|
+
the <code>decorate</code> behavior is a JavaScript function that can take up to three values: the
|
|
46
|
+
request, the response, and a logger. You can either mutate the response passed in (and return nothing),
|
|
47
|
+
or return an altogether new response.
|
|
48
|
+
|
|
49
|
+
<p class='info-icon'>Tip: Setting the <a href='/docs/api/proxies'><code>addDecorateBehavior</code></a>
|
|
50
|
+
flag on proxies will automatically add this function as decorate behavior on the generated responses</p>
|
|
51
|
+
|
|
52
|
+
<p class='warning-icon'>The <code><a href='/docs/commandLine'>--allowInjection</a></code> command
|
|
53
|
+
line flag must be set to support this behavior</p>
|
|
54
|
+
</td>
|
|
55
|
+
</tr>
|
|
56
|
+
<tr>
|
|
57
|
+
<td><code>shellTransform</code></td>
|
|
58
|
+
<td>Like <code>decorate</code>, a <code>shellTransform</code> post-processes the response, but
|
|
59
|
+
instead of using JavaScript injection, it shells out to another application. That application
|
|
60
|
+
will get two command line parameters representing the request JSON and the response JSON, and
|
|
61
|
+
should print to <code>stdout</code> the transformed response JSON.
|
|
62
|
+
|
|
63
|
+
<p class='warning-icon'>The <code><a href='/docs/commandLine'>--allowInjection</a></code> command
|
|
64
|
+
line flag must be set to support this behavior.</p>
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
</table>
|
|
68
|
+
|
|
69
|
+
<p>Multiple behaviors can be added to a response, and they will be executed in array order. While
|
|
70
|
+
each object in the array may contain only one type of behavior, you are free to repeat any
|
|
71
|
+
behavior as many times as you want. For example, take a look at the following response:</p>
|
|
72
|
+
|
|
73
|
+
<pre><code>{
|
|
74
|
+
"is": " { ... },
|
|
75
|
+
"behaviors": [
|
|
76
|
+
{ "copy": { ... } },
|
|
77
|
+
{ "decorate": "..." },
|
|
78
|
+
{ "lookup": "..." },
|
|
79
|
+
{ "shellTransform": "..." },
|
|
80
|
+
{ "decorate": "..." },
|
|
81
|
+
{ "wait": 500 },
|
|
82
|
+
{ "shellTransform": "..." }
|
|
83
|
+
]
|
|
84
|
+
}</code></pre>
|
|
85
|
+
|
|
86
|
+
<p>The ability to compose multiple behaviors together gives you complete control over the
|
|
87
|
+
response transformation.</p>
|
|
88
|
+
|
|
89
|
+
<h2>Examples</h2>
|
|
90
|
+
|
|
91
|
+
<p>Select the behavior below for relevant examples, which explore each type of behavior in isolation:</p>
|
|
92
|
+
|
|
93
|
+
<section class='accordion'>
|
|
94
|
+
<div>
|
|
95
|
+
<a class='section-toggler'
|
|
96
|
+
id='behavior-wait' name='behavior-wait' href='#behavior-wait'>
|
|
97
|
+
wait
|
|
98
|
+
</a>
|
|
99
|
+
<section>
|
|
100
|
+
<%- include('behaviors/wait') -%>
|
|
101
|
+
</section>
|
|
102
|
+
</div>
|
|
103
|
+
<div>
|
|
104
|
+
<a class='section-toggler'
|
|
105
|
+
id='behavior-copy' name='behavior-copy' href='#behavior-copy'>
|
|
106
|
+
copy
|
|
107
|
+
</a>
|
|
108
|
+
<section>
|
|
109
|
+
<%- include('behaviors/copy') -%>
|
|
110
|
+
</section>
|
|
111
|
+
</div>
|
|
112
|
+
<div>
|
|
113
|
+
<a class='section-toggler'
|
|
114
|
+
id='behavior-lookup' name='behavior-lookup' href='#behavior-lookup'>
|
|
115
|
+
lookup
|
|
116
|
+
</a>
|
|
117
|
+
<section>
|
|
118
|
+
<%- include('behaviors/lookup') -%>
|
|
119
|
+
</section>
|
|
120
|
+
</div>
|
|
121
|
+
<div>
|
|
122
|
+
<a class='section-toggler'
|
|
123
|
+
id='behavior-decorate' name='behavior-decorate' href='#behavior-decorate'>
|
|
124
|
+
decorate
|
|
125
|
+
</a>
|
|
126
|
+
<section>
|
|
127
|
+
<%- include('behaviors/decorate') -%>
|
|
128
|
+
</section>
|
|
129
|
+
</div>
|
|
130
|
+
<div>
|
|
131
|
+
<a class='section-toggler'
|
|
132
|
+
id='behavior-shellTransform' name='behavior-shellTransform' href='#behavior-shellTransform'>
|
|
133
|
+
shellTransform
|
|
134
|
+
</a>
|
|
135
|
+
<section>
|
|
136
|
+
<%- include('behaviors/shellTransform') -%>
|
|
137
|
+
</section>
|
|
138
|
+
</div>
|
|
139
|
+
</section>
|
|
140
|
+
|
|
141
|
+
<%- include('../../_footer') -%>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div id='addStub-index-description'>
|
|
2
|
+
<p>The array index to add the stub. Stubs are always evaluated in array order, and the first stub
|
|
3
|
+
whose predicates match the request will be used. If you leave this value off the request, the stub
|
|
4
|
+
will be added to the end of the array.</p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div id='addStub-stub-description'>
|
|
8
|
+
<p>The stub to add. See the imposter contract for more information.</p>
|
|
9
|
+
<p class='info-icon'>More information: <a href='/docs/api/contracts?type=imposter'>imposter contract</a></p>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<pre id='addStub-specification'><code>{
|
|
2
|
+
<span id='addStub-index'><%- indent(2) %>"index": 1,</span>
|
|
3
|
+
<span id='addStub-stub'><%- indent(2) %>"stub": {
|
|
4
|
+
"responses": [{
|
|
5
|
+
"is": {
|
|
6
|
+
"body": "Hello, world!"
|
|
7
|
+
}
|
|
8
|
+
}]
|
|
9
|
+
}</span>
|
|
10
|
+
}</code></pre>
|