@mbtest/mountebank 2.9.2-beta.9050

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/bin/mb +136 -0
  4. package/package.json +71 -0
  5. package/releases.json +52 -0
  6. package/src/cli/api.js +112 -0
  7. package/src/cli/cli.js +420 -0
  8. package/src/controllers/configController.js +64 -0
  9. package/src/controllers/feedController.js +115 -0
  10. package/src/controllers/homeController.js +58 -0
  11. package/src/controllers/imposterController.js +328 -0
  12. package/src/controllers/impostersController.js +215 -0
  13. package/src/controllers/logsController.js +52 -0
  14. package/src/models/behaviors.js +553 -0
  15. package/src/models/behaviorsValidator.js +186 -0
  16. package/src/models/compatibility.js +133 -0
  17. package/src/models/dryRunValidator.js +261 -0
  18. package/src/models/filesystemBackedImpostersRepository.js +908 -0
  19. package/src/models/http/baseHttpServer.js +207 -0
  20. package/src/models/http/headersMap.js +87 -0
  21. package/src/models/http/httpProxy.js +230 -0
  22. package/src/models/http/httpRequest.js +82 -0
  23. package/src/models/http/httpServer.js +18 -0
  24. package/src/models/http/index.js +18 -0
  25. package/src/models/https/cert/mb-cert.pem +20 -0
  26. package/src/models/https/cert/mb-csr.pem +16 -0
  27. package/src/models/https/cert/mb-key.pem +27 -0
  28. package/src/models/https/httpsServer.js +42 -0
  29. package/src/models/https/index.js +18 -0
  30. package/src/models/imposter.js +243 -0
  31. package/src/models/imposterPrinter.js +120 -0
  32. package/src/models/impostersRepository.js +49 -0
  33. package/src/models/inMemoryImpostersRepository.js +418 -0
  34. package/src/models/jsonpath.js +44 -0
  35. package/src/models/mbConnection.js +107 -0
  36. package/src/models/predicates.js +438 -0
  37. package/src/models/protocols.js +242 -0
  38. package/src/models/responseResolver.js +398 -0
  39. package/src/models/smtp/index.js +16 -0
  40. package/src/models/smtp/smtpRequest.js +60 -0
  41. package/src/models/smtp/smtpServer.js +109 -0
  42. package/src/models/tcp/index.js +18 -0
  43. package/src/models/tcp/tcpProxy.js +110 -0
  44. package/src/models/tcp/tcpRequest.js +23 -0
  45. package/src/models/tcp/tcpServer.js +156 -0
  46. package/src/models/tcp/tcpValidator.js +19 -0
  47. package/src/models/xpath.js +95 -0
  48. package/src/mountebank.js +245 -0
  49. package/src/public/images/arrow_down.png +0 -0
  50. package/src/public/images/arrow_up.png +0 -0
  51. package/src/public/images/book.jpg +0 -0
  52. package/src/public/images/dataflow.png +0 -0
  53. package/src/public/images/favicon.ico +0 -0
  54. package/src/public/images/forkme_right_orange_ff7600.png +0 -0
  55. package/src/public/images/mountebank.png +0 -0
  56. package/src/public/images/overview.gif +0 -0
  57. package/src/public/images/quote.png +0 -0
  58. package/src/public/images/tw-logo.png +0 -0
  59. package/src/public/scripts/jquery/jquery-3.6.1.min.js +2 -0
  60. package/src/public/scripts/urlHashHandler.js +31 -0
  61. package/src/public/stylesheets/application.css +424 -0
  62. package/src/public/stylesheets/ie.css +14 -0
  63. package/src/public/stylesheets/imposters.css +121 -0
  64. package/src/public/stylesheets/jqueryui/1.10.4/themes/smoothness/jquery-ui.css +1178 -0
  65. package/src/util/combinators.js +68 -0
  66. package/src/util/date.js +51 -0
  67. package/src/util/errors.js +55 -0
  68. package/src/util/helpers.js +131 -0
  69. package/src/util/inherit.js +28 -0
  70. package/src/util/ip.js +54 -0
  71. package/src/util/logger.js +83 -0
  72. package/src/util/middleware.js +256 -0
  73. package/src/util/scopedLogger.js +47 -0
  74. package/src/views/_footer.ejs +20 -0
  75. package/src/views/_header.ejs +113 -0
  76. package/src/views/_imposter.ejs +8 -0
  77. package/src/views/config.ejs +71 -0
  78. package/src/views/docs/api/behaviors/copy.ejs +427 -0
  79. package/src/views/docs/api/behaviors/decorate.ejs +182 -0
  80. package/src/views/docs/api/behaviors/lookup.ejs +220 -0
  81. package/src/views/docs/api/behaviors/shellTransform.ejs +153 -0
  82. package/src/views/docs/api/behaviors/wait.ejs +121 -0
  83. package/src/views/docs/api/behaviors.ejs +141 -0
  84. package/src/views/docs/api/contracts/addStub-description.ejs +10 -0
  85. package/src/views/docs/api/contracts/addStub.ejs +10 -0
  86. package/src/views/docs/api/contracts/config-description.ejs +32 -0
  87. package/src/views/docs/api/contracts/config.ejs +23 -0
  88. package/src/views/docs/api/contracts/home-description.ejs +18 -0
  89. package/src/views/docs/api/contracts/home.ejs +13 -0
  90. package/src/views/docs/api/contracts/imposter-description.ejs +439 -0
  91. package/src/views/docs/api/contracts/imposter.ejs +182 -0
  92. package/src/views/docs/api/contracts/imposters-description.ejs +13 -0
  93. package/src/views/docs/api/contracts/imposters.ejs +13 -0
  94. package/src/views/docs/api/contracts/logs-description.ejs +3 -0
  95. package/src/views/docs/api/contracts/logs.ejs +14 -0
  96. package/src/views/docs/api/contracts/stub-description.ejs +4 -0
  97. package/src/views/docs/api/contracts/stub.ejs +7 -0
  98. package/src/views/docs/api/contracts/stubs-description.ejs +4 -0
  99. package/src/views/docs/api/contracts/stubs.ejs +11 -0
  100. package/src/views/docs/api/contracts.ejs +133 -0
  101. package/src/views/docs/api/errors.ejs +64 -0
  102. package/src/views/docs/api/fault/connectionReset.ejs +31 -0
  103. package/src/views/docs/api/fault/randomDataThenClose.ejs +31 -0
  104. package/src/views/docs/api/faults.ejs +57 -0
  105. package/src/views/docs/api/injection.ejs +426 -0
  106. package/src/views/docs/api/json.ejs +205 -0
  107. package/src/views/docs/api/jsonpath.ejs +210 -0
  108. package/src/views/docs/api/mocks.ejs +130 -0
  109. package/src/views/docs/api/overview.ejs +968 -0
  110. package/src/views/docs/api/predicates/and.ejs +62 -0
  111. package/src/views/docs/api/predicates/contains.ejs +64 -0
  112. package/src/views/docs/api/predicates/deepEquals.ejs +114 -0
  113. package/src/views/docs/api/predicates/endsWith.ejs +66 -0
  114. package/src/views/docs/api/predicates/equals.ejs +125 -0
  115. package/src/views/docs/api/predicates/exists.ejs +118 -0
  116. package/src/views/docs/api/predicates/inject.ejs +67 -0
  117. package/src/views/docs/api/predicates/matches.ejs +66 -0
  118. package/src/views/docs/api/predicates/not.ejs +52 -0
  119. package/src/views/docs/api/predicates/or.ejs +79 -0
  120. package/src/views/docs/api/predicates/startsWith.ejs +62 -0
  121. package/src/views/docs/api/predicates.ejs +382 -0
  122. package/src/views/docs/api/proxies.ejs +191 -0
  123. package/src/views/docs/api/proxy/addDecorateBehavior.ejs +115 -0
  124. package/src/views/docs/api/proxy/addWaitBehavior.ejs +96 -0
  125. package/src/views/docs/api/proxy/injectHeaders.ejs +91 -0
  126. package/src/views/docs/api/proxy/predicateGenerators.ejs +600 -0
  127. package/src/views/docs/api/proxy/proxyModes.ejs +495 -0
  128. package/src/views/docs/api/stubs.ejs +391 -0
  129. package/src/views/docs/api/xpath.ejs +281 -0
  130. package/src/views/docs/cli/configFiles.ejs +133 -0
  131. package/src/views/docs/cli/customFormatters.ejs +53 -0
  132. package/src/views/docs/cli/help.ejs +6 -0
  133. package/src/views/docs/cli/replay.ejs +42 -0
  134. package/src/views/docs/cli/restart.ejs +10 -0
  135. package/src/views/docs/cli/save.ejs +68 -0
  136. package/src/views/docs/cli/start.ejs +234 -0
  137. package/src/views/docs/cli/stop.ejs +32 -0
  138. package/src/views/docs/commandLine.ejs +93 -0
  139. package/src/views/docs/communityExtensions.ejs +233 -0
  140. package/src/views/docs/gettingStarted.ejs +146 -0
  141. package/src/views/docs/mentalModel.ejs +51 -0
  142. package/src/views/docs/protocols/custom.ejs +231 -0
  143. package/src/views/docs/protocols/http.ejs +238 -0
  144. package/src/views/docs/protocols/https.ejs +246 -0
  145. package/src/views/docs/protocols/smtp.ejs +142 -0
  146. package/src/views/docs/protocols/tcp.ejs +431 -0
  147. package/src/views/docs/security.ejs +38 -0
  148. package/src/views/faqs.ejs +65 -0
  149. package/src/views/feed.ejs +33 -0
  150. package/src/views/imposter.ejs +22 -0
  151. package/src/views/imposters.ejs +33 -0
  152. package/src/views/index.ejs +89 -0
  153. package/src/views/license.ejs +30 -0
  154. package/src/views/logs.ejs +77 -0
  155. package/src/views/releases/v1.1.0.ejs +55 -0
  156. package/src/views/releases/v1.1.36.ejs +84 -0
  157. package/src/views/releases/v1.1.72.ejs +92 -0
  158. package/src/views/releases/v1.10.0.ejs +108 -0
  159. package/src/views/releases/v1.11.0.ejs +109 -0
  160. package/src/views/releases/v1.12.0.ejs +96 -0
  161. package/src/views/releases/v1.13.0.ejs +118 -0
  162. package/src/views/releases/v1.14.0.ejs +107 -0
  163. package/src/views/releases/v1.14.1.ejs +94 -0
  164. package/src/views/releases/v1.15.0.ejs +113 -0
  165. package/src/views/releases/v1.16.0.ejs +104 -0
  166. package/src/views/releases/v1.2.0.ejs +78 -0
  167. package/src/views/releases/v1.2.103.ejs +86 -0
  168. package/src/views/releases/v1.2.122.ejs +86 -0
  169. package/src/views/releases/v1.2.30.ejs +84 -0
  170. package/src/views/releases/v1.2.45.ejs +84 -0
  171. package/src/views/releases/v1.2.56.ejs +79 -0
  172. package/src/views/releases/v1.3.0.ejs +86 -0
  173. package/src/views/releases/v1.3.1.ejs +100 -0
  174. package/src/views/releases/v1.4.0.ejs +96 -0
  175. package/src/views/releases/v1.4.1.ejs +103 -0
  176. package/src/views/releases/v1.4.2.ejs +100 -0
  177. package/src/views/releases/v1.4.3.ejs +113 -0
  178. package/src/views/releases/v1.5.0.ejs +104 -0
  179. package/src/views/releases/v1.5.1.ejs +91 -0
  180. package/src/views/releases/v1.6.0.ejs +109 -0
  181. package/src/views/releases/v1.7.0.ejs +113 -0
  182. package/src/views/releases/v1.7.1.ejs +90 -0
  183. package/src/views/releases/v1.7.2.ejs +96 -0
  184. package/src/views/releases/v1.8.0.ejs +121 -0
  185. package/src/views/releases/v1.9.0.ejs +111 -0
  186. package/src/views/releases/v2.0.0.ejs +159 -0
  187. package/src/views/releases/v2.1.0.ejs +121 -0
  188. package/src/views/releases/v2.1.1.ejs +106 -0
  189. package/src/views/releases/v2.1.2.ejs +84 -0
  190. package/src/views/releases/v2.2.0.ejs +115 -0
  191. package/src/views/releases/v2.2.1.ejs +102 -0
  192. package/src/views/releases/v2.3.0.ejs +121 -0
  193. package/src/views/releases/v2.3.1.ejs +100 -0
  194. package/src/views/releases/v2.3.2.ejs +102 -0
  195. package/src/views/releases/v2.3.3.ejs +97 -0
  196. package/src/views/releases/v2.4.0.ejs +114 -0
  197. package/src/views/releases/v2.5.0.ejs +51 -0
  198. package/src/views/releases/v2.6.0.ejs +35 -0
  199. package/src/views/releases/v2.7.0.ejs +32 -0
  200. package/src/views/releases/v2.8.0.ejs +36 -0
  201. package/src/views/releases/v2.8.1.ejs +7 -0
  202. package/src/views/releases/v2.8.2.ejs +26 -0
  203. package/src/views/releases/v2.9.0.ejs +32 -0
  204. package/src/views/releases/v2.9.1.ejs +10 -0
  205. package/src/views/releases.ejs +26 -0
  206. package/src/views/sitemap.ejs +36 -0
  207. package/src/views/support.ejs +14 -0
