node-tdd 3.3.1 → 3.4.0
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 +12 -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 +149 -159
- package/lib/modules/time-keeper.js +14 -11
- package/lib/util/desc.js +73 -118
- 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,101 @@ 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
|
+
// https://github.com/nock/nock/blob/79ee0429050af929c525ae21a326d22796344bfc/lib/interceptor.js#L616
|
|
187
|
+
if (Number.isInteger(pendingMocks[idx]?.record?.delayConnection)) {
|
|
188
|
+
interceptor.delayConnection(pendingMocks[idx].record.delayConnection);
|
|
189
|
+
}
|
|
190
|
+
if (Number.isInteger(pendingMocks[idx]?.record?.delayBody)) {
|
|
191
|
+
interceptor.delayBody(pendingMocks[idx].record.delayBody);
|
|
195
192
|
}
|
|
196
193
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
after: (scope, scopeIdx) => {
|
|
206
|
-
scope.on('request', (req, interceptor, requestBodyString) => {
|
|
207
|
-
const idx = pendingMocks.findIndex(e => e.idx === scopeIdx);
|
|
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);
|
|
194
|
+
if (anyFlagPresent(['magic', 'headers'])) {
|
|
195
|
+
// add new headers
|
|
196
|
+
const reqheaders = {
|
|
197
|
+
...rewriteHeaders(req.headers),
|
|
198
|
+
...rewriteHeaders(pendingMocks[idx].record.reqheaders)
|
|
199
|
+
};
|
|
200
|
+
pendingMocks[idx].record.reqheaders = rewriteHeaders(reqheaders, overwriteHeaders);
|
|
201
|
+
}
|
|
225
202
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
203
|
+
if (anyFlagPresent(['magic', 'response'])) {
|
|
204
|
+
const responseBody = tryParseJson([
|
|
205
|
+
healSqsSendMessageBatch
|
|
206
|
+
].reduce(
|
|
207
|
+
(respBody, fn) => fn(requestBodyString, respBody, scope),
|
|
208
|
+
interceptor.body
|
|
209
|
+
));
|
|
210
|
+
// eslint-disable-next-line no-param-reassign
|
|
211
|
+
interceptor.body = responseBody;
|
|
212
|
+
pendingMocks[idx].record.response = responseBody;
|
|
213
|
+
}
|
|
229
214
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
215
|
+
expectedCassette.push(pendingMocks[idx].record);
|
|
216
|
+
if (idx !== 0) {
|
|
217
|
+
outOfOrderErrors.push(pendingMocks[idx].key);
|
|
218
|
+
}
|
|
219
|
+
pendingMocks.splice(idx, 1);
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
afterRecord: (recordings) => JSON.stringify(recordings.map((r) => ({
|
|
223
|
+
...r,
|
|
224
|
+
body: tryParseJson(r.body),
|
|
225
|
+
rawHeaders: opts.stripHeaders === true ? undefined : r.rawHeaders,
|
|
226
|
+
reqheaders: rewriteHeaders(r.reqheaders, overwriteHeaders)
|
|
227
|
+
})), null, 2)
|
|
228
|
+
}, resolve);
|
|
229
|
+
});
|
|
239
230
|
requestInjector.inject();
|
|
240
231
|
},
|
|
241
232
|
release: async () => {
|
|
@@ -245,7 +236,7 @@ module.exports = opts => {
|
|
|
245
236
|
for (let idx = 0; idx < expectedCassette.length; idx += 1) {
|
|
246
237
|
if (typeof expectedCassette[idx] === 'function') {
|
|
247
238
|
// eslint-disable-next-line no-await-in-loop
|
|
248
|
-
expectedCassette.splice(idx, 1, ...
|
|
239
|
+
expectedCassette.splice(idx, 1, ...await expectedCassette[idx]());
|
|
249
240
|
idx -= 1;
|
|
250
241
|
}
|
|
251
242
|
}
|
|
@@ -256,26 +247,25 @@ module.exports = opts => {
|
|
|
256
247
|
nockMock.unpatch();
|
|
257
248
|
|
|
258
249
|
if (opts.heal !== false) {
|
|
259
|
-
fs.smartWrite(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
250
|
+
fs.smartWrite(
|
|
251
|
+
cassetteFilePath,
|
|
252
|
+
anyFlagPresent(['prune'])
|
|
253
|
+
? expectedCassette
|
|
254
|
+
: [...expectedCassette, ...pendingMocks.map(({ record }) => record)],
|
|
255
|
+
{ keepOrder: outOfOrderErrors.length === 0 && pendingMocks.length === 0 }
|
|
256
|
+
);
|
|
264
257
|
}
|
|
265
|
-
|
|
266
258
|
if (opts.strict !== false) {
|
|
267
259
|
if (outOfOrderErrors.length !== 0) {
|
|
268
260
|
throw new Error(`Out of Order Recordings: ${outOfOrderErrors.join(', ')}`);
|
|
269
261
|
}
|
|
270
|
-
|
|
271
262
|
if (pendingMocks.length !== 0) {
|
|
272
|
-
throw new Error(`Unmatched Recordings: ${pendingMocks.map(e => e.key).join(', ')}`);
|
|
263
|
+
throw new Error(`Unmatched Recordings: ${pendingMocks.map((e) => e.key).join(', ')}`);
|
|
273
264
|
}
|
|
274
265
|
}
|
|
275
266
|
},
|
|
276
267
|
shutdown: () => {
|
|
277
|
-
const unexpectedFiles = fs.walkDir(opts.cassetteFolder).filter(f => !knownCassetteNames.includes(f));
|
|
278
|
-
|
|
268
|
+
const unexpectedFiles = fs.walkDir(opts.cassetteFolder).filter((f) => !knownCassetteNames.includes(f));
|
|
279
269
|
if (unexpectedFiles.length !== 0) {
|
|
280
270
|
throw new Error(`Unexpected file(s) in cassette folder: ${unexpectedFiles.join(', ')}`);
|
|
281
271
|
}
|
|
@@ -283,9 +273,9 @@ module.exports = opts => {
|
|
|
283
273
|
get: () => ({
|
|
284
274
|
records: records.slice(),
|
|
285
275
|
outOfOrderErrors: outOfOrderErrors.slice(),
|
|
286
|
-
unmatchedRecordings: pendingMocks.map(e => e.key).slice(),
|
|
276
|
+
unmatchedRecordings: pendingMocks.map((e) => e.key).slice(),
|
|
287
277
|
expectedCassette: expectedCassette.slice(),
|
|
288
278
|
cassetteFilePath
|
|
289
279
|
})
|
|
290
|
-
};
|
|
291
|
-
};
|
|
280
|
+
});
|
|
281
|
+
};
|
|
@@ -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
|
+
};
|