http-snapshotter 0.1.4 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +4 -4
  2. package/index.js +84 -43
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -41,7 +41,7 @@ In this mode, http-snapshotter will prevent any real HTTP calls from happening b
41
41
 
42
42
  There is also a `SNAPSHOT=ignore` option to neither read nor write from snapshot files and do real network requests instead. This could be useful while writing a new test.
43
43
 
44
- Tip: When you do `SNAPSHOT=update` to create snapshots, run it against a single test, so you know what exact snapshots that one test created.
44
+ Tip: When you do `SNAPSHOT=update` to create snapshots, run it against a single test, so you know what exact snapshots that one test created/updated.
45
45
 
46
46
  Finally after getting all your tests to use snapshots, run your test runner against all your tests and then take a look at `<snapshots directory>/unused-snapshots.log` file to see which snapshot files haven't been used by your final test suite. You can delete unused snapshot files.
47
47
 
@@ -49,8 +49,8 @@ The tests of this library uses this library itself, check the `tests/` directory
49
49
 
50
50
  ## About snapshot files and its names
51
51
 
52
- A snapshot file name unique identifies a request. By default it is a combination of HTTP method + URL + body that makes a request unique.
53
- The hash of concatenated HTTP method + URL + body makes the file name suffix.
52
+ A snapshot file name uniquely identifies a request. By default it is a combination of HTTP method + URL + body that makes a request unique.
53
+ A SHA256 hash of concatenated HTTP method + URL + body makes the file name suffix.
54
54
 
55
55
  However you may want to specially handle some requests. e.g. DynamoDB calls also need the `x-amz-target` header to uniquely identify the request,
56
56
  because every call is a POST call with DynamoDB. You can add logic to create better snapshot files for this case:
@@ -134,7 +134,7 @@ test('Test behavior on a free account', async (t) => {
134
134
  }
135
135
  )
136
136
  }
137
- //
137
+
138
138
  return response;
139
139
  };
140
140
  attachResponseTransformer(interceptResponse);
package/index.js CHANGED
@@ -140,7 +140,15 @@ async function getSnapshotFileName(request) {
140
140
  }
141
141
 
142
142
  // NOTE: This isn't going to work on a test runner that uses multiple processes / workers
143
- const alreadyWrittenFiles = new Set();
143
+ /**
144
+ * @typedef {Promise<{
145
+ * snapshot: Snapshot,
146
+ * absoluteFilePath: string,
147
+ * fileName: string
148
+ * }>} ReadSnapshotReturnType
149
+ */
150
+ /** @type {Map<string, ReadSnapshotReturnType>} */
151
+ const alreadyWrittenFiles = new Map();
144
152
  const readFiles = new Set();
145
153
 
