@schmock/core 2.2.3 → 2.3.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/dist/binary.d.ts +8 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +9 -0
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +60 -29
- package/dist/http-helpers.d.ts +15 -3
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +24 -6
- package/dist/index.d.ts +90 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +71 -20
- package/dist/plugin-pipeline.d.ts +11 -4
- package/dist/plugin-pipeline.d.ts.map +1 -1
- package/dist/plugin-pipeline.js +98 -35
- package/dist/response-parser.d.ts.map +1 -1
- package/dist/response-parser.js +47 -17
- package/package.json +5 -3
- package/src/binary.test.ts +126 -0
- package/src/binary.ts +11 -0
- package/src/builder.ts +82 -35
- package/src/http-helpers.test.ts +39 -0
- package/src/http-helpers.ts +46 -10
- package/src/index.ts +1 -0
- package/src/interceptor.test.ts +197 -0
- package/src/interceptor.ts +215 -128
- package/src/parser.property.test.ts +91 -55
- package/src/plugin-pipeline.ts +150 -45
- package/src/plugin-system.test.ts +95 -0
- package/src/response-parser.ts +58 -19
- package/src/server.test.ts +81 -12
- package/src/steps/async-support.steps.ts +29 -8
- package/src/steps/audit-onerror-tuple.steps.ts +14 -30
- package/src/steps/error-handling.steps.ts +463 -253
- package/src/steps/fetch-interceptor.steps.ts +457 -0
- package/src/steps/http-methods.steps.ts +19 -8
- package/src/steps/lifecycle-events.steps.ts +50 -43
- package/src/steps/performance-reliability.steps.ts +78 -411
- package/src/steps/plugin-integration.steps.ts +54 -0
- package/src/steps/request-history.steps.ts +412 -230
- package/src/steps/standalone-server.steps.ts +7 -13
- package/src/steps/state-concurrency.steps.ts +142 -714
- package/src/steps/interceptor.steps.ts +0 -260
- package/src/steps/stateful-workflows.steps.ts +0 -353
package/src/builder.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Server } from "node:http";
|
|
2
|
+
import { isBinaryBody } from "./binary.js";
|
|
2
3
|
import { normalizePath, toHttpMethod } from "./constants.js";
|
|
3
4
|
import {
|
|
4
5
|
errorMessage,
|
|
@@ -14,7 +15,11 @@ import {
|
|
|
14
15
|
} from "./http-helpers.js";
|
|
15
16
|
import { createFetchInterceptor } from "./interceptor.js";
|
|
16
17
|
import { parseRouteKey } from "./parser.js";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
recoverGeneratorError,
|
|
20
|
+
runPluginBeforeRequest,
|
|
21
|
+
runPluginPipeline,
|
|
22
|
+
} from "./plugin-pipeline.js";
|
|
18
23
|
import { parseResponse } from "./response-parser.js";
|
|
19
24
|
import type { CompiledCallableRoute } from "./route-matcher.js";
|
|
20
25
|
import {
|
|
@@ -104,8 +109,8 @@ export class CallableMockInstance {
|
|
|
104
109
|
) {
|
|
105
110
|
// Default to plain text for primitives
|
|
106
111
|
routeConfig.contentType = "text/plain";
|
|
107
|
-
} else if (
|
|
108
|
-
// Default to octet-stream for
|
|
112
|
+
} else if (isBinaryBody(generator)) {
|
|
113
|
+
// Default to octet-stream for browser and Node binary values
|
|
109
114
|
routeConfig.contentType = "application/octet-stream";
|
|
110
115
|
} else {
|
|
111
116
|
// Default to JSON for objects/arrays
|
|
@@ -394,8 +399,16 @@ export class CallableMockInstance {
|
|
|
394
399
|
this.server = httpServer;
|
|
395
400
|
|
|
396
401
|
return new Promise((resolve, reject) => {
|
|
397
|
-
|
|
402
|
+
const handleStartupError = (error: Error) => {
|
|
403
|
+
if (this.server === httpServer) {
|
|
404
|
+
this.server = undefined;
|
|
405
|
+
this.serverInfo = undefined;
|
|
406
|
+
}
|
|
407
|
+
reject(error);
|
|
408
|
+
};
|
|
409
|
+
httpServer.once("error", handleStartupError);
|
|
398
410
|
httpServer.listen(port, hostname, () => {
|
|
411
|
+
httpServer.off("error", handleStartupError);
|
|
399
412
|
const addr = httpServer.address();
|
|
400
413
|
const actualPort =
|
|
401
414
|
addr !== null && typeof addr === "object" ? addr.port : port;
|
|
@@ -485,7 +498,7 @@ export class CallableMockInstance {
|
|
|
485
498
|
const response = {
|
|
486
499
|
status: 404,
|
|
487
500
|
body: { error: error.message, code: error.code },
|
|
488
|
-
headers: {},
|
|
501
|
+
headers: { "content-type": "application/json" },
|
|
489
502
|
};
|
|
490
503
|
this.emit("request:end", {
|
|
491
504
|
method,
|
|
@@ -520,7 +533,7 @@ export class CallableMockInstance {
|
|
|
520
533
|
const response = {
|
|
521
534
|
status: 404,
|
|
522
535
|
body: { error: error.message, code: error.code },
|
|
523
|
-
headers: {},
|
|
536
|
+
headers: { "content-type": "application/json" },
|
|
524
537
|
};
|
|
525
538
|
this.emit("request:end", {
|
|
526
539
|
method,
|
|
@@ -547,25 +560,8 @@ export class CallableMockInstance {
|
|
|
547
560
|
params,
|
|
548
561
|
});
|
|
549
562
|
|
|
550
|
-
//
|
|
551
|
-
|
|
552
|
-
method,
|
|
553
|
-
path: requestPath,
|
|
554
|
-
params,
|
|
555
|
-
query: reqQuery,
|
|
556
|
-
headers: reqHeaders,
|
|
557
|
-
body: options?.body,
|
|
558
|
-
state: this.globalConfig.state || {},
|
|
559
|
-
};
|
|
560
|
-
|
|
561
|
-
let result: unknown;
|
|
562
|
-
if (isGeneratorFunction(matchedRoute.generator)) {
|
|
563
|
-
result = await matchedRoute.generator(context);
|
|
564
|
-
} else {
|
|
565
|
-
result = matchedRoute.generator;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Build plugin context
|
|
563
|
+
// Build plugin context before route code so request guards can reject
|
|
564
|
+
// invalid or unauthorized requests without triggering side effects.
|
|
569
565
|
let pluginContext: Schmock.PluginContext = {
|
|
570
566
|
path: requestPath,
|
|
571
567
|
route: matchedRoute.config,
|
|
@@ -578,16 +574,66 @@ export class CallableMockInstance {
|
|
|
578
574
|
routeState: this.globalConfig.state || {},
|
|
579
575
|
};
|
|
580
576
|
|
|
577
|
+
const preflightResult = await runPluginBeforeRequest(
|
|
578
|
+
this.plugins,
|
|
579
|
+
pluginContext,
|
|
580
|
+
this.logger,
|
|
581
|
+
);
|
|
582
|
+
pluginContext = preflightResult.context;
|
|
583
|
+
if (preflightResult.requestShortCircuited === true) {
|
|
584
|
+
pluginContext = { ...pluginContext, requestShortCircuited: true };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let result: unknown = preflightResult.response;
|
|
588
|
+
let skipPostProcessing = preflightResult.recoveredFromError === true;
|
|
589
|
+
|
|
590
|
+
if (result === undefined) {
|
|
591
|
+
const context: Schmock.RequestContext = {
|
|
592
|
+
method: pluginContext.method,
|
|
593
|
+
path: pluginContext.path,
|
|
594
|
+
params: pluginContext.params,
|
|
595
|
+
query: pluginContext.query,
|
|
596
|
+
headers: pluginContext.headers,
|
|
597
|
+
body: pluginContext.body,
|
|
598
|
+
state: pluginContext.routeState ?? this.globalConfig.state ?? {},
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
if (isGeneratorFunction(matchedRoute.generator)) {
|
|
603
|
+
result = await matchedRoute.generator(context);
|
|
604
|
+
} else {
|
|
605
|
+
result = matchedRoute.generator;
|
|
606
|
+
}
|
|
607
|
+
} catch (error) {
|
|
608
|
+
const recovery = await recoverGeneratorError(
|
|
609
|
+
this.plugins,
|
|
610
|
+
pluginContext,
|
|
611
|
+
error,
|
|
612
|
+
this.logger,
|
|
613
|
+
);
|
|
614
|
+
pluginContext = recovery.context;
|
|
615
|
+
result = recovery.response;
|
|
616
|
+
skipPostProcessing = recovery.recoveredFromError === true;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
581
620
|
// Run plugin pipeline to transform the response
|
|
582
621
|
try {
|
|
583
|
-
|
|
584
|
-
this.
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
622
|
+
if (skipPostProcessing) {
|
|
623
|
+
this.logger.log(
|
|
624
|
+
"pipeline",
|
|
625
|
+
"Skipping response processors after error recovery",
|
|
626
|
+
);
|
|
627
|
+
} else {
|
|
628
|
+
const pipelineResult = await runPluginPipeline(
|
|
629
|
+
this.plugins,
|
|
630
|
+
pluginContext,
|
|
631
|
+
result,
|
|
632
|
+
this.logger,
|
|
633
|
+
);
|
|
634
|
+
pluginContext = pipelineResult.context;
|
|
635
|
+
result = pipelineResult.response;
|
|
636
|
+
}
|
|
591
637
|
} catch (error) {
|
|
592
638
|
this.logger.log(
|
|
593
639
|
"error",
|
|
@@ -659,7 +705,7 @@ export class CallableMockInstance {
|
|
|
659
705
|
error: errorMessage(error),
|
|
660
706
|
code: error instanceof SchmockError ? error.code : "INTERNAL_ERROR",
|
|
661
707
|
},
|
|
662
|
-
headers: {},
|
|
708
|
+
headers: { "content-type": "application/json" },
|
|
663
709
|
};
|
|
664
710
|
|
|
665
711
|
// Apply delay even for error responses
|
|
@@ -691,10 +737,11 @@ export class CallableMockInstance {
|
|
|
691
737
|
return;
|
|
692
738
|
}
|
|
693
739
|
|
|
694
|
-
const
|
|
740
|
+
const configuredMs = Array.isArray(effectiveDelay)
|
|
695
741
|
? Math.random() * (effectiveDelay[1] - effectiveDelay[0]) +
|
|
696
742
|
effectiveDelay[0]
|
|
697
743
|
: effectiveDelay;
|
|
744
|
+
const ms = Math.max(0, configuredMs);
|
|
698
745
|
|
|
699
746
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
700
747
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { PassThrough } from "node:stream";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { collectBody } from "./http-helpers.js";
|
|
4
|
+
|
|
5
|
+
function bodyStream(body: Buffer): PassThrough {
|
|
6
|
+
const stream = new PassThrough();
|
|
7
|
+
stream.end(body);
|
|
8
|
+
return stream;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("collectBody size limits", () => {
|
|
12
|
+
it("accepts a body exactly at the configured limit", async () => {
|
|
13
|
+
const body = Buffer.from("12345");
|
|
14
|
+
await expect(collectBody(bodyStream(body), {}, body.length)).resolves.toBe(
|
|
15
|
+
"12345",
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("accepts and parses JSON below the configured limit", async () => {
|
|
20
|
+
const body = Buffer.from('{"ok":true}');
|
|
21
|
+
await expect(
|
|
22
|
+
collectBody(
|
|
23
|
+
bodyStream(body),
|
|
24
|
+
{ "content-type": "application/json" },
|
|
25
|
+
body.length + 1,
|
|
26
|
+
),
|
|
27
|
+
).resolves.toEqual({ ok: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("rejects a body above the configured limit with status 413", async () => {
|
|
31
|
+
const body = Buffer.from("123456");
|
|
32
|
+
await expect(
|
|
33
|
+
collectBody(bodyStream(body), {}, body.length - 1),
|
|
34
|
+
).rejects.toMatchObject({
|
|
35
|
+
message: "Request body too large",
|
|
36
|
+
status: 413,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
package/src/http-helpers.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
import { isBinaryBody } from "./binary.js";
|
|
3
|
+
|
|
4
|
+
interface RequestWithHeaders {
|
|
5
|
+
readonly headers: {
|
|
6
|
+
readonly [header: string]: string | string[] | undefined;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface BodyReadable {
|
|
11
|
+
on(event: "error", listener: (error: Error) => void): this;
|
|
12
|
+
on(event: "data", listener: (chunk: Buffer) => void): this;
|
|
13
|
+
on(event: "end", listener: () => void): this;
|
|
14
|
+
destroy(error?: Error): this;
|
|
15
|
+
}
|
|
2
16
|
|
|
3
17
|
/**
|
|
4
18
|
* Convert Node.js IncomingMessage headers to a flat Record<string, string>.
|
|
5
19
|
* Drops array-valued headers (keeps only string values).
|
|
6
20
|
*/
|
|
7
|
-
export function parseNodeHeaders(
|
|
21
|
+
export function parseNodeHeaders(
|
|
22
|
+
req: RequestWithHeaders,
|
|
23
|
+
): Record<string, string> {
|
|
8
24
|
const headers: Record<string, string> = {};
|
|
9
25
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
10
26
|
if (typeof value === "string") {
|
|
@@ -37,7 +53,7 @@ const DEFAULT_MAX_BODY_SIZE = 10 * 1024 * 1024;
|
|
|
37
53
|
* @param maxBodySize - Maximum body size in bytes (default: 10 MB)
|
|
38
54
|
*/
|
|
39
55
|
export function collectBody(
|
|
40
|
-
req:
|
|
56
|
+
req: BodyReadable,
|
|
41
57
|
headers: Record<string, string>,
|
|
42
58
|
maxBodySize = DEFAULT_MAX_BODY_SIZE,
|
|
43
59
|
): Promise<unknown> {
|
|
@@ -93,20 +109,40 @@ export function writeSchmockResponse(
|
|
|
93
109
|
...extraHeaders,
|
|
94
110
|
};
|
|
95
111
|
|
|
112
|
+
const hasContentType = Object.keys(responseHeaders).some(
|
|
113
|
+
(header) => header.toLowerCase() === "content-type",
|
|
114
|
+
);
|
|
115
|
+
|
|
96
116
|
if (
|
|
97
|
-
!
|
|
117
|
+
!hasContentType &&
|
|
118
|
+
response.body !== undefined &&
|
|
119
|
+
isBinaryBody(response.body)
|
|
120
|
+
) {
|
|
121
|
+
responseHeaders["content-type"] = "application/octet-stream";
|
|
122
|
+
} else if (
|
|
123
|
+
!hasContentType &&
|
|
98
124
|
response.body !== undefined &&
|
|
99
125
|
typeof response.body !== "string"
|
|
100
126
|
) {
|
|
101
127
|
responseHeaders["content-type"] = "application/json";
|
|
102
128
|
}
|
|
103
129
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
let responseBody: string | Uint8Array | undefined;
|
|
131
|
+
if (response.body === undefined) {
|
|
132
|
+
responseBody = undefined;
|
|
133
|
+
} else if (typeof response.body === "string") {
|
|
134
|
+
responseBody = response.body;
|
|
135
|
+
} else if (response.body instanceof ArrayBuffer) {
|
|
136
|
+
responseBody = new Uint8Array(response.body);
|
|
137
|
+
} else if (ArrayBuffer.isView(response.body)) {
|
|
138
|
+
responseBody = new Uint8Array(
|
|
139
|
+
response.body.buffer,
|
|
140
|
+
response.body.byteOffset,
|
|
141
|
+
response.body.byteLength,
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
responseBody = JSON.stringify(response.body);
|
|
145
|
+
}
|
|
110
146
|
|
|
111
147
|
res.writeHead(response.status, responseHeaders);
|
|
112
148
|
res.end(responseBody);
|
package/src/index.ts
CHANGED
package/src/interceptor.test.ts
CHANGED
|
@@ -26,6 +26,83 @@ describe("mock.intercept()", () => {
|
|
|
26
26
|
handle.restore();
|
|
27
27
|
});
|
|
28
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
|
+
|
|
29
106
|
it("passes through unmatched routes when passthrough is true", async () => {
|
|
30
107
|
mock("GET /api/users", [{ id: 1 }]);
|
|
31
108
|
// Save reference to the vi.fn() mock that the interceptor will call on passthrough
|
|
@@ -288,4 +365,124 @@ describe("mock.intercept()", () => {
|
|
|
288
365
|
|
|
289
366
|
handle.restore();
|
|
290
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
|
+
});
|
|
291
488
|
});
|