@@ -0,0 +1,438 @@
1
+ 'use strict';
2
+
3
+ const stringify = require('safe-stable-stringify'),
4
+ safeRegex = require('safe-regex'),
5
+ jsonpath = require('./jsonpath.js'),
6
+ helpers = require('../util/helpers.js'),
7
+ xPath = require('./xpath.js'),
8
+ combinators = require('../util/combinators.js'),
9
+ errors = require('../util/errors.js'),
10
+ compatibility = require('./compatibility.js');
11
+
12
+ /**
13
+ * All the predicates that determine whether a stub matches a request
14
+ * @module
15
+ */
16
+
17
+ function sortObjects (a, b) {
18
+ const isObject = helpers.isObject;
19
+
20
+ if (isObject(a) && isObject(b)) {
21
+ // Make best effort at sorting arrays of objects to make
22
+ // deepEquals order-independent
23
+ return sortObjects(stringify(a), stringify(b));
24
+ }
25
+ else if (a < b) {
26
+ return -1;
27
+ }
28
+ else {
29
+ return 1;
30
+ }
31
+ }
32
+
33
+ function forceStrings (value) {
34
+ const isObject = helpers.isObject;
35
+
36
+ if (value === null) {
37
+ return 'null';
38
+ }
39
+ else if (Array.isArray(value)) {
40
+ return value.map(forceStrings);
41
+ }
42
+ else if (isObject(value)) {
43
+ return Object.keys(value).reduce((accumulator, key) => {
44
+ accumulator[key] = forceStrings(value[key]);
45
+ return accumulator;
46
+ }, {});
47
+ }
48
+ else if (typeof value.toString === 'function') {
49
+ return value.toString();
50
+ }
51
+ else {
52
+ return value;
53
+ }
54
+ }
55
+
56
+ function select (type, selectFn, encoding) {
57
+ if (encoding === 'base64') {
58
+ throw errors.ValidationError(`the ${type} predicate parameter is not allowed in binary mode`);
59
+ }
60
+
61
+ const nodeValues = selectFn();
62
+
63
+ // Return either a string if one match or array if multiple
64
+ // This matches the behavior of node's handling of query parameters,
65
+ // which allows us to maintain the same semantics between deepEquals
66
+ // (all have to match, passing in an array if necessary) and the other
67
+ // predicates (any can match)
68
+ if (nodeValues && nodeValues.length === 1) {
69
+ return nodeValues[0];
70
+ }
71
+ else {
72
+ return nodeValues;
73
+ }
74
+ }
75
+
76
+ function orderIndependent (possibleArray) {
77
+ if (Array.isArray(possibleArray)) {
78
+ return possibleArray.sort(sortObjects);
79
+ }
80
+ else {
81
+ return possibleArray;
82
+ }
83
+ }
84
+
85
+ function transformObject (obj, transform) {
86
+ Object.keys(obj).forEach(key => {
87
+ obj[key] = transform(obj[key]);
88
+ });
89
+ return obj;
90
+ }
91
+
92
+ function selectXPath (config, encoding, text) {
93
+ const selectFn = combinators.curry(xPath.select, config.selector, config.ns, text);
94
+
95
+ return orderIndependent(select('xpath', selectFn, encoding));
96
+ }
97
+
98
+ function selectTransform (config, options, logger) {
99
+ const cloned = helpers.clone(config);
100
+
101
+ if (config.jsonpath) {
102
+ const stringTransform = options.shouldForceStrings ? forceStrings : combinators.identity;
103
+
104
+ // use keyCaseSensitive instead of caseSensitive to help "matches" predicates too
105
+ // see https://github.com/mountebank-testing/mountebank/issues/361
106
+ if (!cloned.keyCaseSensitive) {
107
+ cloned.jsonpath.selector = cloned.jsonpath.selector.toLowerCase();
108
+ }
109
+
110
+ return combinators.curry(selectJSONPath, cloned.jsonpath, options.encoding, config, stringTransform, logger);
111
+ }
112
+ else if (config.xpath) {
113
+ if (!cloned.caseSensitive) {
114
+ cloned.xpath.ns = transformObject(cloned.xpath.ns || {}, lowercase);
115
+ cloned.xpath.selector = cloned.xpath.selector.toLowerCase();
116
+ }
117
+ return combinators.curry(selectXPath, cloned.xpath, options.encoding);
118
+ }
119
+ else {
120
+ return combinators.identity;
121
+ }
122
+ }
123
+
124
+ function lowercase (text) {
125
+ return text.toLowerCase();
126
+ }
127
+
128
+ function caseTransform (config) {
129
+ return config.caseSensitive ? combinators.identity : lowercase;
130
+ }
131
+
132
+ function exceptTransform (config, logger) {
133
+ const exceptRegexOptions = config.caseSensitive ? 'g' : 'gi';
134
+
135
+ if (config.except) {
136
+ if (!safeRegex(config.except)) {
137
+ logger.warn(`If mountebank becomes unresponsive, it is because of this unsafe regular expression: ${config.except}`);
138
+ }
139
+ return text => text.replace(new RegExp(config.except, exceptRegexOptions), '');
140
+ }
141
+ else {
142
+ return combinators.identity;
143
+ }
144
+ }
145
+
146
+ function encodingTransform (encoding) {
147
+ if (encoding === 'base64') {
148
+ return text => Buffer.from(text, 'base64').toString();
149
+ }
150
+ else {
151
+ return combinators.identity;
152
+ }
153
+ }
154
+
155
+ function tryJSON (value, predicateConfig, logger) {
156
+ try {
157
+ const keyCaseTransform = predicateConfig.keyCaseSensitive === false ? lowercase : caseTransform(predicateConfig),
158
+ valueTransforms = [exceptTransform(predicateConfig, logger), caseTransform(predicateConfig)];
159
+
160
+ // We can't call normalize because we want to avoid the array sort transform,
161
+ // which will mess up indexed selectors like $..title[1]
162
+ return transformAll(JSON.parse(value), [keyCaseTransform], valueTransforms, []);
163
+ }
164
+ catch (e) {
165
+ return value;
166
+ }
167
+ }
168
+
169
+ // eslint-disable-next-line max-params
170
+ function selectJSONPath (config, encoding, predicateConfig, stringTransform, logger, text) {
171
+ const possibleJSON = stringTransform(tryJSON(text, predicateConfig, logger)),
172
+ selectFn = combinators.curry(jsonpath.select, config.selector, possibleJSON);
173
+
174
+ return orderIndependent(select('jsonpath', selectFn, encoding));
175
+ }
176
+
177
+ function transformAll (obj, keyTransforms, valueTransforms, arrayTransforms) {
178
+ const apply = fns => combinators.compose.apply(null, fns),
179
+ isObject = helpers.isObject;
180
+
181
+ if (Array.isArray(obj)) {
182
+ return apply(arrayTransforms)(obj.map(element => transformAll(element, keyTransforms, valueTransforms, arrayTransforms)));
183
+ }
184
+ else if (isObject(obj)) {
185
+ return Object.keys(obj).reduce((accumulator, key) => {
186
+ accumulator[apply(keyTransforms)(key)] = transformAll(obj[key], keyTransforms, valueTransforms, arrayTransforms);
187
+ return accumulator;
188
+ }, {});
189
+ }
190
+ else if (typeof obj === 'string') {
191
+ return apply(valueTransforms)(obj);
192
+ }
193
+ else {
194
+ return obj;
195
+ }
196
+ }
197
+
198
+ function normalize (obj, config, options, logger) {
199
+ // Needed to solve a tricky case conversion for "matches" predicates with jsonpath/xpath parameters
200
+ if (typeof config.keyCaseSensitive === 'undefined') {
201
+ config.keyCaseSensitive = config.caseSensitive;
202
+ }
203
+
204
+ const keyCaseTransform = config.keyCaseSensitive === false ? lowercase : caseTransform(config),
205
+ sortTransform = array => array.sort(sortObjects),
206
+ transforms = [];
207
+
208
+ if (options.withSelectors) {
209
+ transforms.push(selectTransform(config, options, logger));
210
+ }
211
+
212
+ transforms.push(exceptTransform(config, logger));
213
+ transforms.push(caseTransform(config));
214
+ transforms.push(encodingTransform(options.encoding));
215
+
216
+ // sort to provide deterministic comparison for deepEquals,
217
+ // where the order in the array for multi-valued querystring keys
218
+ // and xpath selections isn't important
219
+ return transformAll(obj, [keyCaseTransform], transforms, [sortTransform]);
220
+ }
221
+
222
+ function testPredicate (expected, actual, predicateConfig, predicateFn) {
223
+ if (!helpers.defined(actual)) {
224
+ actual = '';
225
+ }
226
+ if (helpers.isObject(expected)) {
227
+ return predicateSatisfied(expected, actual, predicateConfig, predicateFn);
228
+ }
229
+ else {
230
+ return predicateFn(expected, actual);
231
+ }
232
+ }
233
+
234
+ function bothArrays (expected, actual) {
235
+ return Array.isArray(actual) && Array.isArray(expected);
236
+ }
237
+
238
+ function allExpectedArrayValuesMatchActualArray (expectedArray, actualArray, predicateConfig, predicateFn) {
239
+ return expectedArray.every(expectedValue =>
240
+ actualArray.some(actualValue => testPredicate(expectedValue, actualValue, predicateConfig, predicateFn)));
241
+ }
242
+
243
+ function onlyActualIsArray (expected, actual) {
244
+ return Array.isArray(actual) && !Array.isArray(expected);
245
+ }
246
+
247
+ function expectedMatchesAtLeastOneValueInActualArray (expected, actualArray, predicateConfig, predicateFn) {
248
+ return actualArray.some(actual => testPredicate(expected, actual, predicateConfig, predicateFn));
249
+ }
250
+
251
+ function expectedLeftOffArraySyntaxButActualIsArrayOfObjects (expected, actual, fieldName) {
252
+ return !Array.isArray(expected[fieldName]) && !helpers.defined(actual[fieldName]) && Array.isArray(actual);
253
+ }
254
+
255
+ function predicateSatisfied (expected, actual, predicateConfig, predicateFn) {
256
+ if (!actual) {
257
+ return false;
258
+ }
259
+
260
+ // Support predicates that reach into fields encoded in JSON strings (e.g. HTTP bodies)
261
+ if (typeof actual === 'string') {
262
+ actual = tryJSON(actual, predicateConfig);
263
+ }
264
+
265
+ return Object.keys(expected).every(fieldName => {
266
+ const isObject = helpers.isObject;
267
+
268
+ if (bothArrays(expected[fieldName], actual[fieldName])) {
269
+ return allExpectedArrayValuesMatchActualArray(
270
+ expected[fieldName], actual[fieldName], predicateConfig, predicateFn);
271
+ }
272
+ else if (onlyActualIsArray(expected[fieldName], actual[fieldName])) {
273
+ if (predicateConfig.exists && expected[fieldName]) {
274
+ return true;
275
+ }
276
+ else {
277
+ return expectedMatchesAtLeastOneValueInActualArray(
278
+ expected[fieldName], actual[fieldName], predicateConfig, predicateFn);
279
+ }
280
+ }
281
+ else if (expectedLeftOffArraySyntaxButActualIsArrayOfObjects(expected, actual, fieldName)) {
282
+ // This is a little confusing, but predated the ability for users to specify an
283
+ // array for the expected values and is left for backwards compatibility.
284
+ // The predicate might be:
285
+ // { equals: { examples: { key: 'third' } } }
286
+ // and the request might be
287
+ // { examples: '[{ "key": "first" }, { "different": true }, { "key": "third" }]' }
288
+ // We expect that the "key" field in the predicate definition matches any object key
289
+ // in the actual array
290
+ return expectedMatchesAtLeastOneValueInActualArray(expected, actual, predicateConfig, predicateFn);
291
+ }
292
+ else if (isObject(expected[fieldName])) {
293
+ return predicateSatisfied(expected[fieldName], actual[fieldName], predicateConfig, predicateFn);
294
+ }
295
+ else {
296
+ return testPredicate(expected[fieldName], actual[fieldName], predicateConfig, predicateFn);
297
+ }
298
+ });
299
+ }
300
+
301
+ function create (operator, predicateFn) {
302
+ return (predicate, request, encoding, logger) => {
303
+ const expected = normalize(predicate[operator], predicate, { encoding: encoding }, logger),
304
+ actual = normalize(request, predicate, { encoding: encoding, withSelectors: true }, logger);
305
+
306
+ return predicateSatisfied(expected, actual, predicate, predicateFn);
307
+ };
308
+ }
309
+
310
+ function deepEquals (predicate, request, encoding, logger) {
311
+ const expected = normalize(forceStrings(predicate.deepEquals), predicate, { encoding: encoding }, logger),
312
+ actual = normalize(forceStrings(request), predicate, { encoding: encoding, withSelectors: true, shouldForceStrings: true }, logger),
313
+ isObject = helpers.isObject;
314
+
315
+ return Object.keys(expected).every(fieldName => {
316
+ // Support predicates that reach into fields encoded in JSON strings (e.g. HTTP bodies)
317
+ if (isObject(expected[fieldName]) && typeof actual[fieldName] === 'string') {
318
+ const possibleJSON = tryJSON(actual[fieldName], predicate);
319
+ actual[fieldName] = normalize(forceStrings(possibleJSON), predicate, { encoding: encoding }, logger);
320
+ }
321
+ return stringify(expected[fieldName]) === stringify(actual[fieldName]);
322
+ });
323
+ }
324
+
325
+ function matches (predicate, request, encoding, logger) {
326
+ // We want to avoid the lowerCase transform on values so we don't accidentally butcher
327
+ // a regular expression with upper case metacharacters like \W and \S
328
+ // However, we need to maintain the case transform for keys like http header names (issue #169)
329
+ // eslint-disable-next-line no-unneeded-ternary
330
+ const caseSensitive = predicate.caseSensitive ? true : false, // convert to boolean even if undefined
331
+ clone = helpers.merge(predicate, { caseSensitive: true, keyCaseSensitive: caseSensitive }),
332
+ noexcept = helpers.merge(clone, { except: '' }),
333
+ expected = normalize(predicate.matches, noexcept, { encoding: encoding }, logger),
334
+ actual = normalize(request, clone, { encoding: encoding, withSelectors: true }, logger),
335
+ options = caseSensitive ? '' : 'i';
336
+
337
+ if (encoding === 'base64') {
338
+ throw errors.ValidationError('the matches predicate is not allowed in binary mode');
339
+ }
340
+
341
+ return predicateSatisfied(expected, actual, clone, (a, b) => {
342
+ if (!safeRegex(a)) {
343
+ logger.warn(`If mountebank becomes unresponsive, it is because of this unsafe regular expression: ${a}`);
344
+ }
345
+ return new RegExp(a, options).test(b);
346
+ });
347
+ }
348
+
349
+ function not (predicate, request, encoding, logger, imposterState) {
350
+ return !evaluate(predicate.not, request, encoding, logger, imposterState);
351
+ }
352
+
353
+ function evaluateFn (request, encoding, logger, imposterState) {
354
+ return subPredicate => evaluate(subPredicate, request, encoding, logger, imposterState);
355
+ }
356
+
357
+ function or (predicate, request, encoding, logger, imposterState) {
358
+ return predicate.or.some(evaluateFn(request, encoding, logger, imposterState));
359
+ }
360
+
361
+ function and (predicate, request, encoding, logger, imposterState) {
362
+ return predicate.and.every(evaluateFn(request, encoding, logger, imposterState));
363
+ }
364
+
365
+ function inject (predicate, request, encoding, logger, imposterState) {
366
+ if (request.isDryRun === true) {
367
+ return true;
368
+ }
369
+
370
+ const config = {
371
+ request: helpers.clone(request),
372
+ state: imposterState,
373
+ logger: logger
374
+ };
375
+
376
+ compatibility.downcastInjectionConfig(config);
377
+
378
+ const injected = `(${predicate.inject})(config, logger, imposterState);`;
379
+
380
+ try {
381
+ return eval(injected);
382
+ }
383
+ catch (error) {
384
+ logger.error(`injection X=> ${error}`);
385
+ logger.error(` source: ${JSON.stringify(injected)}`);
386
+ logger.error(` config.request: ${JSON.stringify(config.request)}`);
387
+ logger.error(` config.state: ${JSON.stringify(config.state)}`);
388
+ throw errors.InjectionError('invalid predicate injection', { source: injected, data: error.message });
389
+ }
390
+ }
391
+
392
+ function toString (value) {
393
+ if (value !== null && typeof value !== 'undefined' && typeof value.toString === 'function') {
394
+ return value.toString();
395
+ }
396
+ else {
397
+ return value;
398
+ }
399
+ }
400
+
401
+ const predicates = {
402
+ equals: create('equals', (expected, actual) => toString(expected) === toString(actual)),
403
+ deepEquals,
404
+ contains: create('contains', (expected, actual) => actual.indexOf(expected) >= 0),
405
+ startsWith: create('startsWith', (expected, actual) => actual.indexOf(expected) === 0),
406
+ endsWith: create('endsWith', (expected, actual) => actual.indexOf(expected, actual.length - expected.length) >= 0),
407
+ matches,
408
+ exists: create('exists', function (expected, actual) {
409
+ return expected ? (typeof actual !== 'undefined' && actual !== '') : (typeof actual === 'undefined' || actual === '');
410
+ }),
411
+ not,
412
+ or,
413
+ and,
414
+ inject
415
+ };
416
+
417
+ /**
418
+ * Resolves all predicate keys in given predicate
419
+ * @param {Object} predicate - The predicate configuration
420
+ * @param {Object} request - The protocol request object
421
+ * @param {string} encoding - utf8 or base64
422
+ * @param {Object} logger - The logger, useful for debugging purposes
423
+ * @param {Object} imposterState - The current state for the imposter
424
+ * @returns {boolean}
425
+ */
426
+ function evaluate (predicate, request, encoding, logger, imposterState) {
427
+ const predicateFn = Object.keys(predicate).find(key => Object.keys(predicates).indexOf(key) >= 0),
428
+ clone = helpers.clone(predicate);
429
+
430
+ if (predicateFn) {
431
+ return predicates[predicateFn](clone, request, encoding, logger, imposterState);
432
+ }
433
+ else {
434
+ throw errors.ValidationError('missing predicate', { source: predicate });
435
+ }
436
+ }
437
+
438
+ module.exports = { evaluate };
@@ -0,0 +1,242 @@
1
+ 'use strict';
2
+
3
+ const responseResolver = require('./responseResolver'),
4
+ childProcess = require('child_process'),
5
+ fsExtra = require('fs-extra'),
6
+ path = require('path'),
7
+ errors = require('../util/errors.js'),
8
+ Imposter = require('./imposter.js'),
9
+ helpers = require('../util/helpers.js'),
10
+ tcpServer = require('./tcp/tcpServer.js'),
11
+ httpServer = require('./http/httpServer.js'),
12
+ httpsServer = require('./https/httpsServer.js'),
13
+ smtpServer = require('./smtp/smtpServer.js');
14
+
15
+ /**
16
+ * Abstracts the protocol configuration between the built-in in-memory implementations and out of process
17
+ * implementations
18
+ * @module
19
+ */
20
+
21
+ /**
22
+ * Loads the imposter creation functions for all built in and custom protocols
23
+ * @param builtInProtocols {Object} - the in-memory protocol implementations that ship with mountebank
24
+ * @param customProtocols {Object} - custom out-of-process protocol implementations
25
+ * @param options {Object} - command line configuration
26
+ * @param isAllowedConnection {Function} - a function that determines whether the connection is allowed or not for security verification
27
+ * @param mbLogger {Object} - the logger
28
+ * @param impostersRepository {Object} - the imposters repository
29
+ * @returns {Object} - a map of protocol name to creation functions
30
+ */
31
+ // eslint-disable-next-line max-params
32
+ function load (builtInProtocols, customProtocols, options, isAllowedConnection, mbLogger, impostersRepository) {
33
+ function inProcessCreate (createProtocol) {
34
+ return async (creationRequest, logger, responseFn) => {
35
+ const server = await createProtocol(creationRequest, logger, responseFn),
36
+ stubs = impostersRepository.stubsFor(server.port),
37
+ resolver = responseResolver.create(stubs, server.proxy);
38
+
39
+ return {
40
+ port: server.port,
41
+ metadata: server.metadata,
42
+ stubs: stubs,
43
+ resolver: resolver,
44
+ close: server.close,
45
+ encoding: server.encoding || 'utf8'
46
+ };
47
+ };
48
+ }
49
+
50
+ function outOfProcessCreate (protocolName, config) {
51
+ function customFieldsFor (creationRequest) {
52
+ const fields = {},
53
+ commonFields = ['protocol', 'port', 'name', 'recordRequests', 'stubs', 'defaultResponse'];
54
+ Object.keys(creationRequest).forEach(key => {
55
+ if (commonFields.indexOf(key) < 0) {
56
+ fields[key] = creationRequest[key];
57
+ }
58
+ });
59
+ return fields;
60
+ }
61
+
62
+ return (creationRequest, logger) => new Promise((res, rej) => {
63
+ let isPending = true;
64
+ const { spawn } = childProcess,
65
+ command = config.createCommand.split(' ')[0],
66
+ args = config.createCommand.split(' ').splice(1),
67
+ port = creationRequest.port,
68
+ commonArgs = {
69
+ port,
70
+ callbackURLTemplate: options.callbackURLTemplate,
71
+ loglevel: options.loglevel,
72
+ allowInjection: options.allowInjection
73
+ },
74
+ configArgs = helpers.merge(commonArgs, customFieldsFor(creationRequest)),
75
+ resolve = obj => {
76
+ isPending = false;
77
+ res(obj);
78
+ },
79
+ reject = err => {
80
+ isPending = false;
81
+ rej(err);
82
+ };
83
+
84
+ if (typeof creationRequest.defaultResponse !== 'undefined') {
85
+ configArgs.defaultResponse = creationRequest.defaultResponse;
86
+ }
87
+
88
+ const allArgs = args.concat(JSON.stringify(configArgs)),
89
+ imposterProcess = spawn(command, allArgs);
90
+
91
+ let closeCalled = false;
92
+
93
+ imposterProcess.on('error', error => {
94
+ const message = `Invalid configuration for protocol "${protocolName}": cannot run "${config.createCommand}"`;
95
+
96
+ reject(errors.ProtocolError(message,
97
+ { source: config.createCommand, details: error }));
98
+ });
99
+
100
+ imposterProcess.once('exit', code => {
101
+ if (code !== 0 && isPending) {
102
+ const message = `"${protocolName}" start command failed (exit code ${code})`;
103
+
104
+ reject(errors.ProtocolError(message, { source: config.createCommand }));
105
+ }
106
+ else if (!closeCalled) {
107
+ logger.error("Uh oh! I've crashed! Expect subsequent requests to fail.");
108
+ }
109
+ });
110
+
111
+ function resolveWithMetadata (possibleJSON) {
112
+ let metadata = {};
113
+
114
+ try {
115
+ metadata = JSON.parse(possibleJSON);
116
+ }
117
+ catch (error) { /* do nothing */ }
118
+
119
+ let serverPort = creationRequest.port;
120
+ if (metadata.port) {
121
+ serverPort = metadata.port;
122
+ delete metadata.port;
123
+ }
124
+ const callbackURL = options.callbackURLTemplate.replace(':port', serverPort),
125
+ encoding = metadata.encoding || 'utf8',
126
+ stubs = impostersRepository.stubsFor(serverPort),
127
+ resolver = responseResolver.create(stubs, undefined, callbackURL);
128
+
129
+ delete metadata.encoding;
130
+
131
+ resolve({
132
+ port: serverPort,
133
+ metadata: metadata,
134
+ stubs,
135
+ resolver,
136
+ encoding,
137
+ close: callback => {
138
+ closeCalled = true;
139
+ imposterProcess.once('exit', callback);
140
+ imposterProcess.kill();
141
+ }
142
+ });
143
+ }
144
+
145
+ function log (message) {
146
+ if (message.indexOf(' ') > 0) {
147
+ const words = message.split(' '),
148
+ level = words[0],
149
+ rest = words.splice(1).join(' ').trim();
150
+ if (['debug', 'info', 'warn', 'error'].indexOf(level) >= 0) {
151
+ logger[level](rest);
152
+ }
153
+ }
154
+ }
155
+
156
+ imposterProcess.stdout.on('data', data => {
157
+ const lines = data.toString('utf8').trim().split(/\r?\n/);
158
+ lines.forEach(line => {
159
+ if (isPending) {
160
+ resolveWithMetadata(line);
161
+ }
162
+ log(line);
163
+ });
164
+ });
165
+
166
+ imposterProcess.stderr.on('data', logger.error);
167
+ });
168
+ }
169
+
170
+ function createImposter (Protocol, creationRequest) {
171
+ return Imposter.create(Protocol, creationRequest, mbLogger.baseLogger, options, isAllowedConnection);
172
+ }
173
+
174
+ const result = {};
175
+ Object.keys(builtInProtocols).forEach(key => {
176
+ result[key] = builtInProtocols[key];
177
+ result[key].createServer = inProcessCreate(result[key].create);
178
+ result[key].createImposterFrom = creationRequest => createImposter(result[key], creationRequest);
179
+ });
180
+ Object.keys(customProtocols).forEach(key => {
181
+ result[key] = customProtocols[key];
182
+ result[key].createServer = outOfProcessCreate(key, result[key]);
183
+ result[key].createImposterFrom = creationRequest => createImposter(result[key], creationRequest);
184
+ });
185
+ return result;
186
+ }
187
+
188
+ function isBuiltInProtocol (protocol) {
189
+ return ['tcp', 'smtp', 'http', 'https'].indexOf(protocol) >= 0;
190
+ }
191
+
192
+ function loadCustomProtocols (protofile, logger) {
193
+ if (typeof protofile === 'undefined') {
194
+ return {};
195
+ }
196
+
197
+ const filename = path.resolve(path.relative(process.cwd(), protofile));
198
+
199
+ if (fsExtra.existsSync(filename)) {
200
+ try {
201
+ const customProtocols = require(filename);
202
+ Object.keys(customProtocols).forEach(proto => {
203
+ if (isBuiltInProtocol(proto)) {
204
+ logger.warn(`Using custom ${proto} implementation instead of the built-in one`);
205
+ }
206
+ else {
207
+ logger.info(`Loaded custom protocol ${proto}`);
208
+ }
209
+ });
210
+ return customProtocols;
211
+ }
212
+ catch (e) {
213
+ logger.error(`${protofile} contains invalid JSON -- no custom protocols loaded`);
214
+ return {};
215
+ }
216
+ }
217
+ else {
218
+ return {};
219
+ }
220
+ }
221
+
222
+ function loadProtocols (options, baseURL, logger, isAllowedConnection, imposters) {
223
+ const builtInProtocols = {
224
+ tcp: tcpServer,
225
+ http: httpServer,
226
+ https: httpsServer,
227
+ smtp: smtpServer
228
+ },
229
+ customProtocols = loadCustomProtocols(options.protofile, logger),
230
+ config = {
231
+ callbackURLTemplate: `${baseURL}/imposters/:port/_requests`,
232
+ recordRequests: options.mock,
233
+ recordMatches: options.debug,
234
+ loglevel: options.log.level,
235
+ allowInjection: options.allowInjection,
236
+ host: options.host
237
+ };
238
+
239
+ return load(builtInProtocols, customProtocols, config, isAllowedConnection, logger, imposters);
240
+ }
241
+
242
+ module.exports = { load, loadProtocols };