http-snapshotter 0.5.2 → 0.5.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 +27 -0
- package/index.js +87 -3
- package/intercept.js +78 -0
- package/package.json +4 -1
- /package/{test.js → playground.js} +0 -0
package/README.md
CHANGED
|
@@ -62,6 +62,12 @@ Once you are done writing your tests, run your test runner on all your tests and
|
|
|
62
62
|
|
|
63
63
|
The tests of this library uses this library itself, check the `tests/` directory and try the tests `npm ci; npm test`.
|
|
64
64
|
|
|
65
|
+
## Limitations
|
|
66
|
+
|
|
67
|
+
1. MSW interceptor cannot mock `GET` calls with body. So trying to mock elasticsearch / opensearch `GET /<index>/_search` calls with body breaks. I ended up using `POST` only when unit testing, as `GET` is the right way to use with AWS `AmazonOpenSearchServiceReadOnlyAccess` IAM permission 🤷♂️.
|
|
68
|
+
|
|
69
|
+
2. Stripe SDK uses node http module in a way MSW interceptor cannot mock - [reference](https://github.com/mswjs/msw/issues/2259#issuecomment-2379379566). Solution mentioned in a comment there is to use Stripe SDK with `fetch` client - [reference](https://github.com/mswjs/msw/issues/2259#issuecomment-2422672039). Another workaround is to mock Stripe SDK's methods with your testing library of choice.
|
|
70
|
+
|
|
65
71
|
## About snapshot files and its names
|
|
66
72
|
|
|
67
73
|
A snapshot file name uniquely identifies a request. By default it is a combination of HTTP method + URL + body that makes a request unique (headers are ignored).
|
|
@@ -200,3 +206,24 @@ WARNING: This module isn't concurrent or thread safe. Make sure that:
|
|
|
200
206
|
1. within one worker only one test is being executed at a time. e.g. If you use `ava`, and you have multiple `test()` blocks in one file, you need to change it to run serially with `test.serial()`.
|
|
201
207
|
|
|
202
208
|
2. parallel tests don't update the same snapshot file at the same time (i.e. while you run with SNAPSHOT=update). Regardless, updating snapshots of multiple tests at the same time is not a great idea in my opinion, because reviewing the snapshots files are a pain, escpecially if you have a shared snapshot files.
|
|
209
|
+
|
|
210
|
+
## Ignoring requests from snapshots
|
|
211
|
+
|
|
212
|
+
Sometimes you may want certain requests to not be snapshotted (e.g., file uploads to S3 / object storage) so that you can layer function / result snapshotting on top. Use `attachSnapshotIgnoreRules()` to provide a function that determines which requests should be ignored:
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
import { attachSnapshotIgnoreRules, resetSnapshotIgnoreRules } from "http-snapshotter";
|
|
216
|
+
|
|
217
|
+
// Ignore analytics and health check requests
|
|
218
|
+
attachSnapshotIgnoreRules((request) => {
|
|
219
|
+
const url = new URL(request.url);
|
|
220
|
+
return url.hostname.includes('/upload');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Reset to default (no requests ignored)
|
|
224
|
+
resetSnapshotIgnoreRules();
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Behavior varies by mode:**
|
|
228
|
+
- `SNAPSHOT=update/append`: Ignored requests make real network calls but don't create snapshots
|
|
229
|
+
- `SNAPSHOT=read`: Ignored requests throw an error (tests shouldn't make real network calls)
|
package/index.js
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* You can use SNAPSHOT=ignore to neither read not write snapshots, for testing on real
|
|
17
17
|
* network operations.
|
|
18
18
|
*
|
|
19
|
+
* To ignore specific requests from being snapshotted in SNAPSHOT=update or SNAPSHOT=append mode,
|
|
20
|
+
* use attachSnapshotIgnoreRules() to provide a function that receives the request object and
|
|
21
|
+
* returns true if the request should be ignored from snapshotting.
|
|
22
|
+
*
|
|
19
23
|
* Log read/saved snapshots by setting LOG_SNAPSHOT=1 env variable.
|
|
20
24
|
*
|
|
21
25
|
* Unused snapshot files will be written into a log file named 'unused-snapshots.log'.
|
|
@@ -135,6 +139,15 @@ async function defaultSnapshotFileNameGenerator(request) {
|
|
|
135
139
|
};
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Default snapshot ignore rules - by default no requests are ignored
|
|
144
|
+
* @param {Request} request
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
function defaultSnapshotIgnoreRules(request) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
138
151
|
// Dynamically changeable props
|
|
139
152
|
/**
|
|
140
153
|
* @type {(req: Request) => Promise<{ filePrefix: string, fileSuffixKey: string }>}
|
|
@@ -142,6 +155,11 @@ async function defaultSnapshotFileNameGenerator(request) {
|
|
|
142
155
|
let snapshotFileNameGenerator = defaultSnapshotFileNameGenerator;
|
|
143
156
|
let snapshotSubDirectory = '';
|
|
144
157
|
|
|
158
|
+
/**
|
|
159
|
+
* @type {(req: Request) => boolean}
|
|
160
|
+
*/
|
|
161
|
+
let snapshotIgnoreRules = defaultSnapshotIgnoreRules;
|
|
162
|
+
|
|
145
163
|
/**
|
|
146
164
|
* @typedef SnapshotFileInfo
|
|
147
165
|
* @property {string} absoluteFilePath
|
|
@@ -595,6 +613,36 @@ function resetSnapshotFilenameGenerator() {
|
|
|
595
613
|
snapshotFileNameGenerator = defaultSnapshotFileNameGenerator;
|
|
596
614
|
}
|
|
597
615
|
|
|
616
|
+
/**
|
|
617
|
+
* Attach snapshot ignore rules function
|
|
618
|
+
*
|
|
619
|
+
* Here's your opportunity to define custom rules for ignoring requests from being snapshotted.
|
|
620
|
+
* The function receives the Request object and should return true if the request should be ignored.
|
|
621
|
+
*
|
|
622
|
+
* IMPORTANT: Behavior varies by SNAPSHOT mode:
|
|
623
|
+
* - SNAPSHOT=update/append: Ignored requests make real network calls but don't create snapshots
|
|
624
|
+
* - SNAPSHOT=read: Ignored requests throw an error (tests shouldn't make real network calls)
|
|
625
|
+
*
|
|
626
|
+
* Use cases (not limited to):
|
|
627
|
+
* 1. Ignore requests with specific headers (e.g., x-debug-mode: no-snapshot)
|
|
628
|
+
* 2. Ignore requests to specific URLs or domains
|
|
629
|
+
* 3. Ignore requests with specific HTTP methods
|
|
630
|
+
* 4. Ignore requests based on request body content
|
|
631
|
+
*
|
|
632
|
+
* WARNING: Attaching a function on a per-test basis may not be concurrent safe. i.e. If your tests
|
|
633
|
+
* run sequentially, then it is safe. But if your test runner runs test suites concurrently,
|
|
634
|
+
* then it is better to attach a function only once ever.
|
|
635
|
+
* @param {(req: Request) => boolean} func
|
|
636
|
+
*/
|
|
637
|
+
function attachSnapshotIgnoreRules(func) {
|
|
638
|
+
snapshotIgnoreRules = func;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Reset snapshot ignore rules to default (no requests ignored) */
|
|
642
|
+
function resetSnapshotIgnoreRules() {
|
|
643
|
+
snapshotIgnoreRules = defaultSnapshotIgnoreRules;
|
|
644
|
+
}
|
|
645
|
+
|
|
598
646
|
/**
|
|
599
647
|
* Start the interceptor
|
|
600
648
|
* @param {object} opts
|
|
@@ -621,10 +669,35 @@ function start({
|
|
|
621
669
|
});
|
|
622
670
|
|
|
623
671
|
const cache = /** @type {WeakMap<Request, SnapshotFileInfo>} */ (new WeakMap());
|
|
672
|
+
const ignoredRequests = /** @type {WeakSet<Request>} */ (new WeakSet());
|
|
624
673
|
|
|
625
674
|
//@ts-ignore
|
|
626
675
|
interceptor.on('request', async ({ request, controller }) => {
|
|
627
|
-
if
|
|
676
|
+
// Check if request should be ignored from snapshotting using ignore rules
|
|
677
|
+
const shouldIgnoreSnapshot = snapshotIgnoreRules(request);
|
|
678
|
+
|
|
679
|
+
// Track ignored requests
|
|
680
|
+
if (shouldIgnoreSnapshot) {
|
|
681
|
+
ignoredRequests.add(request);
|
|
682
|
+
|
|
683
|
+
// In read mode, we should not allow ignored requests to make real network calls
|
|
684
|
+
if (SNAPSHOT === 'read') {
|
|
685
|
+
console.error(
|
|
686
|
+
`${colors.red}Request ignored by snapshot ignore rules but SNAPSHOT=read mode doesn't allow real network requests:${colors.reset}`,
|
|
687
|
+
{
|
|
688
|
+
request: {
|
|
689
|
+
url: request.url,
|
|
690
|
+
method: request.method,
|
|
691
|
+
headers: Object.fromEntries([...request.headers.entries()]),
|
|
692
|
+
body: await request.clone().text(),
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
);
|
|
696
|
+
throw new Error('Request ignored by snapshot ignore rules but SNAPSHOT=read mode doesn\'t allow real network requests');
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (['read', 'append'].includes(SNAPSHOT) && !shouldIgnoreSnapshot) {
|
|
628
701
|
const snapshotFileInfo = await getSnapshotFileInfo(request);
|
|
629
702
|
cache.set(request, snapshotFileInfo);
|
|
630
703
|
await readSnapshotAndSendResponse(request, controller, snapshotFileInfo);
|
|
@@ -635,15 +708,23 @@ function start({
|
|
|
635
708
|
'response',
|
|
636
709
|
/** @type {(params: { request: Request, response: Response }) => Promise<void>} */
|
|
637
710
|
async ({ request, response }) => {
|
|
711
|
+
// Check if this request was marked to ignore snapshots
|
|
712
|
+
const shouldIgnoreSnapshot = ignoredRequests.has(request);
|
|
713
|
+
|
|
638
714
|
const snapshotFileInfo = cache.get(request) || (await getSnapshotFileInfo(request));
|
|
639
715
|
cache.delete(request);
|
|
716
|
+
|
|
640
717
|
const {
|
|
641
718
|
// absoluteFilePath,
|
|
642
719
|
fileName,
|
|
643
720
|
fileSuffixKey,
|
|
644
721
|
} = snapshotFileInfo;
|
|
645
722
|
if (LOG_REQ) {
|
|
646
|
-
const summary = `----------\n${request.method} ${request.url}\
|
|
723
|
+
const summary = `----------\n${request.method} ${request.url}\n${
|
|
724
|
+
shouldIgnoreSnapshot
|
|
725
|
+
? 'ignored from snapshotting by ignore rules'
|
|
726
|
+
: `Would use file name: ${fileName}`
|
|
727
|
+
}`;
|
|
647
728
|
if (LOG_REQ === '1' || LOG_REQ === 'summary') {
|
|
648
729
|
console.debug(summary);
|
|
649
730
|
} else if (LOG_REQ === 'detailed') {
|
|
@@ -664,7 +745,7 @@ function start({
|
|
|
664
745
|
});
|
|
665
746
|
}
|
|
666
747
|
}
|
|
667
|
-
if (SNAPSHOT === 'update' || (SNAPSHOT === 'append' && !readFiles.has(fileName))) {
|
|
748
|
+
if (!shouldIgnoreSnapshot && (SNAPSHOT === 'update' || (SNAPSHOT === 'append' && !readFiles.has(fileName)))) {
|
|
668
749
|
if (!dirCreatePromise) {
|
|
669
750
|
dirCreatePromise = fs.mkdir( /** @type {string} */(snapshotDirectory), { recursive: true });
|
|
670
751
|
}
|
|
@@ -691,6 +772,9 @@ module.exports = {
|
|
|
691
772
|
defaultSnapshotFileNameGenerator,
|
|
692
773
|
attachSnapshotFilenameGenerator,
|
|
693
774
|
resetSnapshotFilenameGenerator,
|
|
775
|
+
defaultSnapshotIgnoreRules,
|
|
776
|
+
attachSnapshotIgnoreRules,
|
|
777
|
+
resetSnapshotIgnoreRules,
|
|
694
778
|
start,
|
|
695
779
|
stop,
|
|
696
780
|
};
|
package/intercept.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This function was created with a realization that once you override a method for mocking
|
|
3
|
+
* and run a test, you can't undo the override, because shared code (test runner without
|
|
4
|
+
* test isolation) will hold on to closures to the overridden function.
|
|
5
|
+
*
|
|
6
|
+
* So a solution it to intercept once before all tests, mock for a test and "undoMock()" at
|
|
7
|
+
* end of a test will cause the intercept to "proxy" future calls to the original method.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const mocked = new Set();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extracts the keys of an object where the values of the keys are functions.
|
|
14
|
+
* @template T The object type.
|
|
15
|
+
* @typedef {Extract<
|
|
16
|
+
* keyof T,
|
|
17
|
+
* { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]
|
|
18
|
+
* >} FunctionKeys
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @template T
|
|
23
|
+
* @template {FunctionKeys<T>} K
|
|
24
|
+
* @param {T} object
|
|
25
|
+
* @param {K} methodName
|
|
26
|
+
*/
|
|
27
|
+
function intercept(object, methodName) {
|
|
28
|
+
let mockHandler = null;
|
|
29
|
+
const originalMethod = object[methodName];
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
const boundedMethod = object[methodName].bind(object);
|
|
32
|
+
|
|
33
|
+
// @ts-ignore
|
|
34
|
+
// eslint-disable-next-line no-param-reassign
|
|
35
|
+
object[methodName] = function interceptedFunction(...args) {
|
|
36
|
+
return mockHandler
|
|
37
|
+
? mockHandler(boundedMethod, ...args)
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
: originalMethod.apply(object, args);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
/**
|
|
44
|
+
* @param {(originalMethod: T[K], ...args: Parameters<T[K]>) => any} mockFunction
|
|
45
|
+
*/
|
|
46
|
+
mock(mockFunction) {
|
|
47
|
+
mockHandler = mockFunction;
|
|
48
|
+
mocked.add(this);
|
|
49
|
+
},
|
|
50
|
+
undoMock() {
|
|
51
|
+
mockHandler = null;
|
|
52
|
+
mocked.delete(this);
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Un-intercepting will not get rid of closures to interceptedFunction
|
|
56
|
+
*/
|
|
57
|
+
destroy() {
|
|
58
|
+
mockHandler = null;
|
|
59
|
+
// eslint-disable-next-line no-param-reassign
|
|
60
|
+
object[methodName] = originalMethod;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function undoAllMocks() {
|
|
66
|
+
[...mocked].forEach((methods) => methods.undoMock());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const interceptAllMethods = (object) => Object.fromEntries(
|
|
70
|
+
Object
|
|
71
|
+
.keys(object)
|
|
72
|
+
.filter((key) => typeof object[key] === 'function')
|
|
73
|
+
.map(
|
|
74
|
+
(key) => [key, intercept(object, key)],
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
module.exports = { intercept, undoAllMocks, interceptAllMethods };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "http-snapshotter",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Snapshot HTTP requests for tests (node.js)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"require": "./index.js",
|
|
10
10
|
"types": "./index.d.ts"
|
|
11
11
|
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
12
15
|
"scripts": {
|
|
13
16
|
"test": "tape tests/**/*.test.* | tap-arc",
|
|
14
17
|
"tsc": "tsc"
|
|
File without changes
|