@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,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const exceptions = require('../util/errors.js'),
|
|
4
|
+
helpers = require('../util/helpers.js');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The module that does validation of behavior configuration
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates the validator
|
|
13
|
+
* @returns {{validate: validate}}
|
|
14
|
+
*/
|
|
15
|
+
function create () {
|
|
16
|
+
function hasExactlyOneKey (obj) {
|
|
17
|
+
const keys = Object.keys(obj);
|
|
18
|
+
return keys.length === 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function navigate (config, path) {
|
|
22
|
+
if (path === '') {
|
|
23
|
+
return config;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
return path.split('.').reduce(function (field, fieldName) {
|
|
27
|
+
return field[fieldName];
|
|
28
|
+
}, config);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function typeErrorMessageFor (allowedTypes, additionalContext) {
|
|
33
|
+
const spellings = { number: 'a', object: 'an', string: 'a' };
|
|
34
|
+
let message = `must be ${spellings[allowedTypes[0]]} ${allowedTypes[0]}`;
|
|
35
|
+
|
|
36
|
+
for (let i = 1; i < allowedTypes.length; i += 1) {
|
|
37
|
+
message += ` or ${spellings[allowedTypes[i]]} ${allowedTypes[i]}`;
|
|
38
|
+
}
|
|
39
|
+
if (additionalContext) {
|
|
40
|
+
message += `, representing ${additionalContext}`;
|
|
41
|
+
}
|
|
42
|
+
return message;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pathFor (pathPrefix, fieldName) {
|
|
46
|
+
if (pathPrefix === '') {
|
|
47
|
+
return fieldName;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return `${pathPrefix}.${fieldName}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function nonMetadata (fieldName) {
|
|
55
|
+
return fieldName.indexOf('_') !== 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isTopLevelSpec (spec) {
|
|
59
|
+
// True of copy and lookup behaviors that define the metadata below the top level keys
|
|
60
|
+
return helpers.isObject(spec)
|
|
61
|
+
&& Object.keys(spec).filter(nonMetadata).length === Object.keys(spec).length;
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
function enumFieldFor (field) {
|
|
65
|
+
const isObject = helpers.isObject;
|
|
66
|
+
|
|
67
|
+
// Can be the string value or the object key
|
|
68
|
+
if (isObject(field) && Object.keys(field).length > 0) {
|
|
69
|
+
return Object.keys(field)[0];
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
return field;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function matchesEnum (field, enumSpec) {
|
|
77
|
+
return enumSpec.indexOf(enumFieldFor(field)) >= 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function addMissingFieldError (fieldSpec, path, addErrorFn) {
|
|
81
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
82
|
+
if (fieldSpec._required) {
|
|
83
|
+
addErrorFn(path, 'required');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function addTypeErrors (fieldSpec, path, field, config, addErrorFn) {
|
|
88
|
+
/* eslint complexity: 0 */
|
|
89
|
+
const fieldType = typeof field,
|
|
90
|
+
allowedTypes = Object.keys(fieldSpec._allowedTypes), // eslint-disable-line no-underscore-dangle
|
|
91
|
+
typeSpec = fieldSpec._allowedTypes[fieldType]; // eslint-disable-line no-underscore-dangle
|
|
92
|
+
|
|
93
|
+
if (!helpers.defined(typeSpec)) {
|
|
94
|
+
addErrorFn(path, typeErrorMessageFor(allowedTypes, fieldSpec._additionalContext)); // eslint-disable-line no-underscore-dangle
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
if (typeSpec.singleKeyOnly && !hasExactlyOneKey(field)) {
|
|
98
|
+
addErrorFn(path, 'must have exactly one key');
|
|
99
|
+
}
|
|
100
|
+
else if (typeSpec.enum && !matchesEnum(field, typeSpec.enum)) {
|
|
101
|
+
addErrorFn(path, `must be one of [${typeSpec.enum.join(', ')}]`);
|
|
102
|
+
}
|
|
103
|
+
else if (typeSpec.nonNegativeInteger && field < 0) {
|
|
104
|
+
addErrorFn(path, 'must be an integer greater than or equal to 0');
|
|
105
|
+
}
|
|
106
|
+
else if (typeSpec.positiveInteger && field <= 0) {
|
|
107
|
+
addErrorFn(path, 'must be an integer greater than 0');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
addErrorsFor(config, path, fieldSpec, addErrorFn);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function addErrorsFor (config, pathPrefix, spec, addErrorFn) {
|
|
115
|
+
Object.keys(spec).filter(nonMetadata).forEach(fieldName => {
|
|
116
|
+
const fieldSpec = spec[fieldName],
|
|
117
|
+
path = pathFor(pathPrefix, fieldName),
|
|
118
|
+
field = navigate(config, path);
|
|
119
|
+
|
|
120
|
+
if (!helpers.defined(field)) {
|
|
121
|
+
addMissingFieldError(fieldSpec, path, addErrorFn);
|
|
122
|
+
}
|
|
123
|
+
else if (isTopLevelSpec(fieldSpec)) {
|
|
124
|
+
// Recurse but reset pathPrefix so error message is cleaner
|
|
125
|
+
// e.g. 'copy behavior "from" field required' instead of 'copy behavior "copy.from" field required'
|
|
126
|
+
addErrorsFor(field, '', fieldSpec, addErrorFn);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
addTypeErrors(fieldSpec, path, field, config, addErrorFn);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Validates the behavior configuration and returns all errors
|
|
136
|
+
* @memberOf module:models/behaviorsValidator#
|
|
137
|
+
* @param {Object} behaviors - The behaviors list
|
|
138
|
+
* @param {Object} validationSpec - the specification to validate against
|
|
139
|
+
* @returns {Object} The array of errors
|
|
140
|
+
*/
|
|
141
|
+
function validate (behaviors, validationSpec) {
|
|
142
|
+
const errors = [];
|
|
143
|
+
|
|
144
|
+
(behaviors || []).forEach(config => {
|
|
145
|
+
const validBehaviors = [],
|
|
146
|
+
unrecognizedKeys = [];
|
|
147
|
+
|
|
148
|
+
Object.keys(config).forEach(key => {
|
|
149
|
+
const addError = function (field, message, subConfig) {
|
|
150
|
+
errors.push(exceptions.ValidationError(`${key} behavior "${field}" field ${message}`,
|
|
151
|
+
{ source: subConfig || config }));
|
|
152
|
+
},
|
|
153
|
+
spec = {};
|
|
154
|
+
|
|
155
|
+
if (validationSpec[key]) {
|
|
156
|
+
validBehaviors.push(key);
|
|
157
|
+
spec[key] = validationSpec[key];
|
|
158
|
+
addErrorsFor(config, '', spec, addError);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
unrecognizedKeys.push({ key: key, source: config });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Allow adding additional custom fields to valid behaviors but ensure there is a valid behavior
|
|
166
|
+
if (validBehaviors.length === 0 && unrecognizedKeys.length > 0) {
|
|
167
|
+
errors.push(exceptions.ValidationError(`Unrecognized behavior: "${unrecognizedKeys[0].key}"`,
|
|
168
|
+
{ source: unrecognizedKeys[0].source }));
|
|
169
|
+
}
|
|
170
|
+
if (validBehaviors.length > 1) {
|
|
171
|
+
errors.push(exceptions.ValidationError('Each behavior object must have only one behavior type',
|
|
172
|
+
{ source: config }));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return errors;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
validate
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
create
|
|
186
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const helpers = require('../util/helpers');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* mountebank aims to evolve without requiring users to have to worry about versioning,
|
|
7
|
+
* so breaking changes to the API are A Big Deal. This module exists to support transforming
|
|
8
|
+
* older versions of the API to a newer format, so that most of the code can assume the
|
|
9
|
+
* new format, but users who still use the old format don't need to migrate.
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The original shellTransform only accepted one command
|
|
15
|
+
* The new syntax expects an array, creating a shell pipeline
|
|
16
|
+
* @param {Object} request - the request to upcast
|
|
17
|
+
*/
|
|
18
|
+
function upcastShellTransformToArray (request) {
|
|
19
|
+
(request.stubs || []).forEach(stub => {
|
|
20
|
+
(stub.responses || []).forEach(response => {
|
|
21
|
+
if (response._behaviors && response._behaviors.shellTransform &&
|
|
22
|
+
typeof response._behaviors.shellTransform === 'string') {
|
|
23
|
+
response._behaviors.shellTransform = [response._behaviors.shellTransform];
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function canUpcastBehaviors (response) {
|
|
30
|
+
const isObject = helpers.isObject;
|
|
31
|
+
|
|
32
|
+
return typeof response.behaviors === 'undefined'
|
|
33
|
+
&& typeof response.repeat === 'undefined'
|
|
34
|
+
&& isObject(response._behaviors);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function upcastResponseBehaviors (response) {
|
|
38
|
+
const behaviors = [],
|
|
39
|
+
add = (key, value) => {
|
|
40
|
+
const obj = {};
|
|
41
|
+
obj[key] = value;
|
|
42
|
+
behaviors.push(obj);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// This was the old line of code that executed the behaviors, which defined the order:
|
|
46
|
+
// return combinators.compose(decorateFn, shellTransformFn, copyFn, lookupFn, waitFn, Q)(response);
|
|
47
|
+
['wait', 'lookup', 'copy', 'shellTransform', 'decorate'].forEach(key => {
|
|
48
|
+
if (typeof response._behaviors[key] !== 'undefined') {
|
|
49
|
+
if (Array.isArray(response._behaviors[key])) {
|
|
50
|
+
response._behaviors[key].forEach(element => add(key, element));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
add(key, response._behaviors[key]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// The repeat behavior can't be stacked multiple times and sequence of execution doesn't matter,
|
|
59
|
+
// so putting it in the array risks confusion and additional error checking. Pulling it outside
|
|
60
|
+
// the array clearly indicates it only applies once to the entire response.
|
|
61
|
+
if (typeof response._behaviors.repeat !== 'undefined') {
|
|
62
|
+
response.repeat = response._behaviors.repeat;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
response.behaviors = behaviors;
|
|
66
|
+
delete response._behaviors;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The original _behaviors took an object with undefined ordering
|
|
71
|
+
* The new syntax expects an array, creating a behaviors pipeline
|
|
72
|
+
* @param {Object} request - the request to upcast
|
|
73
|
+
*/
|
|
74
|
+
function upcastBehaviorsToArray (request) {
|
|
75
|
+
(request.stubs || []).forEach(stub => {
|
|
76
|
+
(stub.responses || [])
|
|
77
|
+
.filter(canUpcastBehaviors)
|
|
78
|
+
.forEach(upcastResponseBehaviors);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The original tcp proxy.to was an object with a host and port field
|
|
84
|
+
* The new syntax uses a tcp:// url for symmetry with http/s
|
|
85
|
+
* @param {Object} request - the request to upcast
|
|
86
|
+
*/
|
|
87
|
+
function upcastTcpProxyDestinationToUrl (request) {
|
|
88
|
+
if (request.protocol !== 'tcp') {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const isObject = helpers.isObject;
|
|
93
|
+
|
|
94
|
+
(request.stubs || []).forEach(stub => {
|
|
95
|
+
(stub.responses || []).forEach(response => {
|
|
96
|
+
const proxy = response.proxy;
|
|
97
|
+
if (proxy && isObject(proxy.to) && proxy.to.host && proxy.to.port) {
|
|
98
|
+
proxy.to = `tcp://${proxy.to.host}:${proxy.to.port}`;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Upcast the request to the current version
|
|
106
|
+
* @param {Object} request - the request to upcast
|
|
107
|
+
*/
|
|
108
|
+
function upcast (request) {
|
|
109
|
+
upcastShellTransformToArray(request);
|
|
110
|
+
upcastTcpProxyDestinationToUrl(request);
|
|
111
|
+
upcastBehaviorsToArray(request);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* While the new injection interface takes a single config object, the old
|
|
116
|
+
* interface took several parameters, starting with the request object.
|
|
117
|
+
* To make the new interface backwards compatible, we have to add all the
|
|
118
|
+
* request fields to the config object
|
|
119
|
+
* @param {Object} config - the injection parameter
|
|
120
|
+
*/
|
|
121
|
+
function downcastInjectionConfig (config) {
|
|
122
|
+
// Only possible to use older format for http/s and tcp protocols
|
|
123
|
+
if (config.request.method || config.request.data) {
|
|
124
|
+
Object.keys(config.request).forEach(key => {
|
|
125
|
+
config[key] = config.request[key];
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
upcast,
|
|
132
|
+
downcastInjectionConfig
|
|
133
|
+
};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const exceptions = require('../util/errors.js'),
|
|
4
|
+
helpers = require('../util/helpers.js'),
|
|
5
|
+
responseResolver = require('./responseResolver.js'),
|
|
6
|
+
inMemoryImpostersRepository = require('./inMemoryImpostersRepository.js'),
|
|
7
|
+
predicates = require('./predicates.js'),
|
|
8
|
+
combinators = require('../util/combinators.js'),
|
|
9
|
+
behaviors = require('./behaviors.js');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validating a syntactically correct imposter creation statically is quite difficult.
|
|
13
|
+
* This module validates dynamically by running test requests through each predicate and each stub
|
|
14
|
+
* to see if it throws an error. A valid request is one that passes the dry run error-free.
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates the validator
|
|
20
|
+
* @param {Object} options - Configuration for the validator
|
|
21
|
+
* @param {Object} options.testRequest - The protocol-specific request used for each dry run
|
|
22
|
+
* @param {Object} options.testProxyResponse - The protocol-specific fake response from a proxy call
|
|
23
|
+
* @param {boolean} options.allowInjection - Whether JavaScript injection is allowed or not
|
|
24
|
+
* @param {function} options.additionalValidation - A function that performs protocol-specific validation
|
|
25
|
+
* @returns {Object}
|
|
26
|
+
*/
|
|
27
|
+
function create (options) {
|
|
28
|
+
function stubForResponse (originalStub, response, withPredicates) {
|
|
29
|
+
// Each dry run only validates the first response, so we
|
|
30
|
+
// explode the number of stubs to dry run each response separately
|
|
31
|
+
const clonedStub = helpers.clone(originalStub),
|
|
32
|
+
clonedResponse = helpers.clone(response);
|
|
33
|
+
clonedStub.responses = [clonedResponse];
|
|
34
|
+
|
|
35
|
+
// If the predicates don't match the test request, we won't dry run
|
|
36
|
+
// the response (although the predicates will be dry run). We remove
|
|
37
|
+
// the predicates to account for this scenario.
|
|
38
|
+
if (!withPredicates) {
|
|
39
|
+
delete clonedStub.predicates;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return clonedStub;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function reposToTestFor (stub) {
|
|
46
|
+
// Test with predicates (likely won't match) to make sure predicates don't blow up
|
|
47
|
+
// Test without predicates (always matches) to make sure response doesn't blow up
|
|
48
|
+
const stubsToValidateWithPredicates = stub.responses.map(response => stubForResponse(stub, response, true)),
|
|
49
|
+
stubsToValidateWithoutPredicates = stub.responses.map(response => stubForResponse(stub, response, false)),
|
|
50
|
+
stubsToValidate = stubsToValidateWithPredicates.concat(stubsToValidateWithoutPredicates),
|
|
51
|
+
promises = stubsToValidate.map(async stubToValidate => {
|
|
52
|
+
const stubRepository = inMemoryImpostersRepository.create().createStubsRepository();
|
|
53
|
+
await stubRepository.add(stubToValidate);
|
|
54
|
+
return stubRepository;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return Promise.all(promises);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// We call map before calling every so we make sure to call every
|
|
61
|
+
// predicate during dry run validation rather than short-circuiting
|
|
62
|
+
function trueForAll (list, predicate) {
|
|
63
|
+
return list.map(predicate).every(result => result);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function findFirstMatch (stubRepository, request, encoding, logger) {
|
|
67
|
+
const filter = stubPredicates => {
|
|
68
|
+
return trueForAll(stubPredicates,
|
|
69
|
+
predicate => predicates.evaluate(predicate, request, encoding, logger, {}));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return stubRepository.first(filter);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolverFor (stubRepository) {
|
|
76
|
+
// We can get a better test (running behaviors on proxied result) if the protocol gives
|
|
77
|
+
// us a testProxyResult
|
|
78
|
+
if (options.testProxyResponse) {
|
|
79
|
+
const dryRunProxy = { to: proxyTo => {
|
|
80
|
+
if (proxyTo === undefined) {
|
|
81
|
+
throw exceptions.ValidationError('Missing to');
|
|
82
|
+
}
|
|
83
|
+
const url = new URL(proxyTo);
|
|
84
|
+
if (url.protocol.indexOf('http') === 0 && url.pathname !== '/') {
|
|
85
|
+
throw exceptions.ValidationError(`proxy.to must not contain a path '${url.pathname}'`);
|
|
86
|
+
}
|
|
87
|
+
return Promise.resolve(options.testProxyResponse);
|
|
88
|
+
} };
|
|
89
|
+
return responseResolver.create(stubRepository, dryRunProxy);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
return responseResolver.create(stubRepository, undefined, 'URL');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function dryRunSingleRepo (stubRepository, encoding, dryRunLogger) {
|
|
97
|
+
const match = await findFirstMatch(stubRepository, options.testRequest, encoding, dryRunLogger),
|
|
98
|
+
responseConfig = await match.stub.nextResponse();
|
|
99
|
+
|
|
100
|
+
return resolverFor(stubRepository).resolve(responseConfig, options.testRequest, dryRunLogger, {});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function dryRun (stub, encoding, logger) {
|
|
104
|
+
options.testRequest = options.testRequest || {};
|
|
105
|
+
options.testRequest.isDryRun = true;
|
|
106
|
+
|
|
107
|
+
const dryRunLogger = {
|
|
108
|
+
debug: combinators.noop,
|
|
109
|
+
info: combinators.noop,
|
|
110
|
+
warn: combinators.noop,
|
|
111
|
+
error: logger.error
|
|
112
|
+
},
|
|
113
|
+
dryRunRepositories = await reposToTestFor(stub),
|
|
114
|
+
dryRuns = dryRunRepositories.map(stubRepository => dryRunSingleRepo(stubRepository, encoding, dryRunLogger));
|
|
115
|
+
|
|
116
|
+
return Promise.all(dryRuns);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function addDryRunErrors (stub, encoding, errors, logger) {
|
|
120
|
+
try {
|
|
121
|
+
await dryRun(stub, encoding, logger);
|
|
122
|
+
}
|
|
123
|
+
catch (reason) {
|
|
124
|
+
reason.source = reason.source || JSON.stringify(stub);
|
|
125
|
+
errors.push(reason);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function hasPredicateGeneratorInjection (response) {
|
|
130
|
+
return response.proxy && response.proxy.predicateGenerators &&
|
|
131
|
+
response.proxy.predicateGenerators.some(generator => generator.inject);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hasBehavior (response, type, valueFilter) {
|
|
135
|
+
if (typeof valueFilter === 'undefined') {
|
|
136
|
+
valueFilter = () => true;
|
|
137
|
+
}
|
|
138
|
+
return (response.behaviors || []).some(behavior => {
|
|
139
|
+
return typeof behavior[type] !== 'undefined' && valueFilter(behavior[type]);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function hasStubInjection (stub) {
|
|
144
|
+
const hasResponseInjections = stub.responses.some(response => {
|
|
145
|
+
const hasDecorator = hasBehavior(response, 'decorate'),
|
|
146
|
+
hasWaitFunction = hasBehavior(response, 'wait', value => typeof value === 'string');
|
|
147
|
+
|
|
148
|
+
return response.inject || hasDecorator || hasWaitFunction || hasPredicateGeneratorInjection(response);
|
|
149
|
+
}),
|
|
150
|
+
hasPredicateInjections = Object.keys(stub.predicates || {}).some(predicate => stub.predicates[predicate].inject),
|
|
151
|
+
hasAddDecorateBehaviorInProxy = stub.responses.some(response => response.proxy && response.proxy.addDecorateBehavior);
|
|
152
|
+
return hasResponseInjections || hasPredicateInjections || hasAddDecorateBehaviorInProxy;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function hasShellExecution (stub) {
|
|
156
|
+
return stub.responses.some(response => hasBehavior(response, 'shellTransform'));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function addStubInjectionErrors (stub, errors) {
|
|
160
|
+
if (options.allowInjection) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (hasStubInjection(stub)) {
|
|
165
|
+
errors.push(exceptions.InjectionError(
|
|
166
|
+
'JavaScript injection is not allowed unless mb is run with the --allowInjection flag', { source: stub }));
|
|
167
|
+
}
|
|
168
|
+
if (hasShellExecution(stub)) {
|
|
169
|
+
errors.push(exceptions.InjectionError(
|
|
170
|
+
'Shell execution is not allowed unless mb is run with the --allowInjection flag', { source: stub }));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function addAllTo (values, additionalValues) {
|
|
175
|
+
additionalValues.forEach(value => {
|
|
176
|
+
values.push(value);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function addRepeatErrorsTo (errors, response) {
|
|
181
|
+
const repeat = response.repeat,
|
|
182
|
+
type = typeof repeat,
|
|
183
|
+
error = exceptions.ValidationError('"repeat" field must be an integer greater than 0', {
|
|
184
|
+
source: response
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (['undefined', 'number', 'string'].indexOf(type) < 0) {
|
|
188
|
+
errors.push(error);
|
|
189
|
+
}
|
|
190
|
+
if ((type === 'string' && parseInt(repeat) <= 0) || (type === 'number' && repeat <= 0)) {
|
|
191
|
+
errors.push(error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function addBehaviorErrors (stub, errors) {
|
|
196
|
+
stub.responses.forEach(response => {
|
|
197
|
+
addAllTo(errors, behaviors.validate(response.behaviors));
|
|
198
|
+
addRepeatErrorsTo(errors, response);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function errorsForStub (stub, encoding, logger) {
|
|
203
|
+
const errors = [];
|
|
204
|
+
|
|
205
|
+
if (!Array.isArray(stub.responses) || stub.responses.length === 0) {
|
|
206
|
+
errors.push(exceptions.ValidationError("'responses' must be a non-empty array", {
|
|
207
|
+
source: stub
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
addStubInjectionErrors(stub, errors);
|
|
212
|
+
addBehaviorErrors(stub, errors);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (errors.length === 0) {
|
|
216
|
+
// no sense in dry-running if there are already problems;
|
|
217
|
+
// it will just add noise to the errors
|
|
218
|
+
await addDryRunErrors(stub, encoding, errors, logger);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return errors;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function errorsForRequest (request) {
|
|
225
|
+
const errors = [],
|
|
226
|
+
hasRequestInjection = request.endOfRequestResolver && request.endOfRequestResolver.inject;
|
|
227
|
+
|
|
228
|
+
if (!options.allowInjection && hasRequestInjection) {
|
|
229
|
+
errors.push(exceptions.InjectionError(
|
|
230
|
+
'JavaScript injection is not allowed unless mb is run with the --allowInjection flag',
|
|
231
|
+
{ source: request.endOfRequestResolver }));
|
|
232
|
+
}
|
|
233
|
+
return errors;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Validates that the imposter creation is syntactically valid
|
|
238
|
+
* @memberOf module:models/dryRunValidator#
|
|
239
|
+
* @param {Object} request - The request containing the imposter definition
|
|
240
|
+
* @param {Object} logger - The logger
|
|
241
|
+
* @returns {Object} Promise resolving to an object containing isValid and an errors array
|
|
242
|
+
*/
|
|
243
|
+
async function validate (request, logger) {
|
|
244
|
+
const stubs = request.stubs || [],
|
|
245
|
+
encoding = request.mode === 'binary' ? 'base64' : 'utf8',
|
|
246
|
+
validations = stubs.map(stub => errorsForStub(stub, encoding, logger));
|
|
247
|
+
|
|
248
|
+
validations.push(Promise.resolve(errorsForRequest(request)));
|
|
249
|
+
if (typeof options.additionalValidation === 'function') {
|
|
250
|
+
validations.push(Promise.resolve(options.additionalValidation(request)));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const errorsForAllStubs = await Promise.all(validations),
|
|
254
|
+
allErrors = errorsForAllStubs.reduce((stubErrors, accumulator) => accumulator.concat(stubErrors), []);
|
|
255
|
+
return { isValid: allErrors.length === 0, errors: allErrors };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { validate };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = { create };
|