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 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 getSnapshotFileName(request) {
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 {object} param
171
- * @param {Request} param.request
172
- * @param {Response} param.response
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
- request,
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 } = await getSnapshotFileName(request);
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 {Request} request
311
+ * @param {RequestController} controller
311
312
  * @param {Snapshot} snapshot
312
313
  */
313
- async function sendResponse(request, snapshot) {
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
- responseType === 'json'
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
- request.respondWith(newResponse);
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(request, snapshot);
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 readSnapshotAndSendResponse(request);
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 { absoluteFilePath, fileName, fileSuffixKey } = await getSnapshotFileName(request);
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.4.2",
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.24.1",
27
+ "@mswjs/interceptors": "^0.37.5",
28
28
  "@sindresorhus/slugify": "^1.1.2"
29
29
  },
30
30
  "devDependencies": {