@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,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
|
+
};
|