node-tdd 3.3.0 → 3.3.3
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/README.md +2 -2
- package/lib/index.js +12 -7
- package/lib/modules/env-manager.js +5 -10
- package/lib/modules/log-recorder.js +15 -16
- package/lib/modules/random-seeder.js +18 -21
- package/lib/modules/request-recorder/apply-modifiers.js +12 -19
- package/lib/modules/request-recorder/heal-sqs-send-message-batch.js +62 -33
- package/lib/modules/request-recorder/nock-listener.js +5 -7
- package/lib/modules/request-recorder/nock-mock.js +6 -9
- package/lib/modules/request-recorder/request-injector.js +8 -18
- package/lib/modules/request-recorder/util.js +11 -13
- package/lib/modules/request-recorder.js +142 -160
- package/lib/modules/time-keeper.js +14 -11
- package/lib/util/desc.js +73 -117
- package/lib/util/mocha-test.js +10 -10
- package/package.json +30 -62
|
@@ -1,54 +1,43 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const cloneDeep = require('lodash.clonedeep');
|
|
18
|
-
|
|
19
|
-
const compareUrls = require('compare-urls');
|
|
20
|
-
|
|
21
|
-
const nockCommon = require('nock/lib/common');
|
|
22
|
-
|
|
23
|
-
const nockListener = require('./request-recorder/nock-listener');
|
|
24
|
-
|
|
25
|
-
const nockMock = require('./request-recorder/nock-mock');
|
|
26
|
-
|
|
27
|
-
const healSqsSendMessageBatch = require('./request-recorder/heal-sqs-send-message-batch');
|
|
28
|
-
|
|
29
|
-
const applyModifiers = require('./request-recorder/apply-modifiers');
|
|
30
|
-
|
|
31
|
-
const {
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'smart-fs';
|
|
5
|
+
import Joi from 'joi-strict';
|
|
6
|
+
import nock from 'nock';
|
|
7
|
+
import cloneDeep from 'lodash.clonedeep';
|
|
8
|
+
import compareUrls from 'compare-urls';
|
|
9
|
+
import nockCommon from 'nock/lib/common.js';
|
|
10
|
+
import nockListener from './request-recorder/nock-listener.js';
|
|
11
|
+
import nockMock from './request-recorder/nock-mock.js';
|
|
12
|
+
import healSqsSendMessageBatch from './request-recorder/heal-sqs-send-message-batch.js';
|
|
13
|
+
import applyModifiers from './request-recorder/apply-modifiers.js';
|
|
14
|
+
import requestInjector from './request-recorder/request-injector.js';
|
|
15
|
+
|
|
16
|
+
import {
|
|
32
17
|
buildKey,
|
|
33
18
|
tryParseJson,
|
|
34
19
|
nullAsString,
|
|
35
20
|
convertHeaders,
|
|
36
21
|
rewriteHeaders
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const requestInjector = require('./request-recorder/request-injector');
|
|
22
|
+
} from './request-recorder/util.js';
|
|
40
23
|
|
|
41
24
|
const nockBack = nock.back;
|
|
42
25
|
const nockRecorder = nock.recorder;
|
|
43
26
|
|
|
44
|
-
|
|
27
|
+
export default (opts) => {
|
|
45
28
|
Joi.assert(opts, Joi.object().keys({
|
|
46
29
|
cassetteFolder: Joi.string(),
|
|
47
30
|
stripHeaders: Joi.boolean(),
|
|
48
|
-
reqHeaderOverwrite: Joi.object().pattern(
|
|
31
|
+
reqHeaderOverwrite: Joi.object().pattern(
|
|
32
|
+
Joi.string().case('lower'),
|
|
33
|
+
Joi.alternatives(Joi.string(), Joi.function())
|
|
34
|
+
),
|
|
49
35
|
strict: Joi.boolean(),
|
|
50
36
|
heal: Joi.alternatives(Joi.boolean(), Joi.string()),
|
|
51
|
-
modifiers: Joi.object().pattern(
|
|
37
|
+
modifiers: Joi.object().pattern(
|
|
38
|
+
Joi.string(),
|
|
39
|
+
Joi.alternatives(Joi.function(), Joi.link('#modifiers'))
|
|
40
|
+
)
|
|
52
41
|
}), 'Invalid Options Provided');
|
|
53
42
|
let nockDone = null;
|
|
54
43
|
let cassetteFilePath = null;
|
|
@@ -58,47 +47,44 @@ module.exports = opts => {
|
|
|
58
47
|
const expectedCassette = [];
|
|
59
48
|
const pendingMocks = [];
|
|
60
49
|
|
|
61
|
-
const anyFlagPresent = flags => {
|
|
50
|
+
const anyFlagPresent = (flags) => {
|
|
62
51
|
assert(Array.isArray(flags) && flags.length !== 0);
|
|
63
|
-
|
|
64
52
|
if (typeof opts.heal !== 'string') {
|
|
65
53
|
return false;
|
|
66
54
|
}
|
|
67
|
-
|
|
68
55
|
const needleFlags = opts.heal.split(',');
|
|
69
|
-
return flags.some(flag => needleFlags.includes(flag));
|
|
56
|
+
return flags.some((flag) => needleFlags.includes(flag));
|
|
70
57
|
};
|
|
71
58
|
|
|
72
59
|
const overwriteHeaders = (key, value, headers) => {
|
|
73
60
|
if (key in opts.reqHeaderOverwrite) {
|
|
74
|
-
return typeof opts.reqHeaderOverwrite[key] === 'function'
|
|
75
|
-
key,
|
|
76
|
-
|
|
77
|
-
headers
|
|
78
|
-
}) : opts.reqHeaderOverwrite[key];
|
|
61
|
+
return typeof opts.reqHeaderOverwrite[key] === 'function'
|
|
62
|
+
? opts.reqHeaderOverwrite[key]({ key, value, headers })
|
|
63
|
+
: opts.reqHeaderOverwrite[key];
|
|
79
64
|
}
|
|
80
|
-
|
|
81
65
|
return value;
|
|
82
66
|
};
|
|
83
67
|
|
|
84
|
-
return {
|
|
85
|
-
inject: async cassetteFile => {
|
|
68
|
+
return ({
|
|
69
|
+
inject: async (cassetteFile) => {
|
|
86
70
|
assert(nockDone === null);
|
|
87
71
|
knownCassetteNames.push(cassetteFile);
|
|
88
72
|
records.length = 0;
|
|
89
73
|
outOfOrderErrors.length = 0;
|
|
90
74
|
expectedCassette.length = 0;
|
|
91
75
|
pendingMocks.length = 0;
|
|
76
|
+
|
|
92
77
|
cassetteFilePath = path.join(opts.cassetteFolder, cassetteFile);
|
|
93
78
|
const hasCassette = fs.existsSync(cassetteFilePath);
|
|
94
|
-
|
|
95
79
|
if (hasCassette) {
|
|
96
80
|
const cassetteContent = fs.smartRead(cassetteFilePath);
|
|
97
|
-
pendingMocks.push(...nock
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
81
|
+
pendingMocks.push(...nock
|
|
82
|
+
.define(cassetteContent)
|
|
83
|
+
.map((e, idx) => ({
|
|
84
|
+
idx,
|
|
85
|
+
key: buildKey(e.interceptors[0]),
|
|
86
|
+
record: cassetteContent[idx]
|
|
87
|
+
})));
|
|
102
88
|
}
|
|
103
89
|
|
|
104
90
|
nockBack.setMode(hasCassette ? 'lockdown' : 'record');
|
|
@@ -106,12 +92,7 @@ module.exports = opts => {
|
|
|
106
92
|
nockMock.patch();
|
|
107
93
|
nockListener.subscribe('no match', () => {
|
|
108
94
|
assert(hasCassette === true);
|
|
109
|
-
const {
|
|
110
|
-
protocol,
|
|
111
|
-
options,
|
|
112
|
-
body
|
|
113
|
-
} = requestInjector.getLast();
|
|
114
|
-
|
|
95
|
+
const { protocol, options, body } = requestInjector.getLast();
|
|
115
96
|
if (anyFlagPresent(['record'])) {
|
|
116
97
|
expectedCassette.push(async () => {
|
|
117
98
|
nockRecorder.rec({
|
|
@@ -119,25 +100,20 @@ module.exports = opts => {
|
|
|
119
100
|
dont_print: true,
|
|
120
101
|
enable_reqheaders_recording: true
|
|
121
102
|
});
|
|
122
|
-
await new Promise(resolve => {
|
|
103
|
+
await new Promise((resolve) => {
|
|
123
104
|
options.protocol = `${protocol}:`;
|
|
124
|
-
const r = {
|
|
125
|
-
http,
|
|
126
|
-
https
|
|
127
|
-
}[protocol].request(options, response => {
|
|
105
|
+
const r = { http, https: http }[protocol].request(options, (response) => {
|
|
128
106
|
response.on('data', () => {});
|
|
129
107
|
response.on('end', resolve);
|
|
130
108
|
});
|
|
131
|
-
|
|
132
109
|
if (body !== undefined) {
|
|
133
110
|
r.write(body);
|
|
134
111
|
}
|
|
135
|
-
|
|
136
112
|
r.end();
|
|
137
113
|
});
|
|
138
114
|
const recorded = nockRecorder.play();
|
|
139
115
|
nockRecorder.clear();
|
|
140
|
-
return recorded.map(record => Object.assign(record, {
|
|
116
|
+
return recorded.map((record) => Object.assign(record, {
|
|
141
117
|
headers: opts.stripHeaders === true ? undefined : convertHeaders(record.rawHeaders),
|
|
142
118
|
rawHeaders: undefined,
|
|
143
119
|
reqheaders: rewriteHeaders(record.reqheaders, overwriteHeaders)
|
|
@@ -156,86 +132,93 @@ module.exports = opts => {
|
|
|
156
132
|
});
|
|
157
133
|
}
|
|
158
134
|
});
|
|
159
|
-
nockDone = await new Promise(resolve =>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
135
|
+
nockDone = await new Promise((resolve) => {
|
|
136
|
+
nockBack(cassetteFile, {
|
|
137
|
+
before: (scope, scopeIdx) => {
|
|
138
|
+
records.push(cloneDeep(scope));
|
|
139
|
+
applyModifiers(scope, opts.modifiers);
|
|
140
|
+
// eslint-disable-next-line no-param-reassign
|
|
141
|
+
scope.reqheaders = rewriteHeaders(
|
|
142
|
+
scope.reqheaders,
|
|
143
|
+
(k, valueRecording) => (valueRequest) => {
|
|
144
|
+
const match = nockCommon.matchStringOrRegexp(
|
|
145
|
+
valueRequest,
|
|
146
|
+
/^\^.*\$$/.test(valueRecording) ? new RegExp(valueRecording) : valueRecording
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (!match && anyFlagPresent(['magic', 'headers'])) {
|
|
150
|
+
const idx = pendingMocks.findIndex((m) => m.idx === scopeIdx);
|
|
151
|
+
// overwrite existing headers
|
|
152
|
+
pendingMocks[idx].record.reqheaders[k] = valueRequest;
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return match;
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
// eslint-disable-next-line no-param-reassign
|
|
160
|
+
scope.filteringRequestBody = (body) => {
|
|
161
|
+
if (anyFlagPresent(['magic', 'body'])) {
|
|
162
|
+
const idx = pendingMocks.findIndex((m) => m.idx === scopeIdx);
|
|
163
|
+
const requestBody = tryParseJson(body);
|
|
164
|
+
pendingMocks[idx].record.body = nullAsString(requestBody);
|
|
165
|
+
return scope.body;
|
|
166
|
+
}
|
|
167
|
+
return body;
|
|
168
|
+
};
|
|
169
|
+
// eslint-disable-next-line no-param-reassign
|
|
170
|
+
scope.filteringPath = (requestPath) => {
|
|
171
|
+
if (anyFlagPresent(['magic', 'path'])) {
|
|
172
|
+
const idx = pendingMocks.findIndex((m) => m.idx === scopeIdx);
|
|
173
|
+
if (!compareUrls(pendingMocks[idx].record.path, requestPath)) {
|
|
174
|
+
pendingMocks[idx].record.path = requestPath;
|
|
175
|
+
}
|
|
176
|
+
return scope.path;
|
|
177
|
+
}
|
|
178
|
+
return requestPath;
|
|
179
|
+
};
|
|
180
|
+
return scope;
|
|
181
|
+
},
|
|
182
|
+
after: (scope, scopeIdx) => {
|
|
183
|
+
scope.on('request', (req, interceptor, requestBodyString) => {
|
|
184
|
+
const idx = pendingMocks.findIndex((e) => e.idx === scopeIdx);
|
|
185
|
+
|
|
186
|
+
if (anyFlagPresent(['magic', 'headers'])) {
|
|
187
|
+
// add new headers
|
|
188
|
+
const reqheaders = {
|
|
189
|
+
...rewriteHeaders(req.headers),
|
|
190
|
+
...rewriteHeaders(pendingMocks[idx].record.reqheaders)
|
|
191
|
+
};
|
|
192
|
+
pendingMocks[idx].record.reqheaders = rewriteHeaders(reqheaders, overwriteHeaders);
|
|
195
193
|
}
|
|
196
194
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (anyFlagPresent(['magic', 'headers'])) {
|
|
210
|
-
// add new headers
|
|
211
|
-
const reqheaders = { ...rewriteHeaders(req.headers),
|
|
212
|
-
...rewriteHeaders(pendingMocks[idx].record.reqheaders)
|
|
213
|
-
};
|
|
214
|
-
pendingMocks[idx].record.reqheaders = rewriteHeaders(reqheaders, overwriteHeaders);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (anyFlagPresent(['magic', 'response'])) {
|
|
218
|
-
const responseBody = tryParseJson([healSqsSendMessageBatch].reduce((respBody, fn) => fn(requestBodyString, respBody, scope), interceptor.body)); // eslint-disable-next-line no-param-reassign
|
|
219
|
-
|
|
220
|
-
interceptor.body = responseBody;
|
|
221
|
-
pendingMocks[idx].record.response = responseBody;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
expectedCassette.push(pendingMocks[idx].record);
|
|
225
|
-
|
|
226
|
-
if (idx !== 0) {
|
|
227
|
-
outOfOrderErrors.push(pendingMocks[idx].key);
|
|
228
|
-
}
|
|
195
|
+
if (anyFlagPresent(['magic', 'response'])) {
|
|
196
|
+
const responseBody = tryParseJson([
|
|
197
|
+
healSqsSendMessageBatch
|
|
198
|
+
].reduce(
|
|
199
|
+
(respBody, fn) => fn(requestBodyString, respBody, scope),
|
|
200
|
+
interceptor.body
|
|
201
|
+
));
|
|
202
|
+
// eslint-disable-next-line no-param-reassign
|
|
203
|
+
interceptor.body = responseBody;
|
|
204
|
+
pendingMocks[idx].record.response = responseBody;
|
|
205
|
+
}
|
|
229
206
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
207
|
+
expectedCassette.push(pendingMocks[idx].record);
|
|
208
|
+
if (idx !== 0) {
|
|
209
|
+
outOfOrderErrors.push(pendingMocks[idx].key);
|
|
210
|
+
}
|
|
211
|
+
pendingMocks.splice(idx, 1);
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
afterRecord: (recordings) => JSON.stringify(recordings.map((r) => ({
|
|
215
|
+
...r,
|
|
216
|
+
body: tryParseJson(r.body),
|
|
217
|
+
rawHeaders: opts.stripHeaders === true ? undefined : r.rawHeaders,
|
|
218
|
+
reqheaders: rewriteHeaders(r.reqheaders, overwriteHeaders)
|
|
219
|
+
})), null, 2)
|
|
220
|
+
}, resolve);
|
|
221
|
+
});
|
|
239
222
|
requestInjector.inject();
|
|
240
223
|
},
|
|
241
224
|
release: async () => {
|
|
@@ -245,7 +228,7 @@ module.exports = opts => {
|
|
|
245
228
|
for (let idx = 0; idx < expectedCassette.length; idx += 1) {
|
|
246
229
|
if (typeof expectedCassette[idx] === 'function') {
|
|
247
230
|
// eslint-disable-next-line no-await-in-loop
|
|
248
|
-
expectedCassette.splice(idx, 1, ...
|
|
231
|
+
expectedCassette.splice(idx, 1, ...await expectedCassette[idx]());
|
|
249
232
|
idx -= 1;
|
|
250
233
|
}
|
|
251
234
|
}
|
|
@@ -256,26 +239,25 @@ module.exports = opts => {
|
|
|
256
239
|
nockMock.unpatch();
|
|
257
240
|
|
|
258
241
|
if (opts.heal !== false) {
|
|
259
|
-
fs.smartWrite(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
242
|
+
fs.smartWrite(
|
|
243
|
+
cassetteFilePath,
|
|
244
|
+
anyFlagPresent(['prune'])
|
|
245
|
+
? expectedCassette
|
|
246
|
+
: [...expectedCassette, ...pendingMocks.map(({ record }) => record)],
|
|
247
|
+
{ keepOrder: outOfOrderErrors.length === 0 && pendingMocks.length === 0 }
|
|
248
|
+
);
|
|
264
249
|
}
|
|
265
|
-
|
|
266
250
|
if (opts.strict !== false) {
|
|
267
251
|
if (outOfOrderErrors.length !== 0) {
|
|
268
252
|
throw new Error(`Out of Order Recordings: ${outOfOrderErrors.join(', ')}`);
|
|
269
253
|
}
|
|
270
|
-
|
|
271
254
|
if (pendingMocks.length !== 0) {
|
|
272
|
-
throw new Error(`Unmatched Recordings: ${pendingMocks.map(e => e.key).join(', ')}`);
|
|
255
|
+
throw new Error(`Unmatched Recordings: ${pendingMocks.map((e) => e.key).join(', ')}`);
|
|
273
256
|
}
|
|
274
257
|
}
|
|
275
258
|
},
|
|
276
259
|
shutdown: () => {
|
|
277
|
-
const unexpectedFiles = fs.walkDir(opts.cassetteFolder).filter(f => !knownCassetteNames.includes(f));
|
|
278
|
-
|
|
260
|
+
const unexpectedFiles = fs.walkDir(opts.cassetteFolder).filter((f) => !knownCassetteNames.includes(f));
|
|
279
261
|
if (unexpectedFiles.length !== 0) {
|
|
280
262
|
throw new Error(`Unexpected file(s) in cassette folder: ${unexpectedFiles.join(', ')}`);
|
|
281
263
|
}
|
|
@@ -283,9 +265,9 @@ module.exports = opts => {
|
|
|
283
265
|
get: () => ({
|
|
284
266
|
records: records.slice(),
|
|
285
267
|
outOfOrderErrors: outOfOrderErrors.slice(),
|
|
286
|
-
unmatchedRecordings: pendingMocks.map(e => e.key).slice(),
|
|
268
|
+
unmatchedRecordings: pendingMocks.map((e) => e.key).slice(),
|
|
287
269
|
expectedCassette: expectedCassette.slice(),
|
|
288
270
|
cassetteFilePath
|
|
289
271
|
})
|
|
290
|
-
};
|
|
291
|
-
};
|
|
272
|
+
});
|
|
273
|
+
};
|
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import Joi from 'joi-strict';
|
|
3
|
+
import tk from 'timekeeper';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const Joi = require('joi-strict');
|
|
6
|
-
|
|
7
|
-
const tk = require('timekeeper');
|
|
8
|
-
|
|
9
|
-
module.exports = opts => {
|
|
5
|
+
export default (opts) => {
|
|
10
6
|
Joi.assert(opts, Joi.object().keys({
|
|
11
|
-
timestamp: Joi.alternatives(
|
|
7
|
+
timestamp: Joi.alternatives(
|
|
8
|
+
Joi.number().integer().min(0),
|
|
9
|
+
Joi.date().iso()
|
|
10
|
+
)
|
|
12
11
|
}), 'Invalid Options Provided');
|
|
13
12
|
return {
|
|
14
13
|
inject: () => {
|
|
15
14
|
assert(tk.isKeepingTime() === false);
|
|
16
|
-
tk.freeze(
|
|
15
|
+
tk.freeze(
|
|
16
|
+
Number.isInteger(opts.timestamp)
|
|
17
|
+
? new Date(opts.timestamp * 1000)
|
|
18
|
+
: new Date(opts.timestamp)
|
|
19
|
+
);
|
|
17
20
|
},
|
|
18
21
|
release: () => {
|
|
19
22
|
assert(tk.isKeepingTime());
|
|
@@ -21,4 +24,4 @@ module.exports = opts => {
|
|
|
21
24
|
},
|
|
22
25
|
isInjected: () => tk.isKeepingTime()
|
|
23
26
|
};
|
|
24
|
-
};
|
|
27
|
+
};
|