http-snapshotter 0.5.2 → 0.5.3-beta.1

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/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 (['read', 'append'].includes(SNAPSHOT)) {
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}\nWould use file name: ${fileName}`;
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.2",
3
+ "version": "0.5.3-beta.1",
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