http-snapshotter 0.4.2 → 0.5.0-beta.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 +4 -0
- package/index.d.ts +11 -0
- package/index.js +65 -34
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,10 +25,14 @@ import { resolve, dirname } from "node:path";
|
|
|
25
25
|
import { start, startTestCase, endTestCase } from "http-snapshotter";
|
|
26
26
|
|
|
27
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
// if you are using an isolated test runner, then use a different directory per test (e.g. http-snapshots/test-case-1)
|
|
28
29
|
start({ snapshotDirectory: resolve(__dirname, "http-snapshots") });
|
|
29
30
|
|
|
30
31
|
test("Latest XKCD comic (ESM)", async (t) => {
|
|
32
|
+
// if you are *not* using an isolated test runner (e.g. tape), then `startTestCase` adds snapshots to separate directory
|
|
33
|
+
// Remove this line if it doesn't apply to your test runner
|
|
31
34
|
startTestCase('test-case-1');
|
|
35
|
+
|
|
32
36
|
const res = await fetch("https://xkcd.com/info.0.json");
|
|
33
37
|
const json = await res.json();
|
|
34
38
|
|
package/index.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export type SnapshotJson = {
|
|
|
33
33
|
};
|
|
34
34
|
};
|
|
35
35
|
export type Snapshot = SnapshotText | SnapshotJson;
|
|
36
|
+
export type SnapshotFileInfo = Awaited<ReturnType<typeof getSnapshotFileInfo>>;
|
|
36
37
|
export type ReadSnapshotReturnType = Promise<{
|
|
37
38
|
snapshot: Snapshot;
|
|
38
39
|
absoluteFilePath: string;
|
|
@@ -92,3 +93,13 @@ export function start({ snapshotDirectory: _snapshotDirectory, }?: {
|
|
|
92
93
|
}): void;
|
|
93
94
|
/** Stop the interceptor */
|
|
94
95
|
export function stop(): void;
|
|
96
|
+
/**
|
|
97
|
+
* @param {Request} request
|
|
98
|
+
*/
|
|
99
|
+
declare function getSnapshotFileInfo(request: Request): Promise<{
|
|
100
|
+
absoluteFilePath: string;
|
|
101
|
+
fileName: string;
|
|
102
|
+
filePrefix: string;
|
|
103
|
+
fileSuffixKey: string;
|
|
104
|
+
}>;
|
|
105
|
+
export {};
|
package/index.js
CHANGED
|
@@ -27,13 +27,19 @@
|
|
|
27
27
|
* More docs at the end of this file, find the exported methods.
|
|
28
28
|
*/
|
|
29
29
|
// Tested with @mswjs/interceptors v0.24.1
|
|
30
|
-
const { BatchInterceptor } = require('@mswjs/interceptors');
|
|
30
|
+
const { BatchInterceptor, RequestController } = require('@mswjs/interceptors');
|
|
31
31
|
const { ClientRequestInterceptor } = require('@mswjs/interceptors/ClientRequest');
|
|
32
32
|
const { FetchInterceptor } = require('@mswjs/interceptors/fetch');
|
|
33
33
|
const slugify = require('@sindresorhus/slugify');
|
|
34
34
|
const { createHash } = require('node:crypto');
|
|
35
35
|
const { promises: fs } = require('node:fs');
|
|
36
36
|
const { resolve, dirname, relative } = require('node:path');
|
|
37
|
+
const zlib = require('node:zlib');
|
|
38
|
+
const { promisify } = require('node:util');
|
|
39
|
+
|
|
40
|
+
const gzip = promisify(zlib.gzip);
|
|
41
|
+
const brotliCompress = promisify(zlib.brotliCompress);
|
|
42
|
+
const deflate = promisify(zlib.deflate);
|
|
37
43
|
|
|
38
44
|
// Environment variable SNAPSHOT = update / append / ignore / read (default)
|
|
39
45
|
const SNAPSHOT = process.env.SNAPSHOT || 'read';
|
|
@@ -96,7 +102,7 @@ async function defaultSnapshotFileNameGenerator(request) {
|
|
|
96
102
|
filePrefix = [
|
|
97
103
|
'dynamodb',
|
|
98
104
|
matches[1], // e.g. eu-west-1
|
|
99
|
-
slugify(request.headers?.get?.('x-amz-target')?.split?.('.')?.pop?.() || ''),
|
|
105
|
+
slugify(request.headers?.get?.('x-amz-target')?.split?.('.')?.pop?.() || ''), // e.g. get-item, put-item
|
|
100
106
|
slugify(JSON.parse(await request.clone().text())?.TableName),
|
|
101
107
|
].filter(Boolean).join('-');
|
|
102
108
|
} else {
|
|
@@ -134,7 +140,7 @@ let snapshotSubDirectory = '';
|
|
|
134
140
|
/**
|
|
135
141
|
* @param {Request} request
|
|
136
142
|
*/
|
|
137
|
-
async function
|
|
143
|
+
async function getSnapshotFileInfo(request) {
|
|
138
144
|
const { fileSuffixKey, filePrefix } = await snapshotFileNameGenerator(request.clone());
|
|
139
145
|
|
|
140
146
|
// 15 characters are enough for uniqueness
|
|
@@ -153,6 +159,8 @@ async function getSnapshotFileName(request) {
|
|
|
153
159
|
};
|
|
154
160
|
}
|
|
155
161
|
|
|
162
|
+
/** @typedef {Awaited<ReturnType<getSnapshotFileInfo>>} SnapshotFileInfo */
|
|
163
|
+
|
|
156
164
|
// NOTE: This isn't going to work on a test runner that uses multiple processes / workers
|
|
157
165
|
/**
|
|
158
166
|
* @typedef {Promise<{
|
|
@@ -167,20 +175,12 @@ const readFiles = new Set();
|
|
|
167
175
|
const existingSubDirectories = new Set();
|
|
168
176
|
|
|
169
177
|
/**
|
|
170
|
-
* @param {
|
|
171
|
-
* @param {
|
|
172
|
-
* @param {
|
|
173
|
-
* @param {string} param.absoluteFilePath
|
|
174
|
-
* @param {string} param.fileName
|
|
175
|
-
* @param {string} param.fileSuffixKey
|
|
178
|
+
* @param {Request} request
|
|
179
|
+
* @param {Response} response
|
|
180
|
+
* @param {SnapshotFileInfo} snapshotFileInfo
|
|
176
181
|
*/
|
|
177
|
-
async function saveSnapshot({
|
|
178
|
-
|
|
179
|
-
response,
|
|
180
|
-
absoluteFilePath,
|
|
181
|
-
fileName,
|
|
182
|
-
fileSuffixKey,
|
|
183
|
-
}) {
|
|
182
|
+
async function saveSnapshot(request, response, snapshotFileInfo) {
|
|
183
|
+
const { absoluteFilePath, fileName, fileSuffixKey } = snapshotFileInfo;
|
|
184
184
|
// Prevent multiple tests from having same snapshot
|
|
185
185
|
if (alreadyWrittenFiles.has(absoluteFilePath)) {
|
|
186
186
|
return /** @type {ReadSnapshotReturnType} */ (alreadyWrittenFiles.get(absoluteFilePath));
|
|
@@ -264,9 +264,10 @@ const snapshotCache = {};
|
|
|
264
264
|
|
|
265
265
|
/**
|
|
266
266
|
* @param {Request} request
|
|
267
|
+
* @param {SnapshotFileInfo} snapshotFileInfo
|
|
267
268
|
*/
|
|
268
|
-
async function readSnapshot(request) {
|
|
269
|
-
const { absoluteFilePath, fileName, fileSuffixKey } =
|
|
269
|
+
async function readSnapshot(request, snapshotFileInfo) {
|
|
270
|
+
const { absoluteFilePath, fileName, fileSuffixKey } = snapshotFileInfo;
|
|
270
271
|
|
|
271
272
|
if (!snapshotCache[absoluteFilePath]) {
|
|
272
273
|
if (LOG_SNAPSHOT) {
|
|
@@ -307,10 +308,10 @@ async function readSnapshot(request) {
|
|
|
307
308
|
}
|
|
308
309
|
|
|
309
310
|
/**
|
|
310
|
-
* @param {
|
|
311
|
+
* @param {RequestController} controller
|
|
311
312
|
* @param {Snapshot} snapshot
|
|
312
313
|
*/
|
|
313
|
-
async function sendResponse(
|
|
314
|
+
async function sendResponse(controller, snapshot) {
|
|
314
315
|
const {
|
|
315
316
|
responseType,
|
|
316
317
|
response: {
|
|
@@ -321,10 +322,31 @@ async function sendResponse(request, snapshot) {
|
|
|
321
322
|
},
|
|
322
323
|
} = snapshot;
|
|
323
324
|
|
|
325
|
+
/** @type {string|Buffer} */
|
|
326
|
+
let encodedBody = responseType === 'json'
|
|
327
|
+
? JSON.stringify(body)
|
|
328
|
+
: /** @type {string} */ (body);
|
|
329
|
+
const contentEncoding = headers.find(tuple => tuple[0]?.toLowerCase() === 'content-encoding');
|
|
330
|
+
|
|
331
|
+
if (contentEncoding) {
|
|
332
|
+
if (contentEncoding[1].includes('br')) {
|
|
333
|
+
encodedBody = await brotliCompress(encodedBody);
|
|
334
|
+
} else if (contentEncoding[1].includes('gzip')) {
|
|
335
|
+
encodedBody = await gzip(encodedBody);
|
|
336
|
+
} else if (contentEncoding[1].includes('deflate')) {
|
|
337
|
+
encodedBody = await deflate(encodedBody);
|
|
338
|
+
} else if (contentEncoding[1].includes('compress')) {
|
|
339
|
+
// Most servers don't send compress responses and node.js
|
|
340
|
+
// doesn't have built-in compress support even for fetch().
|
|
341
|
+
throw new Error('compress encoding not supported');
|
|
342
|
+
} else if (contentEncoding[1].includes('zstd')) {
|
|
343
|
+
// Node.js doesn't have built-in zstd support at the moment
|
|
344
|
+
throw new Error('zstd encoding not supported');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
324
348
|
const newResponse = new Response(
|
|
325
|
-
|
|
326
|
-
? JSON.stringify(body)
|
|
327
|
-
: /** @type {string} */ (body),
|
|
349
|
+
encodedBody,
|
|
328
350
|
{
|
|
329
351
|
status,
|
|
330
352
|
statusText,
|
|
@@ -332,19 +354,20 @@ async function sendResponse(request, snapshot) {
|
|
|
332
354
|
},
|
|
333
355
|
);
|
|
334
356
|
|
|
335
|
-
// respondWith is a method added by @mswjs/interceptors
|
|
336
357
|
// @ts-ignore
|
|
337
|
-
|
|
358
|
+
controller.respondWith(newResponse);
|
|
338
359
|
return newResponse;
|
|
339
360
|
}
|
|
340
361
|
|
|
341
362
|
/**
|
|
342
363
|
* @param {Request} request
|
|
364
|
+
* @param {RequestController} controller
|
|
365
|
+
* @param {SnapshotFileInfo} snapshotFileInfo
|
|
343
366
|
*/
|
|
344
|
-
async function readSnapshotAndSendResponse(request) {
|
|
345
|
-
const { snapshot } = await readSnapshot(request);
|
|
367
|
+
async function readSnapshotAndSendResponse(request, controller, snapshotFileInfo) {
|
|
368
|
+
const { snapshot } = await readSnapshot(request, snapshotFileInfo);
|
|
346
369
|
if (snapshot) {
|
|
347
|
-
return sendResponse(
|
|
370
|
+
return sendResponse(controller, snapshot);
|
|
348
371
|
}
|
|
349
372
|
return undefined;
|
|
350
373
|
}
|
|
@@ -464,10 +487,14 @@ function start({
|
|
|
464
487
|
],
|
|
465
488
|
});
|
|
466
489
|
|
|
490
|
+
const cache = /** @type {WeakMap<Request, SnapshotFileInfo>} */ (new WeakMap());
|
|
491
|
+
|
|
467
492
|
// @ts-ignore
|
|
468
|
-
interceptor.on('request', async ({ request }) => {
|
|
493
|
+
interceptor.on('request', async ({ request, controller }) => {
|
|
469
494
|
if (['read', 'append'].includes(SNAPSHOT)) {
|
|
470
|
-
await
|
|
495
|
+
const snapshotFileInfo = await getSnapshotFileInfo(request);
|
|
496
|
+
cache.set(request, snapshotFileInfo);
|
|
497
|
+
await readSnapshotAndSendResponse(request, controller, snapshotFileInfo);
|
|
471
498
|
}
|
|
472
499
|
});
|
|
473
500
|
interceptor.on(
|
|
@@ -475,7 +502,13 @@ function start({
|
|
|
475
502
|
'response',
|
|
476
503
|
/** @type {(params: { request: Request, response: Response }) => Promise<void>} */
|
|
477
504
|
async ({ request, response }) => {
|
|
478
|
-
const
|
|
505
|
+
const snapshotFileInfo = cache.get(request) || (await getSnapshotFileInfo(request));
|
|
506
|
+
cache.delete(request);
|
|
507
|
+
const {
|
|
508
|
+
// absoluteFilePath,
|
|
509
|
+
fileName,
|
|
510
|
+
fileSuffixKey,
|
|
511
|
+
} = snapshotFileInfo;
|
|
479
512
|
if (LOG_REQ) {
|
|
480
513
|
const summary = `----------\n${request.method} ${request.url}\nWould use file name: ${fileName}`;
|
|
481
514
|
if (LOG_REQ === '1' || LOG_REQ === 'summary') {
|
|
@@ -503,9 +536,7 @@ function start({
|
|
|
503
536
|
dirCreatePromise = fs.mkdir( /** @type {string} */(snapshotDirectory), { recursive: true });
|
|
504
537
|
}
|
|
505
538
|
await dirCreatePromise;
|
|
506
|
-
await saveSnapshot(
|
|
507
|
-
request, response, absoluteFilePath, fileName, fileSuffixKey,
|
|
508
|
-
});
|
|
539
|
+
await saveSnapshot(request, response, snapshotFileInfo);
|
|
509
540
|
}
|
|
510
541
|
},
|
|
511
542
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "http-snapshotter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-beta.0",
|
|
4
4
|
"description": "Snapshot HTTP requests for tests (node.js)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"url": "https://github.com/Munawwar/http-snapshotter/issues"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@mswjs/interceptors": "^0.
|
|
27
|
+
"@mswjs/interceptors": "^0.37.5",
|
|
28
28
|
"@sindresorhus/slugify": "^1.1.2"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|