@schmock/core 2.3.0 → 2.4.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 (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/abort.d.ts +3 -0
  4. package/dist/abort.d.ts.map +1 -0
  5. package/dist/abort.js +30 -0
  6. package/dist/builder.d.ts +31 -10
  7. package/dist/builder.d.ts.map +1 -1
  8. package/dist/builder.js +760 -190
  9. package/dist/constants.d.ts +38 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/constants.js +123 -2
  12. package/dist/errors.d.ts +14 -3
  13. package/dist/errors.d.ts.map +1 -1
  14. package/dist/errors.js +41 -6
  15. package/dist/helpers.d.ts +9 -0
  16. package/dist/helpers.d.ts.map +1 -1
  17. package/dist/helpers.js +18 -2
  18. package/dist/http-helpers.d.ts +39 -4
  19. package/dist/http-helpers.d.ts.map +1 -1
  20. package/dist/http-helpers.js +147 -49
  21. package/dist/index.d.ts +292 -34
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +8 -3
  24. package/dist/interceptor.d.ts +11 -1
  25. package/dist/interceptor.d.ts.map +1 -1
  26. package/dist/interceptor.js +316 -169
  27. package/dist/parser.d.ts.map +1 -1
  28. package/dist/parser.js +12 -3
  29. package/dist/plugin-pipeline.d.ts +3 -3
  30. package/dist/plugin-pipeline.d.ts.map +1 -1
  31. package/dist/plugin-pipeline.js +29 -14
  32. package/dist/response-normalizer.d.ts +16 -0
  33. package/dist/response-normalizer.d.ts.map +1 -0
  34. package/dist/response-normalizer.js +316 -0
  35. package/dist/response-parser.d.ts.map +1 -1
  36. package/dist/response-parser.js +39 -2
  37. package/dist/route-matcher.d.ts +3 -0
  38. package/dist/route-matcher.d.ts.map +1 -1
  39. package/dist/route-matcher.js +12 -6
  40. package/dist/types.d.ts +7 -2
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +18 -6
  43. package/src/audit-core-builder.test.ts +0 -218
  44. package/src/audit-response-guard.test.ts +0 -38
  45. package/src/binary.test.ts +0 -126
  46. package/src/binary.ts +0 -11
  47. package/src/builder.test.ts +0 -289
  48. package/src/builder.ts +0 -748
  49. package/src/constants.test.ts +0 -99
  50. package/src/constants.ts +0 -73
  51. package/src/debug.test.ts +0 -241
  52. package/src/delay.test.ts +0 -319
  53. package/src/dist-shape.test.ts +0 -35
  54. package/src/errors.test.ts +0 -223
  55. package/src/errors.ts +0 -130
  56. package/src/factory.test.ts +0 -133
  57. package/src/helpers.test.ts +0 -147
  58. package/src/helpers.ts +0 -58
  59. package/src/http-helpers.test.ts +0 -39
  60. package/src/http-helpers.ts +0 -149
  61. package/src/index.ts +0 -152
  62. package/src/interceptor.test.ts +0 -488
  63. package/src/interceptor.ts +0 -407
  64. package/src/namespace.test.ts +0 -274
  65. package/src/parser.property.test.ts +0 -630
  66. package/src/parser.test.ts +0 -148
  67. package/src/parser.ts +0 -64
  68. package/src/plugin-pipeline.ts +0 -208
  69. package/src/plugin-system.test.ts +0 -697
  70. package/src/response-parser.ts +0 -114
  71. package/src/response-parsing.test.ts +0 -333
  72. package/src/route-matcher.ts +0 -69
  73. package/src/route-matching.test.ts +0 -394
  74. package/src/server.test.ts +0 -234
  75. package/src/smart-defaults.test.ts +0 -361
  76. package/src/steps/async-support.steps.ts +0 -421
  77. package/src/steps/audit-core-builder.steps.ts +0 -188
  78. package/src/steps/audit-onerror-tuple.steps.ts +0 -83
  79. package/src/steps/basic-usage.steps.ts +0 -245
  80. package/src/steps/developer-experience.steps.ts +0 -204
  81. package/src/steps/error-handling.steps.ts +0 -555
  82. package/src/steps/fetch-interceptor.steps.ts +0 -457
  83. package/src/steps/fluent-api.steps.ts +0 -255
  84. package/src/steps/http-methods.steps.ts +0 -342
  85. package/src/steps/lifecycle-events.steps.ts +0 -149
  86. package/src/steps/performance-reliability.steps.ts +0 -90
  87. package/src/steps/plugin-integration.steps.ts +0 -334
  88. package/src/steps/request-history.steps.ts +0 -458
  89. package/src/steps/response-delay.steps.ts +0 -88
  90. package/src/steps/route-key-format.steps.ts +0 -99
  91. package/src/steps/standalone-server.steps.ts +0 -227
  92. package/src/steps/state-concurrency.steps.ts +0 -167
  93. package/src/types.ts +0 -36
@@ -1,488 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { schmock } from "./index.js";
3
-
4
- describe("mock.intercept()", () => {
5
- let originalFetch: typeof globalThis.fetch;
6
- let mock: Schmock.CallableMockInstance;
7
-
8
- beforeEach(() => {
9
- originalFetch = globalThis.fetch;
10
- globalThis.fetch = vi.fn().mockResolvedValue(new Response("real backend"));
11
- mock = schmock();
12
- });
13
-
14
- afterEach(() => {
15
- globalThis.fetch = originalFetch;
16
- });
17
-
18
- it("intercepts a matched fetch request and returns mocked response", async () => {
19
- mock("GET /api/users", [{ id: 1, name: "Alice" }]);
20
- const handle = mock.intercept();
21
-
22
- const res = await fetch("http://localhost/api/users");
23
- expect(res.status).toBe(200);
24
- expect(await res.json()).toEqual([{ id: 1, name: "Alice" }]);
25
-
26
- handle.restore();
27
- });
28
-
29
- it("returns browser binary bodies without JSON serialization", async () => {
30
- const bytes = new Uint8Array([1, 2, 3]);
31
- mock("GET /api/binary", bytes);
32
- const handle = mock.intercept();
33
-
34
- try {
35
- const response = await fetch("http://localhost/api/binary");
36
- expect(response.headers.get("content-type")).toBe(
37
- "application/octet-stream",
38
- );
39
- expect(new Uint8Array(await response.arrayBuffer())).toEqual(bytes);
40
- } finally {
41
- handle.restore();
42
- }
43
- });
44
-
45
- it("returns dynamic binary bodies with binary content type", async () => {
46
- const bytes = new Uint8Array([4, 5, 6]);
47
- mock("GET /api/dynamic-binary", () => bytes.buffer);
48
- const handle = mock.intercept();
49
-
50
- try {
51
- const response = await fetch("http://localhost/api/dynamic-binary");
52
- expect(response.headers.get("content-type")).toBe(
53
- "application/octet-stream",
54
- );
55
- expect(new Uint8Array(await response.arrayBuffer())).toEqual(bytes);
56
- } finally {
57
- handle.restore();
58
- }
59
- });
60
-
61
- it("returns tuple binary bodies with binary content type", async () => {
62
- const bytes = new Uint8Array([7, 8, 9]);
63
- mock(
64
- "GET /api/tuple-binary",
65
- () => [206, new DataView(bytes.buffer)] satisfies [number, unknown],
66
- );
67
- const handle = mock.intercept();
68
-
69
- try {
70
- const response = await fetch("http://localhost/api/tuple-binary");
71
- expect(response.status).toBe(206);
72
- expect(response.headers.get("content-type")).toBe(
73
- "application/octet-stream",
74
- );
75
- expect(new Uint8Array(await response.arrayBuffer())).toEqual(bytes);
76
- } finally {
77
- handle.restore();
78
- }
79
- });
80
-
81
- it("normalizes a binary body supplied by beforeResponse", async () => {
82
- mock("GET /api/source", "source");
83
- const sharedBytes = new Uint8Array(new SharedArrayBuffer(3));
84
- sharedBytes.set([10, 11, 12]);
85
- const handle = mock.intercept({
86
- beforeResponse: () => ({
87
- status: 200,
88
- body: new DataView(sharedBytes.buffer),
89
- headers: {},
90
- }),
91
- });
92
-
93
- try {
94
- const response = await fetch("http://localhost/api/source");
95
- expect(response.headers.get("content-type")).toBe(
96
- "application/octet-stream",
97
- );
98
- expect(new Uint8Array(await response.arrayBuffer())).toEqual(
99
- new Uint8Array([10, 11, 12]),
100
- );
101
- } finally {
102
- handle.restore();
103
- }
104
- });
105
-
106
- it("passes through unmatched routes when passthrough is true", async () => {
107
- mock("GET /api/users", [{ id: 1 }]);
108
- // Save reference to the vi.fn() mock that the interceptor will call on passthrough
109
- const mockFetch = globalThis.fetch as ReturnType<typeof vi.fn>;
110
- const handle = mock.intercept({ passthrough: true });
111
-
112
- await fetch("http://localhost/api/other");
113
- // The interceptor saves the vi.fn() as its original and calls it on passthrough
114
- expect(mockFetch).toHaveBeenCalled();
115
- handle.restore();
116
- });
117
-
118
- it("returns 404 when passthrough is disabled and route not found", async () => {
119
- mock("GET /api/users", [{ id: 1 }]);
120
- const handle = mock.intercept({ passthrough: false });
121
-
122
- const res = await fetch("http://localhost/api/other");
123
- expect(res.status).toBe(404);
124
-
125
- handle.restore();
126
- });
127
-
128
- it("restores original fetch", () => {
129
- const savedFetch = globalThis.fetch;
130
- const handle = mock.intercept();
131
-
132
- expect(globalThis.fetch).not.toBe(savedFetch);
133
- handle.restore();
134
- expect(globalThis.fetch).toBe(savedFetch);
135
- });
136
-
137
- it("reports active status", () => {
138
- const handle = mock.intercept();
139
- expect(handle.active).toBe(true);
140
-
141
- handle.restore();
142
- expect(handle.active).toBe(false);
143
- });
144
-
145
- it("filters by baseUrl", async () => {
146
- mock("GET /api/users", [{ id: 1 }]);
147
- const savedFetch = globalThis.fetch;
148
- const handle = mock.intercept({ baseUrl: "/api" });
149
-
150
- await fetch("http://localhost/other/path");
151
- // Should have called the saved fetch (passthrough for non-matching baseUrl)
152
- expect(savedFetch).toHaveBeenCalled();
153
-
154
- handle.restore();
155
- });
156
-
157
- it("throws when intercepting twice", () => {
158
- const handle = mock.intercept();
159
- expect(() => mock.intercept()).toThrow(/already intercepting/i);
160
- handle.restore();
161
- });
162
-
163
- it("applies beforeRequest hook", async () => {
164
- mock("GET /api/users", ({ headers }) => [
165
- 200,
166
- { token: headers["x-token"] },
167
- ]);
168
- const handle = mock.intercept({
169
- beforeRequest: (req) => ({
170
- ...req,
171
- headers: { ...req.headers, "x-token": "injected" },
172
- }),
173
- });
174
-
175
- const res = await fetch("http://localhost/api/users");
176
- expect(await res.json()).toEqual({ token: "injected" });
177
-
178
- handle.restore();
179
- });
180
-
181
- it("applies beforeResponse hook", async () => {
182
- mock("GET /api/users", [{ id: 1 }]);
183
- const handle = mock.intercept({
184
- beforeResponse: (resp) => ({
185
- ...resp,
186
- headers: { ...resp.headers, "x-mock": "true" },
187
- }),
188
- });
189
-
190
- const res = await fetch("http://localhost/api/users");
191
- expect(res.headers.get("x-mock")).toBe("true");
192
-
193
- handle.restore();
194
- });
195
-
196
- it("handles relative URLs", async () => {
197
- mock("GET /api/users", [{ id: 1 }]);
198
- const handle = mock.intercept();
199
-
200
- const res = await fetch("/api/users");
201
- expect(res.status).toBe(200);
202
- expect(await res.json()).toEqual([{ id: 1 }]);
203
-
204
- handle.restore();
205
- });
206
-
207
- it("applies errorFormatter when beforeRequest throws", async () => {
208
- mock("GET /api/users", [{ id: 1 }]);
209
- const handle = mock.intercept({
210
- beforeRequest: () => {
211
- throw new Error("hook failed");
212
- },
213
- errorFormatter: (err) => ({ custom: err.message }),
214
- });
215
-
216
- const res = await fetch("http://localhost/api/users");
217
- expect(res.status).toBe(500);
218
- expect(await res.json()).toEqual({ custom: "hook failed" });
219
-
220
- handle.restore();
221
- });
222
-
223
- it("normalizes header keys to lowercase", async () => {
224
- mock("POST /api/data", ({ headers }) => [
225
- 200,
226
- { auth: headers.authorization, ct: headers["content-type"] },
227
- ]);
228
- const handle = mock.intercept();
229
-
230
- const res = await fetch("http://localhost/api/data", {
231
- method: "POST",
232
- headers: {
233
- Authorization: "Bearer tok",
234
- "Content-Type": "application/json",
235
- },
236
- body: JSON.stringify({}),
237
- });
238
- expect(await res.json()).toEqual({
239
- auth: "Bearer tok",
240
- ct: "application/json",
241
- });
242
-
243
- handle.restore();
244
- });
245
-
246
- it("parses JSON body from fetch init", async () => {
247
- mock("POST /api/users", ({ body }) => [201, body]);
248
- const handle = mock.intercept();
249
-
250
- const res = await fetch("http://localhost/api/users", {
251
- method: "POST",
252
- headers: { "content-type": "application/json" },
253
- body: JSON.stringify({ name: "Alice" }),
254
- });
255
- expect(res.status).toBe(201);
256
- expect(await res.json()).toEqual({ name: "Alice" });
257
-
258
- handle.restore();
259
- });
260
-
261
- it("intercepts fetch called with a Request object", async () => {
262
- mock("GET /api/users", [{ id: 1, name: "Alice" }]);
263
- const handle = mock.intercept();
264
-
265
- const req = new Request("http://localhost/api/users");
266
- const res = await fetch(req);
267
- expect(res.status).toBe(200);
268
- expect(await res.json()).toEqual([{ id: 1, name: "Alice" }]);
269
-
270
- handle.restore();
271
- });
272
-
273
- it("prefers init.headers over Request.headers", async () => {
274
- mock("GET /api/data", ({ headers }) => [200, { val: headers["x-custom"] }]);
275
- const handle = mock.intercept();
276
-
277
- const req = new Request("http://localhost/api/data", {
278
- headers: { "X-Custom": "from-request" },
279
- });
280
- const res = await fetch(req, {
281
- headers: { "X-Custom": "from-init" },
282
- });
283
- expect(await res.json()).toEqual({ val: "from-init" });
284
-
285
- handle.restore();
286
- });
287
-
288
- it("parses URLSearchParams body", async () => {
289
- mock("POST /api/form", ({ body }) => [200, body]);
290
- const handle = mock.intercept();
291
-
292
- const params = new URLSearchParams();
293
- params.set("name", "Alice");
294
- params.set("role", "admin");
295
-
296
- const res = await fetch("http://localhost/api/form", {
297
- method: "POST",
298
- body: params,
299
- });
300
- expect(await res.json()).toEqual({ name: "Alice", role: "admin" });
301
-
302
- handle.restore();
303
- });
304
-
305
- it("baseUrl /api should NOT match /apiv2 (segment boundary)", async () => {
306
- mock("GET /apiv2/data", [{ v2: true }]);
307
- const savedFetch = globalThis.fetch;
308
- const handle = mock.intercept({ baseUrl: "/api" });
309
-
310
- // /apiv2/data does not start with "/api/" — it should passthrough
311
- await fetch("http://localhost/apiv2/data");
312
- expect(savedFetch).toHaveBeenCalled();
313
-
314
- handle.restore();
315
- });
316
-
317
- it("baseUrl with trailing slash matches the same routes as without", async () => {
318
- mock("GET /api/items", [{ id: 1 }]);
319
-
320
- // Without trailing slash
321
- const handle1 = mock.intercept({ baseUrl: "/api" });
322
- const res1 = await fetch("http://localhost/api/items");
323
- expect(res1.status).toBe(200);
324
- expect(await res1.json()).toEqual([{ id: 1 }]);
325
- handle1.restore();
326
-
327
- // With trailing slash — should still match /api/items
328
- const handle2 = mock.intercept({ baseUrl: "/api/" });
329
- const res2 = await fetch("http://localhost/api/items");
330
- expect(res2.status).toBe(200);
331
- expect(await res2.json()).toEqual([{ id: 1 }]);
332
- handle2.restore();
333
- });
334
-
335
- it("extractBody: init.body wins over Request.body per Fetch spec", async () => {
336
- mock("POST /api/data", ({ body }) => [200, body]);
337
- const handle = mock.intercept();
338
-
339
- const req = new Request("http://localhost/api/data", {
340
- method: "POST",
341
- headers: { "content-type": "application/json" },
342
- body: JSON.stringify({ source: "request" }),
343
- });
344
-
345
- const res = await fetch(req, {
346
- body: JSON.stringify({ source: "init" }),
347
- });
348
- expect(await res.json()).toEqual({ source: "init" });
349
-
350
- handle.restore();
351
- });
352
-
353
- it("extractBody: failed JSON parse falls back to text", async () => {
354
- mock("POST /api/text", ({ body }) => [200, { received: body }]);
355
- const handle = mock.intercept();
356
-
357
- const res = await fetch("http://localhost/api/text", {
358
- method: "POST",
359
- headers: { "content-type": "text/plain" },
360
- body: "not-json-{broken",
361
- });
362
-
363
- const json = await res.json();
364
- expect(json.received).toBe("not-json-{broken");
365
-
366
- handle.restore();
367
- });
368
-
369
- it("composes mock instances newest-first and chains passthrough", async () => {
370
- const olderMock = schmock();
371
- olderMock("GET /api/shared", { source: "older" });
372
- olderMock("GET /api/older", { source: "older" });
373
-
374
- const newerMock = schmock();
375
- newerMock("GET /api/shared", { source: "newer" });
376
-
377
- const olderHandle = olderMock.intercept();
378
- const newerHandle = newerMock.intercept();
379
-
380
- try {
381
- const sharedResponse = await fetch("http://localhost/api/shared");
382
- expect(await sharedResponse.json()).toEqual({ source: "newer" });
383
-
384
- const olderResponse = await fetch("http://localhost/api/older");
385
- expect(await olderResponse.json()).toEqual({ source: "older" });
386
- } finally {
387
- newerHandle.restore();
388
- olderHandle.restore();
389
- }
390
- });
391
-
392
- it("keeps remaining interceptors installed after out-of-order restore", async () => {
393
- const baseline = globalThis.fetch;
394
- const olderMock = schmock();
395
- const newerMock = schmock();
396
- newerMock("GET /api/newer", { source: "newer" });
397
-
398
- const olderHandle = olderMock.intercept();
399
- const newerHandle = newerMock.intercept();
400
-
401
- olderHandle.restore();
402
- expect(olderHandle.active).toBe(false);
403
- expect(newerHandle.active).toBe(true);
404
- expect(globalThis.fetch).not.toBe(baseline);
405
-
406
- try {
407
- const response = await fetch("http://localhost/api/newer");
408
- expect(await response.json()).toEqual({ source: "newer" });
409
- } finally {
410
- newerHandle.restore();
411
- }
412
-
413
- expect(globalThis.fetch).toBe(baseline);
414
- });
415
-
416
- it("restores idempotently", () => {
417
- const baseline = globalThis.fetch;
418
- const handle = mock.intercept();
419
-
420
- handle.restore();
421
- handle.restore();
422
-
423
- expect(handle.active).toBe(false);
424
- expect(globalThis.fetch).toBe(baseline);
425
- });
426
-
427
- it("does not overwrite a fetch replacement installed after Schmock", () => {
428
- const handle = mock.intercept();
429
- const replacementFetch = vi
430
- .fn()
431
- .mockResolvedValue(new Response("third-party backend"));
432
- globalThis.fetch = replacementFetch;
433
-
434
- handle.restore();
435
-
436
- expect(globalThis.fetch).toBe(replacementFetch);
437
- });
438
-
439
- it("keeps a third-party wrapper's captured dispatcher usable", async () => {
440
- const baseline = globalThis.fetch;
441
- const handle = mock.intercept();
442
- const schmockDispatcher = globalThis.fetch;
443
- const replacementFetch = vi.fn(
444
- (input: RequestInfo | URL, init?: RequestInit) =>
445
- schmockDispatcher(input, init),
446
- );
447
- globalThis.fetch = replacementFetch;
448
-
449
- handle.restore();
450
-
451
- const response = await fetch("http://localhost/real-backend");
452
- expect(await response.text()).toBe("real backend");
453
- expect(replacementFetch).toHaveBeenCalledOnce();
454
- expect(baseline).toHaveBeenCalledOnce();
455
- });
456
-
457
- it("wraps the current fetch when a later interceptor follows a replacement", async () => {
458
- const olderMock = schmock();
459
- olderMock("GET /api/older", { source: "older" });
460
- const olderHandle = olderMock.intercept();
461
-
462
- const replacementFetch = vi
463
- .fn()
464
- .mockResolvedValue(new Response("third-party backend"));
465
- globalThis.fetch = replacementFetch;
466
-
467
- const newerMock = schmock();
468
- newerMock("GET /api/newer", { source: "newer" });
469
- const newerHandle = newerMock.intercept();
470
-
471
- olderHandle.restore();
472
- expect(olderHandle.active).toBe(false);
473
- expect(newerHandle.active).toBe(true);
474
-
475
- try {
476
- const mockedResponse = await fetch("http://localhost/api/newer");
477
- expect(await mockedResponse.json()).toEqual({ source: "newer" });
478
-
479
- const passthroughResponse = await fetch("http://localhost/real-backend");
480
- expect(await passthroughResponse.text()).toBe("third-party backend");
481
- expect(replacementFetch).toHaveBeenCalledOnce();
482
- } finally {
483
- newerHandle.restore();
484
- }
485
-
486
- expect(globalThis.fetch).toBe(replacementFetch);
487
- });
488
- });