@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,398 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const prometheus = require('prom-client'),
|
|
4
|
+
stringify = require('safe-stable-stringify'),
|
|
5
|
+
helpers = require('../util/helpers.js'),
|
|
6
|
+
compatibility = require('./compatibility.js'),
|
|
7
|
+
exceptions = require('../util/errors.js'),
|
|
8
|
+
xpath = require('./xpath.js'),
|
|
9
|
+
jsonpath = require('./jsonpath.js'),
|
|
10
|
+
behaviors = require('./behaviors.js');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Determines the response for a stub based on the user-provided response configuration
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const metrics = {
|
|
18
|
+
proxyDuration: new prometheus.Histogram({
|
|
19
|
+
name: 'mb_proxy_duration_seconds',
|
|
20
|
+
help: 'Time it takes to get the response from the downstream service',
|
|
21
|
+
buckets: [0.1, 0.2, 0.5, 1, 3, 5, 10, 30],
|
|
22
|
+
labelNames: ['imposter']
|
|
23
|
+
}),
|
|
24
|
+
proxyCount: new prometheus.Counter({
|
|
25
|
+
name: 'mb_proxy_total',
|
|
26
|
+
help: 'Number of times a request was proxied to a downstream service',
|
|
27
|
+
labelNames: ['imposter']
|
|
28
|
+
})
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates the resolver
|
|
33
|
+
* @param {Object} stubs - The stubs repository
|
|
34
|
+
* @param {Object} proxy - The protocol-specific proxy implementation
|
|
35
|
+
* @param {String} callbackURL - The protocol callback URL for response resolution
|
|
36
|
+
* @returns {Object}
|
|
37
|
+
*/
|
|
38
|
+
function create (stubs, proxy, callbackURL) {
|
|
39
|
+
// injectState is deprecated in favor of imposterState, but kept for backwards compatibility
|
|
40
|
+
const injectState = {}, // eslint-disable-line no-unused-vars
|
|
41
|
+
pendingProxyResolutions = {},
|
|
42
|
+
inProcessProxy = Boolean(proxy);
|
|
43
|
+
let nextProxyResolutionKey = 0;
|
|
44
|
+
|
|
45
|
+
function inject (request, fn, logger, imposterState) {
|
|
46
|
+
if (request.isDryRun) {
|
|
47
|
+
return Promise.resolve({});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return new Promise((done, reject) => {
|
|
51
|
+
// Leave parameters for older interface
|
|
52
|
+
const injected = `(${fn})(config, injectState, logger, done, imposterState);`,
|
|
53
|
+
config = {
|
|
54
|
+
request: helpers.clone(request),
|
|
55
|
+
state: imposterState,
|
|
56
|
+
logger: logger,
|
|
57
|
+
callback: done
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
compatibility.downcastInjectionConfig(config);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const response = eval(injected);
|
|
64
|
+
if (helpers.defined(response)) {
|
|
65
|
+
done(response);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
logger.error(`injection X=> ${error}`);
|
|
70
|
+
logger.error(` full source: ${JSON.stringify(injected)}`);
|
|
71
|
+
logger.error(` config.request: ${JSON.stringify(config.request)}`);
|
|
72
|
+
logger.error(` config.state: ${JSON.stringify(config.state)}`);
|
|
73
|
+
reject(exceptions.InjectionError('invalid response injection', {
|
|
74
|
+
source: injected,
|
|
75
|
+
data: error.message
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function selectionValue (nodes) {
|
|
82
|
+
if (!helpers.defined(nodes)) {
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
else if (!Array.isArray(nodes)) {
|
|
86
|
+
return nodes; // booleans and counts
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
return (nodes.length === 1) ? nodes[0] : nodes;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function xpathValue (xpathConfig, possibleXML, logger) {
|
|
94
|
+
const nodes = xpath.select(xpathConfig.selector, xpathConfig.ns, possibleXML, logger);
|
|
95
|
+
|
|
96
|
+
return selectionValue(nodes);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function jsonpathValue (jsonpathConfig, possibleJSON, logger) {
|
|
100
|
+
const nodes = jsonpath.select(jsonpathConfig.selector, possibleJSON, logger);
|
|
101
|
+
|
|
102
|
+
return selectionValue(nodes);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildDeepEqual (request, fieldName, predicateGenerators, valueOf) {
|
|
106
|
+
if (!predicateGenerators.ignore) {
|
|
107
|
+
return valueOf(request[fieldName]);
|
|
108
|
+
}
|
|
109
|
+
const objFilter = helpers.objFilter;
|
|
110
|
+
return valueOf(objFilter(request[fieldName], predicateGenerators.ignore[fieldName]));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildEquals (request, matchers, valueOf) {
|
|
114
|
+
const result = {},
|
|
115
|
+
isObject = helpers.isObject;
|
|
116
|
+
|
|
117
|
+
Object.keys(matchers).forEach(key => {
|
|
118
|
+
if (isObject(request[key])) {
|
|
119
|
+
result[key] = buildEquals(request[key], matchers[key], valueOf);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
result[key] = valueOf(request[key]);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const path = [];
|
|
129
|
+
|
|
130
|
+
function buildExists (request, fieldName, matchers, initialRequest) {
|
|
131
|
+
const isObject = helpers.isObject,
|
|
132
|
+
setDeep = helpers.setDeep;
|
|
133
|
+
Object.keys(request).forEach(key => {
|
|
134
|
+
path.push(key);
|
|
135
|
+
if (isObject(request[key])) {
|
|
136
|
+
buildExists(request[key], fieldName, matchers[key], initialRequest);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const booleanValue = (typeof fieldName !== 'undefined' && fieldName !== null && fieldName !== '');
|
|
140
|
+
setDeep(initialRequest, path, booleanValue);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return initialRequest;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function predicatesFor (request, matchers, logger) {
|
|
147
|
+
const predicates = [];
|
|
148
|
+
|
|
149
|
+
matchers.forEach(matcher => {
|
|
150
|
+
if (matcher.inject) {
|
|
151
|
+
// eslint-disable-next-line no-unused-vars
|
|
152
|
+
const config = { request, logger },
|
|
153
|
+
injected = `(${matcher.inject})(config);`;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
predicates.push(...eval(injected));
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
logger.error(`injection X=> ${error}`);
|
|
160
|
+
logger.error(` source: ${JSON.stringify(injected)}`);
|
|
161
|
+
logger.error(` request: ${JSON.stringify(request)}`);
|
|
162
|
+
throw exceptions.InjectionError('invalid predicateGenerator injection', { source: injected, data: error.message });
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const basePredicate = {};
|
|
168
|
+
let hasPredicateOperator = false;
|
|
169
|
+
let predicateOperator; // eslint-disable-line no-unused-vars
|
|
170
|
+
let valueOf = field => field;
|
|
171
|
+
|
|
172
|
+
// Add parameters
|
|
173
|
+
Object.keys(matcher).forEach(key => {
|
|
174
|
+
if (key !== 'matches' && key !== 'predicateOperator' && key !== 'ignore') {
|
|
175
|
+
basePredicate[key] = matcher[key];
|
|
176
|
+
}
|
|
177
|
+
if (key === 'xpath') {
|
|
178
|
+
valueOf = field => xpathValue(matcher.xpath, field, logger);
|
|
179
|
+
}
|
|
180
|
+
else if (key === 'jsonpath') {
|
|
181
|
+
valueOf = field => jsonpathValue(matcher.jsonpath, field, logger);
|
|
182
|
+
}
|
|
183
|
+
else if (key === 'predicateOperator') {
|
|
184
|
+
hasPredicateOperator = true;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
Object.keys(matcher.matches).forEach(fieldName => {
|
|
189
|
+
const matcherValue = matcher.matches[fieldName],
|
|
190
|
+
predicate = helpers.clone(basePredicate);
|
|
191
|
+
|
|
192
|
+
if (matcherValue === true && hasPredicateOperator === false) {
|
|
193
|
+
predicate.deepEquals = {};
|
|
194
|
+
predicate.deepEquals[fieldName] = buildDeepEqual(request, fieldName, matcher, valueOf);
|
|
195
|
+
}
|
|
196
|
+
else if (hasPredicateOperator === true && matcher.predicateOperator === 'exists') {
|
|
197
|
+
predicate[matcher.predicateOperator] = buildExists(request, fieldName, matcherValue, request);
|
|
198
|
+
}
|
|
199
|
+
else if (hasPredicateOperator === true && matcher.predicateOperator !== 'exists') {
|
|
200
|
+
predicate[matcher.predicateOperator] = valueOf(request);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
predicate.equals = {};
|
|
204
|
+
predicate.equals[fieldName] = buildEquals(request[fieldName], matcherValue, valueOf);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
predicates.push(predicate);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return predicates;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function deepEqual (obj1, obj2) {
|
|
215
|
+
return stringify(obj1) === stringify(obj2);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function newIsResponse (response, proxyConfig) {
|
|
219
|
+
const result = { is: response },
|
|
220
|
+
addBehaviors = [];
|
|
221
|
+
|
|
222
|
+
if (proxyConfig.addWaitBehavior && response._proxyResponseTime) {
|
|
223
|
+
addBehaviors.push({ wait: response._proxyResponseTime });
|
|
224
|
+
}
|
|
225
|
+
if (proxyConfig.addDecorateBehavior) {
|
|
226
|
+
addBehaviors.push({ decorate: proxyConfig.addDecorateBehavior });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (addBehaviors.length > 0) {
|
|
230
|
+
result.behaviors = addBehaviors;
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function recordProxyAlways (newPredicates, newResponse, responseConfig) {
|
|
236
|
+
const filter = stubPredicates => deepEqual(newPredicates, stubPredicates),
|
|
237
|
+
index = await responseConfig.stubIndex(),
|
|
238
|
+
match = await stubs.first(filter, index + 1);
|
|
239
|
+
|
|
240
|
+
if (match.success) {
|
|
241
|
+
return match.stub.addResponse(newResponse);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
return stubs.add({ predicates: newPredicates, responses: [newResponse] });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function recordProxyResponse (responseConfig, request, response, logger) {
|
|
249
|
+
const newPredicates = predicatesFor(request, responseConfig.proxy.predicateGenerators || [], logger),
|
|
250
|
+
newResponse = newIsResponse(response, responseConfig.proxy);
|
|
251
|
+
|
|
252
|
+
if (responseConfig.proxy.mode === 'proxyOnce') {
|
|
253
|
+
const index = await responseConfig.stubIndex();
|
|
254
|
+
await stubs.insertAtIndex({ predicates: newPredicates, responses: [newResponse] }, index);
|
|
255
|
+
}
|
|
256
|
+
else if (responseConfig.proxy.mode === 'proxyAlways') {
|
|
257
|
+
await recordProxyAlways(newPredicates, newResponse, responseConfig);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function proxyAndRecord (responseConfig, request, logger, requestDetails, imposterState) {
|
|
262
|
+
const startTime = new Date(),
|
|
263
|
+
observeProxyDuration = metrics.proxyDuration.startTimer();
|
|
264
|
+
|
|
265
|
+
metrics.proxyCount.inc({ imposter: logger.scopePrefix });
|
|
266
|
+
|
|
267
|
+
if (['proxyOnce', 'proxyAlways', 'proxyTransparent'].indexOf(responseConfig.proxy.mode) < 0) {
|
|
268
|
+
responseConfig.proxy.mode = 'proxyOnce';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (inProcessProxy) {
|
|
272
|
+
const response = await proxy.to(responseConfig.proxy.to, request, responseConfig.proxy, requestDetails);
|
|
273
|
+
observeProxyDuration({ imposter: logger.scopePrefix });
|
|
274
|
+
response._proxyResponseTime = new Date() - startTime;
|
|
275
|
+
|
|
276
|
+
// Run behaviors here to persist decorated response
|
|
277
|
+
const transformed = await behaviors.execute(request, response, responseConfig.behaviors, logger, imposterState);
|
|
278
|
+
await recordProxyResponse(responseConfig, request, transformed, logger);
|
|
279
|
+
return transformed;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
pendingProxyResolutions[nextProxyResolutionKey] = {
|
|
283
|
+
responseConfig: responseConfig,
|
|
284
|
+
request: request,
|
|
285
|
+
requestDetails: requestDetails,
|
|
286
|
+
observeProxyDuration: observeProxyDuration,
|
|
287
|
+
startTime: startTime
|
|
288
|
+
};
|
|
289
|
+
nextProxyResolutionKey += 1;
|
|
290
|
+
return {
|
|
291
|
+
proxy: responseConfig.proxy,
|
|
292
|
+
request: request,
|
|
293
|
+
callbackURL: `${callbackURL}/${nextProxyResolutionKey - 1}`
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function processResponse (responseConfig, request, logger, imposterState, requestDetails) {
|
|
299
|
+
if (responseConfig.is) {
|
|
300
|
+
// Clone to prevent accidental state changes downstream
|
|
301
|
+
return Promise.resolve(helpers.clone(responseConfig.is));
|
|
302
|
+
}
|
|
303
|
+
else if (responseConfig.proxy) {
|
|
304
|
+
return proxyAndRecord(responseConfig, request, logger, requestDetails, imposterState);
|
|
305
|
+
}
|
|
306
|
+
else if (responseConfig.inject) {
|
|
307
|
+
return inject(request, responseConfig.inject, logger, imposterState);
|
|
308
|
+
}
|
|
309
|
+
else if (responseConfig.fault) {
|
|
310
|
+
// Clone to prevent accidental state changes downstream
|
|
311
|
+
return Promise.resolve(helpers.clone(responseConfig));
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
return Promise.reject(exceptions.ValidationError('unrecognized response type',
|
|
315
|
+
{ source: helpers.clone(responseConfig) }));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// eslint-disable-next-line complexity
|
|
320
|
+
function hasMultipleTypes (responseConfig) {
|
|
321
|
+
return (responseConfig.is && responseConfig.proxy) ||
|
|
322
|
+
(responseConfig.is && responseConfig.inject) ||
|
|
323
|
+
(responseConfig.proxy && responseConfig.inject) ||
|
|
324
|
+
(responseConfig.fault && responseConfig.proxy) ||
|
|
325
|
+
(responseConfig.fault && responseConfig.is) ||
|
|
326
|
+
(responseConfig.fault && responseConfig.inject);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Resolves a single response
|
|
331
|
+
* @memberOf module:models/responseResolver#
|
|
332
|
+
* @param {Object} responseConfig - The API-provided response configuration
|
|
333
|
+
* @param {Object} request - The protocol-specific request object
|
|
334
|
+
* @param {Object} logger - The logger
|
|
335
|
+
* @param {Object} imposterState - The current state for the imposter
|
|
336
|
+
* @param {Object} options - Additional options not carried with the request
|
|
337
|
+
* @returns {Object} - Promise resolving to the response
|
|
338
|
+
*/
|
|
339
|
+
async function resolve (responseConfig, request, logger, imposterState, options) {
|
|
340
|
+
if (hasMultipleTypes(responseConfig)) {
|
|
341
|
+
return Promise.reject(exceptions.ValidationError('each response object must have only one response type',
|
|
342
|
+
{ source: responseConfig }));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let response = await processResponse(responseConfig, helpers.clone(request), logger, imposterState, options);
|
|
346
|
+
|
|
347
|
+
// We may have already run the behaviors in the proxy call to persist the decorated response
|
|
348
|
+
// in the new stub. If so, we need to ensure we don't re-run it
|
|
349
|
+
// If we're doing fault simulation there's no need to execute the behaviours
|
|
350
|
+
if (!responseConfig.proxy && !responseConfig.fault) {
|
|
351
|
+
response = await behaviors.execute(request, response, responseConfig.behaviors, logger, imposterState);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (inProcessProxy) {
|
|
355
|
+
return response;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
return responseConfig.proxy ? response : { response };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Finishes the protocol implementation dance for proxy. On the first call,
|
|
364
|
+
* mountebank selects a JSON proxy response and sends it to the protocol implementation,
|
|
365
|
+
* saving state indexed by proxyResolutionKey. The protocol implementation sends the proxy
|
|
366
|
+
* to the downstream system and calls mountebank again with the response so mountebank
|
|
367
|
+
* can save it and add behaviors
|
|
368
|
+
* @memberOf module:models/responseResolver#
|
|
369
|
+
* @param {Object} proxyResponse - the proxy response from the protocol implementation
|
|
370
|
+
* @param {Number} proxyResolutionKey - the key into the saved proxy state
|
|
371
|
+
* @param {Object} logger - the logger
|
|
372
|
+
* @param {Object} imposterState - the user controlled state variable
|
|
373
|
+
* @returns {Object} - Promise resolving to the response
|
|
374
|
+
*/
|
|
375
|
+
async function resolveProxy (proxyResponse, proxyResolutionKey, logger, imposterState) {
|
|
376
|
+
const pendingProxyConfig = pendingProxyResolutions[proxyResolutionKey];
|
|
377
|
+
|
|
378
|
+
if (pendingProxyConfig) {
|
|
379
|
+
pendingProxyConfig.observeProxyDuration({ imposter: logger.scopePrefix });
|
|
380
|
+
proxyResponse._proxyResponseTime = new Date() - pendingProxyConfig.startTime;
|
|
381
|
+
|
|
382
|
+
const response = await behaviors.execute(pendingProxyConfig.request, proxyResponse,
|
|
383
|
+
pendingProxyConfig.responseConfig.behaviors, logger, imposterState);
|
|
384
|
+
await recordProxyResponse(pendingProxyConfig.responseConfig, pendingProxyConfig.request, response, logger);
|
|
385
|
+
delete pendingProxyResolutions[proxyResolutionKey];
|
|
386
|
+
return response;
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
logger.error('Invalid proxy resolution key: ' + proxyResolutionKey);
|
|
390
|
+
return Promise.reject(exceptions.MissingResourceError('invalid proxy resolution key',
|
|
391
|
+
{ source: `${callbackURL}/${proxyResolutionKey}` }));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return { resolve, resolveProxy };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
module.exports = { create };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const config = JSON.parse(process.argv[2]),
|
|
4
|
+
smtpServer = require('./smtpServer'),
|
|
5
|
+
mbConnection = require('../mbConnection').create(config);
|
|
6
|
+
|
|
7
|
+
smtpServer.create(config, mbConnection.logger(), mbConnection.getResponse).then(server => {
|
|
8
|
+
mbConnection.setPort(server.port);
|
|
9
|
+
|
|
10
|
+
const metadata = server.metadata;
|
|
11
|
+
metadata.port = server.port;
|
|
12
|
+
console.log(JSON.stringify(metadata));
|
|
13
|
+
}).catch(error => {
|
|
14
|
+
console.error(JSON.stringify(error));
|
|
15
|
+
process.exit(1); // eslint-disable-line no-process-exit
|
|
16
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transforms an SMTP request into the simplified API-friendly mountebank request
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const mailParser = require('mailparser');
|
|
9
|
+
|
|
10
|
+
function addressValues (addresses) {
|
|
11
|
+
// mailparser sometimes returns an array, sometimes an object, so we have to normalize
|
|
12
|
+
if (!addresses) {
|
|
13
|
+
addresses = [];
|
|
14
|
+
}
|
|
15
|
+
if (!Array.isArray(addresses)) {
|
|
16
|
+
addresses = [addresses];
|
|
17
|
+
}
|
|
18
|
+
return addresses.map(address => address.value[0]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function transform (session, email) {
|
|
22
|
+
return {
|
|
23
|
+
requestFrom: session.remoteAddress,
|
|
24
|
+
ip: session.remoteAddress,
|
|
25
|
+
envelopeFrom: session.envelope.mailFrom.address,
|
|
26
|
+
envelopeTo: session.envelope.rcptTo.map(value => value.address),
|
|
27
|
+
from: email.from.value[0],
|
|
28
|
+
to: addressValues(email.to),
|
|
29
|
+
cc: addressValues(email.cc),
|
|
30
|
+
bcc: addressValues(email.bcc),
|
|
31
|
+
subject: email.subject,
|
|
32
|
+
priority: email.priority || 'normal',
|
|
33
|
+
references: email.references || [],
|
|
34
|
+
inReplyTo: email.inReplyTo || [],
|
|
35
|
+
text: (email.text || '').trim(),
|
|
36
|
+
html: (email.html || '').trim(),
|
|
37
|
+
attachments: email.attachments || []
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Transforms the raw SMTP request into the mountebank request
|
|
43
|
+
* @param {Object} request - The raw SMTP request
|
|
44
|
+
* @returns {Object}
|
|
45
|
+
*/
|
|
46
|
+
function createFrom (request) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const simpleParser = mailParser.simpleParser;
|
|
49
|
+
simpleParser(request.source, (err, mail) => {
|
|
50
|
+
if (err) {
|
|
51
|
+
reject(err);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
resolve(transform(request.session, mail));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { createFrom };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const smtpServer = require('smtp-server'),
|
|
4
|
+
helpers = require('../../util/helpers.js'),
|
|
5
|
+
errors = require('../../util/errors.js'),
|
|
6
|
+
smtpRequest = require('./smtpRequest.js');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents an smtp imposter
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function create (options, logger, responseFn) {
|
|
14
|
+
const connections = {},
|
|
15
|
+
SMTPServer = smtpServer.SMTPServer,
|
|
16
|
+
server = new SMTPServer({
|
|
17
|
+
maxAllowedUnauthenticatedCommands: 1000,
|
|
18
|
+
disableReverseLookup: true,
|
|
19
|
+
authOptional: true,
|
|
20
|
+
onConnect (socket, callback) {
|
|
21
|
+
const name = helpers.socketName(socket);
|
|
22
|
+
|
|
23
|
+
logger.debug('%s ESTABLISHED', name);
|
|
24
|
+
|
|
25
|
+
if (socket.on) {
|
|
26
|
+
connections[name] = socket;
|
|
27
|
+
|
|
28
|
+
socket.on('error', error => {
|
|
29
|
+
logger.error('%s transmission error X=> %s', name, JSON.stringify(error));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
socket.on('end', () => {
|
|
33
|
+
logger.debug('%s LAST-ACK', name);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
socket.on('close', () => {
|
|
37
|
+
logger.debug('%s CLOSED', name);
|
|
38
|
+
delete connections[name];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return callback();
|
|
42
|
+
},
|
|
43
|
+
onData (stream, socket, callback) {
|
|
44
|
+
const request = { session: socket, source: stream, callback: callback },
|
|
45
|
+
clientName = helpers.socketName(socket);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
smtpRequest.createFrom(request).then(simpleRequest => {
|
|
49
|
+
logger.info(`${clientName} => Envelope from: ${JSON.stringify(simpleRequest.from)} to: ${JSON.stringify(simpleRequest.to)}`);
|
|
50
|
+
logger.debug('%s => %s', clientName, JSON.stringify(simpleRequest));
|
|
51
|
+
return responseFn(simpleRequest);
|
|
52
|
+
}).then(response => {
|
|
53
|
+
if (response) {
|
|
54
|
+
logger.debug('%s <= %s', clientName, JSON.stringify(response));
|
|
55
|
+
}
|
|
56
|
+
return Promise.resolve(true);
|
|
57
|
+
}).then(() => Promise.resolve(request.callback()))
|
|
58
|
+
.catch(error => {
|
|
59
|
+
logger.error('%s X=> %s', clientName, JSON.stringify(errors.details(error)));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
logger.error('%s X=> %s', clientName, JSON.stringify(errors.details(error)));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
server.on('error', error => {
|
|
70
|
+
if (error.errno === 'EADDRINUSE') {
|
|
71
|
+
reject(errors.ResourceConflictError(`Port ${options.port} is already in use`));
|
|
72
|
+
}
|
|
73
|
+
else if (error.errno === 'EACCES') {
|
|
74
|
+
reject(errors.InsufficientAccessError());
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
reject(error);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
server.listen(options.port || 0, options.host, () => {
|
|
82
|
+
resolve({
|
|
83
|
+
port: server.server.address().port,
|
|
84
|
+
metadata: {},
|
|
85
|
+
close: callback => {
|
|
86
|
+
server.close(callback);
|
|
87
|
+
Object.keys(connections).forEach(socket => {
|
|
88
|
+
connections[socket].destroy();
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
proxy: {},
|
|
92
|
+
encoding: 'utf8'
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
testRequest: {
|
|
100
|
+
from: 'test@test.com',
|
|
101
|
+
to: ['test@test.com'],
|
|
102
|
+
subject: 'Test',
|
|
103
|
+
text: 'Test'
|
|
104
|
+
},
|
|
105
|
+
testProxyResponse: {},
|
|
106
|
+
create: create,
|
|
107
|
+
validate: undefined
|
|
108
|
+
};
|
|
109
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const config = JSON.parse(process.argv[2]),
|
|
4
|
+
tcpServer = require('./tcpServer.js'),
|
|
5
|
+
tcpProxy = require('./tcpProxy.js'),
|
|
6
|
+
mbConnection = require('../mbConnection.js').create(config);
|
|
7
|
+
|
|
8
|
+
tcpServer.create(config, mbConnection.logger(), mbConnection.getResponse).then(server => {
|
|
9
|
+
mbConnection.setPort(server.port);
|
|
10
|
+
mbConnection.setProxy(tcpProxy.create(mbConnection.logger(), server.encoding, server.isEndOfRequest));
|
|
11
|
+
|
|
12
|
+
const metadata = server.metadata;
|
|
13
|
+
metadata.port = server.port;
|
|
14
|
+
console.log(JSON.stringify(metadata));
|
|
15
|
+
}).catch(error => {
|
|
16
|
+
console.error(JSON.stringify(error));
|
|
17
|
+
process.exit(1); // eslint-disable-line no-process-exit
|
|
18
|
+
});
|