@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,553 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The functionality behind the behaviors field in the API, supporting post-processing responses
5
+ * @module
6
+ */
7
+
8
+ const os = require('os'),
9
+ fsExtra = require('fs-extra'),
10
+ childProcess = require('child_process'),
11
+ safeRegex = require('safe-regex'),
12
+ csvParse = require('csv-parse'),
13
+ buffer = require('buffer'),
14
+ prometheus = require('prom-client'),
15
+ xPath = require('./xpath'),
16
+ jsonPath = require('./jsonpath'),
17
+ helpers = require('../util/helpers.js'),
18
+ exceptions = require('../util/errors.js'),
19
+ behaviorsValidator = require('./behaviorsValidator.js'),
20
+ compatibility = require('./compatibility.js');
21
+
22
+
23
+ const metrics = {
24
+ behaviorDuration: new prometheus.Histogram({
25
+ name: 'mb_behavior_duration_seconds',
26
+ help: 'Time it takes to run all the behaviors',
27
+ buckets: [0.05, 0.1, 0.2, 0.5, 1, 3],
28
+ labelNames: ['imposter']
29
+ })
30
+ };
31
+
32
+ // The following schemas are used by both the lookup and copy behaviors and should be kept consistent
33
+ const fromSchema = {
34
+ _required: true,
35
+ _allowedTypes: {
36
+ string: {},
37
+ object: { singleKeyOnly: true }
38
+ },
39
+ _additionalContext: 'the request field to select from'
40
+ },
41
+ intoSchema = {
42
+ _required: true,
43
+ _allowedTypes: { string: {} },
44
+ _additionalContext: 'the token to replace in response fields'
45
+ },
46
+ usingSchema = {
47
+ _required: true,
48
+ _allowedTypes: { object: {} },
49
+ method: {
50
+ _required: true,
51
+ _allowedTypes: { string: { enum: ['regex', 'xpath', 'jsonpath'] } }
52
+ },
53
+ selector: {
54
+ _required: true,
55
+ _allowedTypes: { string: {} }
56
+ }
57
+ },
58
+ validations = {
59
+ wait: {
60
+ _required: true,
61
+ _allowedTypes: { string: {}, number: { nonNegativeInteger: true } }
62
+ },
63
+ copy: {
64
+ from: fromSchema,
65
+ into: intoSchema,
66
+ using: usingSchema
67
+ },
68
+ lookup: {
69
+ key: {
70
+ _required: true,
71
+ _allowedTypes: { object: {} },
72
+ from: fromSchema,
73
+ using: usingSchema
74
+ },
75
+ fromDataSource: {
76
+ _required: true,
77
+ _allowedTypes: { object: { singleKeyOnly: true, enum: ['csv'] } },
78
+ csv: {
79
+ _required: false,
80
+ _allowedTypes: { object: {} },
81
+ path: {
82
+ _required: true,
83
+ _allowedTypes: { string: {} },
84
+ _additionalContext: 'the path to the CSV file'
85
+ },
86
+ delimiter: {
87
+ _required: false,
88
+ _allowedTypes: { string: {} },
89
+ _additionalContext: 'the delimiter separator values'
90
+ },
91
+ keyColumn: {
92
+ _required: true,
93
+ _allowedTypes: { string: {} },
94
+ _additionalContext: 'the column header to select against the "key" field'
95
+ }
96
+ }
97
+ },
98
+ into: intoSchema
99
+ },
100
+ shellTransform: {
101
+ _required: true,
102
+ _allowedTypes: { string: {} },
103
+ _additionalContext: 'the path to a command line application'
104
+ },
105
+ decorate: {
106
+ _required: true,
107
+ _allowedTypes: { string: {} },
108
+ _additionalContext: 'a JavaScript function'
109
+ }
110
+ };
111
+
112
+ /**
113
+ * Validates the behavior configuration and returns all errors
114
+ * @param {Object} config - The behavior configuration
115
+ * @returns {Object} The array of errors
116
+ */
117
+ function validate (config) {
118
+ const validator = behaviorsValidator.create();
119
+ return validator.validate(config, validations);
120
+ }
121
+
122
+ /**
123
+ * Waits a specified number of milliseconds before sending the response. Due to the approximate
124
+ * nature of the timer, there is no guarantee that it will wait the given amount, but it will be close.
125
+ * @param {Object} request - The request object
126
+ * @param {Object} response - The response
127
+ * @param {number} millisecondsOrFn - The number of milliseconds to wait before returning, or a function returning milliseconds
128
+ * @param {Object} logger - The mountebank logger, useful for debugging
129
+ * @returns {Object} A promise resolving to the response
130
+ */
131
+ async function wait (request, response, millisecondsOrFn, logger) {
132
+ const fn = `(${millisecondsOrFn})()`;
133
+
134
+ let milliseconds = parseInt(millisecondsOrFn);
135
+
136
+ if (isNaN(milliseconds)) {
137
+ try {
138
+ milliseconds = eval(fn);
139
+ }
140
+ catch (error) {
141
+ logger.error('injection X=> ' + error);
142
+ logger.error(' full source: ' + JSON.stringify(fn));
143
+ return Promise.reject(exceptions.InjectionError('invalid wait injection',
144
+ { source: millisecondsOrFn, data: error.message }));
145
+ }
146
+ }
147
+
148
+ logger.debug('Waiting %s ms...', milliseconds);
149
+ return new Promise(resolve => {
150
+ setTimeout(() => resolve(response), milliseconds);
151
+ });
152
+ }
153
+
154
+ function quoteForShell (obj) {
155
+ const json = JSON.stringify(obj),
156
+ isWindows = os.platform().indexOf('win') === 0;
157
+
158
+ if (isWindows) {
159
+ // Confused? Me too. All other approaches I tried were spectacular failures
160
+ // in both 1) keeping the JSON as a single CLI arg, and 2) maintaining the inner quotes
161
+ return `"${json.replace(/"/g, '\\"')}"`;
162
+ }
163
+ else {
164
+ return `'${json}'`;
165
+ }
166
+ }
167
+
168
+ function execShell (command, request, response, logger) {
169
+ const exec = childProcess.exec,
170
+ env = helpers.clone(process.env),
171
+ maxBuffer = buffer.constants.MAX_STRING_LENGTH,
172
+ maxShellCommandLength = 2048;
173
+
174
+ logger.debug(`Shelling out to ${command}`);
175
+
176
+ // Switched to environment variables because of inconsistencies in Windows shell quoting
177
+ // Leaving the CLI args for backwards compatibility
178
+ env.MB_REQUEST = JSON.stringify(request);
179
+ env.MB_RESPONSE = JSON.stringify(response);
180
+
181
+ // Windows has a pretty low character limit to the command line. When we're in danger
182
+ // of the character limit, we'll remove the command line arguments under the assumption
183
+ // that backwards compatibility doesn't matter when it never would have worked to begin with
184
+ let fullCommand = `${command} ${quoteForShell(request)} ${quoteForShell(response)}`;
185
+ if (fullCommand.length >= maxShellCommandLength) {
186
+ fullCommand = command;
187
+ }
188
+
189
+ return new Promise((resolve, reject) => {
190
+ exec(fullCommand, { env, maxBuffer }, (error, stdout, stderr) => {
191
+ if (error) {
192
+ if (stderr) {
193
+ logger.error(stderr);
194
+ }
195
+ reject(error.message);
196
+ }
197
+ else {
198
+ logger.debug(`Shell returned '${stdout}'`);
199
+ try {
200
+ resolve(JSON.parse(stdout));
201
+ }
202
+ catch (err) {
203
+ reject(`Shell command returned invalid JSON: '${stdout}'`);
204
+ }
205
+ }
206
+ });
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Runs the response through a shell function, passing the JSON in as stdin and using
212
+ * stdout as the new response
213
+ * @param {Object} request - The request
214
+ * @param {Object} response - The response
215
+ * @param {string} command - The shell command to execute
216
+ * @param {Object} logger - The mountebank logger, useful in debugging
217
+ * @returns {Object}
218
+ */
219
+ function shellTransform (request, response, command, logger) {
220
+ return execShell(command, request, response, logger);
221
+ }
222
+
223
+ /**
224
+ * Runs the response through a post-processing function provided by the user
225
+ * @param {Object} originalRequest - The request object, in case post-processing depends on it
226
+ * @param {Object} response - The response
227
+ * @param {Function} fn - The function that performs the post-processing
228
+ * @param {Object} logger - The mountebank logger, useful in debugging
229
+ * @param {Object} imposterState - The user controlled state variable
230
+ * @returns {Object}
231
+ */
232
+ function decorate (originalRequest, response, fn, logger, imposterState) {
233
+ const config = {
234
+ request: helpers.clone(originalRequest),
235
+ response,
236
+ logger,
237
+ state: imposterState
238
+ },
239
+ injected = `(${fn})(config, response, logger);`; // backwards compatibility
240
+
241
+ compatibility.downcastInjectionConfig(config);
242
+
243
+ try {
244
+ // Support functions that mutate response in place and those
245
+ // that return a new response
246
+ let result = eval(injected);
247
+ if (!result) {
248
+ result = response;
249
+ }
250
+ return Promise.resolve(result);
251
+ }
252
+ catch (error) {
253
+ logger.error('injection X=> ' + error);
254
+ logger.error(' full source: ' + JSON.stringify(injected));
255
+ logger.error(' config: ' + JSON.stringify(config));
256
+ return Promise.reject(exceptions.InjectionError('invalid decorator injection', { source: injected, data: error.message }));
257
+ }
258
+ }
259
+
260
+ function getKeyIgnoringCase (obj, expectedKey) {
261
+ return Object.keys(obj).find(key => {
262
+ if (key.toLowerCase() === expectedKey.toLowerCase()) {
263
+ return key;
264
+ }
265
+ else {
266
+ return undefined;
267
+ }
268
+ });
269
+ }
270
+
271
+ function getFrom (obj, from) {
272
+ const isObject = helpers.isObject;
273
+
274
+ if (typeof obj === 'undefined') {
275
+ return undefined;
276
+ }
277
+ else if (isObject(from)) {
278
+ const keys = Object.keys(from);
279
+ return getFrom(obj[keys[0]], from[keys[0]]);
280
+ }
281
+ else {
282
+ const result = obj[getKeyIgnoringCase(obj, from)];
283
+
284
+ // Some request fields, like query parameters, can be multi-valued
285
+ if (Array.isArray(result)) {
286
+ return result[0];
287
+ }
288
+ else {
289
+ return result;
290
+ }
291
+ }
292
+ }
293
+
294
+ function regexFlags (options) {
295
+ let result = '';
296
+ if (options && options.ignoreCase) {
297
+ result += 'i';
298
+ }
299
+ if (options && options.multiline) {
300
+ result += 'm';
301
+ }
302
+ return result;
303
+ }
304
+
305
+ function getMatches (selectionFn, selector, logger) {
306
+ const matches = selectionFn();
307
+
308
+ if (matches && matches.length > 0) {
309
+ return matches;
310
+ }
311
+ else {
312
+ logger.debug('No match for "%s"', selector);
313
+ return [];
314
+ }
315
+ }
316
+
317
+ function regexValue (from, config, logger) {
318
+ const regex = new RegExp(config.using.selector, regexFlags(config.using.options)),
319
+ selectionFn = () => regex.exec(from);
320
+
321
+ if (!safeRegex(regex)) {
322
+ logger.warn(`If mountebank becomes unresponsive, it is because of this unsafe regular expression: ${config.using.selector}`);
323
+ }
324
+ return getMatches(selectionFn, regex, logger);
325
+ }
326
+
327
+ function xpathValue (from, config, logger) {
328
+ const selectionFn = () => {
329
+ return xPath.select(config.using.selector, config.using.ns, from, logger);
330
+ };
331
+ return getMatches(selectionFn, config.using.selector, logger);
332
+ }
333
+
334
+ function jsonpathValue (from, config, logger) {
335
+ const selectionFn = () => {
336
+ return jsonPath.select(config.using.selector, from, logger);
337
+ };
338
+ return getMatches(selectionFn, config.using.selector, logger);
339
+ }
340
+
341
+ function globalStringReplace (str, substring, newSubstring, logger) {
342
+ if (substring !== newSubstring) {
343
+ logger.debug('Replacing %s with %s', JSON.stringify(substring), JSON.stringify(newSubstring));
344
+ return str.split(substring).join(newSubstring);
345
+ }
346
+ else {
347
+ return str;
348
+ }
349
+ }
350
+
351
+ function globalObjectReplace (obj, replacer) {
352
+ const isObject = helpers.isObject,
353
+ renames = {};
354
+
355
+ Object.keys(obj).forEach(key => {
356
+ if (typeof obj[key] === 'string') {
357
+ obj[key] = replacer(obj[key]);
358
+ }
359
+ else if (isObject(obj[key])) {
360
+ globalObjectReplace(obj[key], replacer);
361
+ }
362
+ var newKey = replacer(key);
363
+ if (newKey !== key) {
364
+ renames[key] = newKey;
365
+ }
366
+ });
367
+ Object.keys(renames).forEach(key => {
368
+ obj[renames[key]] = obj[key];
369
+ delete obj[key];
370
+ });
371
+ }
372
+
373
+ function replaceArrayValuesIn (response, token, values, logger) {
374
+ const replacer = field => {
375
+ values.forEach(function (replacement, index) {
376
+ // replace ${TOKEN}[1] with indexed element
377
+ const indexedToken = `${token}[${index}]`;
378
+ field = globalStringReplace(field, indexedToken, replacement, logger);
379
+ });
380
+ if (values.length > 0) {
381
+ // replace ${TOKEN} with first element
382
+ field = globalStringReplace(field, token, values[0], logger);
383
+ }
384
+ return field;
385
+ };
386
+
387
+ globalObjectReplace(response, replacer);
388
+ }
389
+
390
+ /**
391
+ * Copies a value from the request and replaces response tokens with that value
392
+ * @param {Object} originalRequest - The request object, in case post-processing depends on it
393
+ * @param {Object} response - The response
394
+ * @param {Function} copyConfig - The config to copy
395
+ * @param {Object} logger - The mountebank logger, useful in debugging
396
+ * @returns {Object}
397
+ */
398
+ function copy (originalRequest, response, copyConfig, logger) {
399
+ const from = getFrom(originalRequest, copyConfig.from),
400
+ using = copyConfig.using || {},
401
+ fnMap = { regex: regexValue, xpath: xpathValue, jsonpath: jsonpathValue },
402
+ values = fnMap[using.method](from, copyConfig, logger);
403
+
404
+ replaceArrayValuesIn(response, copyConfig.into, values, logger);
405
+ return response;
406
+ }
407
+
408
+ function containsKey (headers, keyColumn) {
409
+ const key = Object.values(headers).find(value => value === keyColumn);
410
+
411
+ return helpers.defined(key);
412
+ }
413
+
414
+ function createRowObject (headers, rowArray) {
415
+ const row = {};
416
+ rowArray.forEach(function (value, index) {
417
+ row[headers[index]] = value;
418
+ });
419
+ return row;
420
+ }
421
+
422
+ function selectRowFromCSV (csvConfig, keyValue, logger) {
423
+ const delimiter = csvConfig.delimiter || ',',
424
+ inputStream = fsExtra.createReadStream(csvConfig.path),
425
+ parser = csvParse.parse({ delimiter: delimiter }),
426
+ pipe = inputStream.pipe(parser);
427
+ let headers;
428
+
429
+ return new Promise(resolve => {
430
+ inputStream.on('error', e => {
431
+ logger.error('Cannot read ' + csvConfig.path + ': ' + e);
432
+ resolve({});
433
+ });
434
+
435
+ pipe.on('data', function (rowArray) {
436
+ if (!helpers.defined(headers)) {
437
+ headers = rowArray;
438
+ const keyOnHeader = containsKey(headers, csvConfig.keyColumn);
439
+ if (!keyOnHeader) {
440
+ logger.error('CSV headers "' + headers + '" with delimiter "' + delimiter + '" does not contain keyColumn:"' + csvConfig.keyColumn + '"');
441
+ resolve({});
442
+ }
443
+ }
444
+ else {
445
+ const row = createRowObject(headers, rowArray);
446
+ if (helpers.defined(row[csvConfig.keyColumn]) && row[csvConfig.keyColumn].localeCompare(keyValue) === 0) {
447
+ resolve(row);
448
+ }
449
+ }
450
+ });
451
+
452
+ pipe.on('error', e => {
453
+ logger.debug('Error: ' + e);
454
+ resolve({});
455
+ });
456
+
457
+ pipe.on('end', () => {
458
+ resolve({});
459
+ });
460
+ });
461
+ }
462
+
463
+ function lookupRow (lookupConfig, originalRequest, logger) {
464
+ const from = getFrom(originalRequest, lookupConfig.key.from),
465
+ fnMap = { regex: regexValue, xpath: xpathValue, jsonpath: jsonpathValue },
466
+ keyValues = fnMap[lookupConfig.key.using.method](from, lookupConfig.key, logger),
467
+ index = lookupConfig.key.index || 0;
468
+
469
+ if (lookupConfig.fromDataSource.csv) {
470
+ return selectRowFromCSV(lookupConfig.fromDataSource.csv, keyValues[index], logger);
471
+ }
472
+ else {
473
+ return Promise.resolve({});
474
+ }
475
+ }
476
+
477
+ function replaceObjectValuesIn (response, token, values, logger) {
478
+ const replacer = field => {
479
+ Object.keys(values).forEach(key => {
480
+ // replace ${TOKEN}["key"] and ${TOKEN}['key'] and ${TOKEN}[key]
481
+ ['"', "'", ''].forEach(function (quoteChar) {
482
+ const quoted = `${token}[${quoteChar}${key}${quoteChar}]`;
483
+ field = globalStringReplace(field, quoted, values[key], logger);
484
+ });
485
+ });
486
+ return field;
487
+ };
488
+
489
+ globalObjectReplace(response, replacer);
490
+ }
491
+
492
+
493
+ /**
494
+ * Looks up request values from a data source and replaces response tokens with the resulting data
495
+ * @param {Object} originalRequest - The request object
496
+ * @param {Object} response - The response
497
+ * @param {Function} lookupConfig - The lookup configurations
498
+ * @param {Object} logger - The mountebank logger, useful in debugging
499
+ * @returns {Object}
500
+ */
501
+ async function lookup (originalRequest, response, lookupConfig, logger) {
502
+ try {
503
+ const row = await lookupRow(lookupConfig, originalRequest, logger);
504
+ replaceObjectValuesIn(response, lookupConfig.into, row, logger);
505
+ }
506
+ catch (error) {
507
+ logger.error(error);
508
+ }
509
+ return response;
510
+ }
511
+
512
+ /**
513
+ * The entry point to execute all behaviors provided in the API
514
+ * @param {Object} request - The request object
515
+ * @param {Object} response - The response generated from the stubs
516
+ * @param {Object} behaviors - The behaviors specified in the API
517
+ * @param {Object} logger - The mountebank logger, useful for debugging
518
+ * @param {Object} imposterState - the user-controlled state variable
519
+ * @returns {Object}
520
+ */
521
+ async function execute (request, response, behaviors, logger, imposterState) {
522
+ const fnMap = {
523
+ wait: wait,
524
+ copy: copy,
525
+ lookup: lookup,
526
+ shellTransform: shellTransform,
527
+ decorate: decorate
528
+ };
529
+ let result = Promise.resolve(response);
530
+
531
+ if (!behaviors || behaviors.length === 0 || request.isDryRun) {
532
+ return result;
533
+ }
534
+
535
+ logger.debug('using stub response behavior ' + JSON.stringify(behaviors));
536
+ behaviors.forEach(behavior => {
537
+ Object.keys(behavior).forEach(key => {
538
+ if (fnMap[key]) {
539
+ result = result.then(newResponse => fnMap[key](request, newResponse, behavior[key], logger, imposterState));
540
+ }
541
+ });
542
+ });
543
+
544
+ const observeBehaviorDuration = metrics.behaviorDuration.startTimer(),
545
+ transformed = await result;
546
+ observeBehaviorDuration({ imposter: logger.scopePrefix });
547
+ return transformed;
548
+ }
549
+
550
+ module.exports = {
551
+ validate,
552
+ execute
553
+ };