146
154
  /**
@@ -152,40 +160,48 @@ async function saveSnapshot(request, response) {
152
160
  // console.log(fileName);
153
161
 
154
162
  // Prevent multiple tests from having same snapshot
155
- if (alreadyWrittenFiles.has(absoluteFilePath)) return fileName;
156
- alreadyWrittenFiles.add(absoluteFilePath);
157
-
158
- let body;
159
- /** @type {'text' | 'json'} */
160
- let responseType;
161
- const contentType = response.headers.get('content-type') || '';
162
- if (contentType.includes('application/json') || contentType.includes('application/x-amz-json-1.0')) {
163
- responseType = 'json';
164
- body = await response.clone().json();
165
- } else {
166
- responseType = 'text';
167
- body = await response.clone().text();
163
+ if (alreadyWrittenFiles.has(absoluteFilePath)) {
164
+ return /** @type {ReadSnapshotReturnType} */ (alreadyWrittenFiles.get(absoluteFilePath));
168
165
  }
169
- /** @type {Snapshot} */
170
- const snapshot = {
171
- request: {
172
- method: request.method,
173
- url: request.url,
174
- headers: [...request.headers.entries()],
175
- body: await request.clone().text(),
176
- },
177
- responseType,
178
- response: {
179
- status: response.status,
180
- statusText: response.statusText,
181
- headers: [...response.headers.entries()],
182
- body,
183
- },
184
- fileSuffixKey,
166
+
167
+ /** @returns {ReadSnapshotReturnType} */
168
+ const saveFreshSnapshot = async () => {
169
+ let body;
170
+ /** @type {'text' | 'json'} */
171
+ let responseType;
172
+ const contentType = response.headers.get('content-type') || '';
173
+ if (contentType.includes('application/json') || contentType.includes('application/x-amz-json-1.0')) {
174
+ responseType = 'json';
175
+ body = await response.clone().json();
176
+ } else {
177
+ responseType = 'text';
178
+ body = await response.clone().text();
179
+ }
180
+ /** @type {Snapshot} */
181
+ const snapshot = {
182
+ request: {
183
+ method: request.method,
184
+ url: request.url,
185
+ headers: [...request.headers.entries()],
186
+ body: await request.clone().text(),
187
+ },
188
+ responseType,
189
+ response: {
190
+ status: response.status,
191
+ statusText: response.statusText,
192
+ headers: [...response.headers.entries()],
193
+ body,
194
+ },
195
+ fileSuffixKey,
196
+ };
197
+ const json = JSON.stringify(snapshot, null, 2);
198
+ await fs.writeFile(absoluteFilePath, json, 'utf-8');
199
+ return { snapshot, absoluteFilePath, fileName };
185
200
  };
186
- const json = JSON.stringify(snapshot, null, 2);
187
- await fs.writeFile(absoluteFilePath, json, 'utf-8');
188
- return fileName;
201
+
202
+ const savePromise = saveFreshSnapshot();
203
+ alreadyWrittenFiles.set(absoluteFilePath, savePromise);
204
+ return savePromise;
189
205
  }
190
206
 
191
207
  /** @type {Record<string, Snapshot>} */
@@ -194,7 +210,7 @@ const snapshotCache = {};
194
210
  /**
195
211
  * @param {Request} request
196
212
  */
197
- async function enforceSnapshotResponse(request) {
213
+ async function readSnapshot(request) {
198
214
  const { absoluteFilePath, fileName, fileSuffixKey } = await getSnapshotFileName(request);
199
215
  // console.log(fileName);
200
216
 
@@ -221,14 +237,22 @@ async function enforceSnapshotResponse(request) {
221
237
  } else {
222
238
  // @ts-ignore
223
239
  console.error('Error reading network snapshot file:', err.message);
240
+ throw err;
224
241
  }
225
- return null;
226
242
  }
227
243
  snapshotCache[absoluteFilePath] = JSON.parse(json);
228
244
  readFiles.add(fileName);
229
245
  }
230
246
 
231
247
  const snapshot = snapshotCache[absoluteFilePath];
248
+ return { snapshot, absoluteFilePath, fileName };
249
+ }
250
+
251
+ /**
252
+ * @param {Request} request
253
+ * @param {Snapshot} snapshot
254
+ */
255
+ async function sendResponse(request, snapshot) {
232
256
  const {
233
257
  responseType,
234
258
  response: {
@@ -258,6 +282,23 @@ async function enforceSnapshotResponse(request) {
258
282
  return newResponse;
259
283
  }
260
284
 
285
+ /**
286
+ * @param {Request} request
287
+ */
288
+ async function readSnapshotAndSendResponse(request) {
289
+ const { snapshot } = await readSnapshot(request);
290
+ return sendResponse(request, snapshot);
291
+ }
292
+
293
+ /**
294
+ * @param {Request} request
295
+ * @param {Response} response
296
+ */
297
+ async function saveSnapshotAndSendResponse(request, response) {
298
+ const { snapshot } = await saveSnapshot(request, response);
299
+ return sendResponse(request, snapshot);
300
+ }
301
+
261
302
  /** @typedef {import('@mswjs/interceptors/ClientRequest').ClientRequestInterceptor} ClientRequestInterceptorType */
262
303
  /** @typedef {import('@mswjs/interceptors/fetch').FetchInterceptor} FetchInterceptorType */
263
304
  /**
@@ -376,7 +417,7 @@ function start({
376
417
  // @ts-ignore
377
418
  interceptor.on('request', async ({ request }) => {
378
419
  if (SNAPSHOT === 'read') {
379
- await enforceSnapshotResponse(request);
420
+ await readSnapshotAndSendResponse(request);
380
421
  }
381
422
  });
382
423
  interceptor.on(
@@ -384,13 +425,6 @@ function start({
384
425
  'response',
385
426
  /** @type {(params: { request: Request, response: Response }) => Promise<void>} */
386
427
  async ({ request, response }) => {
387
- if (SNAPSHOT === 'update') {
388
- if (!dirCreatePromise) {
389
- dirCreatePromise = fs.mkdir( /** @type {string} */(snapshotDirectory), { recursive: true });
390
- }
391
- await dirCreatePromise;
392
- saveSnapshot(request, response);
393
- }
394
428
  if (LOG_REQ) {
395
429
  const { fileName, fileSuffixKey } = await getSnapshotFileName(request);
396
430
  console.debug('Request', {
@@ -404,6 +438,13 @@ function start({
404
438
  wouldBeFileSuffixKey: fileSuffixKey,
405
439
  });
406
440
  }
441
+ if (SNAPSHOT === 'update') {
442
+ if (!dirCreatePromise) {
443
+ dirCreatePromise = fs.mkdir( /** @type {string} */(snapshotDirectory), { recursive: true });
444
+ }
445
+ await dirCreatePromise;
446
+ await saveSnapshotAndSendResponse(request, response);
447
+ }
407
448
  },
408
449
  );
409
450
  interceptor.apply();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "http-snapshotter",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Snapshot HTTP requests for tests (node.js)",
5
5
  "main": "index.cjs",
6
6
  "types": "index.d.ts",