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