@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.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/abort.d.ts +3 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +30 -0
- package/dist/builder.d.ts +31 -10
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +760 -190
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +123 -2
- package/dist/errors.d.ts +14 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +41 -6
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +18 -2
- package/dist/http-helpers.d.ts +39 -4
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +147 -49
- package/dist/index.d.ts +292 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/interceptor.d.ts +11 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +316 -169
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +12 -3
- package/dist/plugin-pipeline.d.ts +3 -3
- package/dist/plugin-pipeline.d.ts.map +1 -1
- package/dist/plugin-pipeline.js +29 -14
- package/dist/response-normalizer.d.ts +16 -0
- package/dist/response-normalizer.d.ts.map +1 -0
- package/dist/response-normalizer.js +316 -0
- package/dist/response-parser.d.ts.map +1 -1
- package/dist/response-parser.js +39 -2
- package/dist/route-matcher.d.ts +3 -0
- package/dist/route-matcher.d.ts.map +1 -1
- package/dist/route-matcher.js +12 -6
- package/dist/types.d.ts +7 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -6
- package/src/audit-core-builder.test.ts +0 -218
- package/src/audit-response-guard.test.ts +0 -38
- package/src/binary.test.ts +0 -126
- package/src/binary.ts +0 -11
- package/src/builder.test.ts +0 -289
- package/src/builder.ts +0 -748
- package/src/constants.test.ts +0 -99
- package/src/constants.ts +0 -73
- package/src/debug.test.ts +0 -241
- package/src/delay.test.ts +0 -319
- package/src/dist-shape.test.ts +0 -35
- package/src/errors.test.ts +0 -223
- package/src/errors.ts +0 -130
- package/src/factory.test.ts +0 -133
- package/src/helpers.test.ts +0 -147
- package/src/helpers.ts +0 -58
- package/src/http-helpers.test.ts +0 -39
- package/src/http-helpers.ts +0 -149
- package/src/index.ts +0 -152
- package/src/interceptor.test.ts +0 -488
- package/src/interceptor.ts +0 -407
- package/src/namespace.test.ts +0 -274
- package/src/parser.property.test.ts +0 -630
- package/src/parser.test.ts +0 -148
- package/src/parser.ts +0 -64
- package/src/plugin-pipeline.ts +0 -208
- package/src/plugin-system.test.ts +0 -697
- package/src/response-parser.ts +0 -114
- package/src/response-parsing.test.ts +0 -333
- package/src/route-matcher.ts +0 -69
- package/src/route-matching.test.ts +0 -394
- package/src/server.test.ts +0 -234
- package/src/smart-defaults.test.ts +0 -361
- package/src/steps/async-support.steps.ts +0 -421
- package/src/steps/audit-core-builder.steps.ts +0 -188
- package/src/steps/audit-onerror-tuple.steps.ts +0 -83
- package/src/steps/basic-usage.steps.ts +0 -245
- package/src/steps/developer-experience.steps.ts +0 -204
- package/src/steps/error-handling.steps.ts +0 -555
- package/src/steps/fetch-interceptor.steps.ts +0 -457
- package/src/steps/fluent-api.steps.ts +0 -255
- package/src/steps/http-methods.steps.ts +0 -342
- package/src/steps/lifecycle-events.steps.ts +0 -149
- package/src/steps/performance-reliability.steps.ts +0 -90
- package/src/steps/plugin-integration.steps.ts +0 -334
- package/src/steps/request-history.steps.ts +0 -458
- package/src/steps/response-delay.steps.ts +0 -88
- package/src/steps/route-key-format.steps.ts +0 -99
- package/src/steps/standalone-server.steps.ts +0 -227
- package/src/steps/state-concurrency.steps.ts +0 -167
- package/src/types.ts +0 -36
package/package.json
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schmock/core",
|
|
3
3
|
"description": "Core functionality for Schmock",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.4.0",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/khalic-lab/schmock.git",
|
|
9
|
+
"directory": "packages/core"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/khalic-lab/schmock/tree/main/packages/core#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/khalic-lab/schmock/issues"
|
|
14
|
+
},
|
|
6
15
|
"main": "./dist/index.js",
|
|
7
16
|
"types": "./dist/index.d.ts",
|
|
8
17
|
"files": [
|
|
9
|
-
"dist"
|
|
10
|
-
"src"
|
|
18
|
+
"dist"
|
|
11
19
|
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"registry": "https://registry.npmjs.org/"
|
|
23
|
+
},
|
|
12
24
|
"exports": {
|
|
13
25
|
".": {
|
|
14
26
|
"types": "./dist/index.d.ts",
|
|
@@ -18,8 +30,9 @@
|
|
|
18
30
|
"./package.json": "./package.json"
|
|
19
31
|
},
|
|
20
32
|
"scripts": {
|
|
21
|
-
"
|
|
22
|
-
"build
|
|
33
|
+
"clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo",
|
|
34
|
+
"build": "bun run clean && bun run build:lib && bun run build:types",
|
|
35
|
+
"build:lib": "tsc -p tsconfig.runtime.json",
|
|
23
36
|
"build:types": "rm -f tsconfig.tsbuildinfo && tsc --build && node scripts/embed-ambient-types.js",
|
|
24
37
|
"pretest": "rm -f src/*.js src/*.d.ts || true",
|
|
25
38
|
"test": "vitest run",
|
|
@@ -37,7 +50,6 @@
|
|
|
37
50
|
"devDependencies": {
|
|
38
51
|
"@amiceli/vitest-cucumber": "^7.0.0",
|
|
39
52
|
"@types/node": "^26.1.0",
|
|
40
|
-
"@vitest/ui": "^4.1.9",
|
|
41
53
|
"vitest": "^4.1.9"
|
|
42
54
|
}
|
|
43
55
|
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { schmock } from "./index";
|
|
3
|
-
|
|
4
|
-
// ── FIX 1.2: defineRoute must shallow-clone the config ────────────────────────
|
|
5
|
-
|
|
6
|
-
describe("FIX 1.2 — defineRoute must not mutate the caller's config", () => {
|
|
7
|
-
it("does not add contentType to the caller's config object", () => {
|
|
8
|
-
const sharedConfig: Schmock.RouteConfig = {};
|
|
9
|
-
const mock = schmock();
|
|
10
|
-
mock("GET /a", { x: 1 }, sharedConfig);
|
|
11
|
-
// sharedConfig must remain empty — defineRoute must work on a clone
|
|
12
|
-
expect(Object.keys(sharedConfig)).toHaveLength(0);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("second route registered with same config is not polluted by first route detection", () => {
|
|
16
|
-
const sharedConfig: Schmock.RouteConfig = {};
|
|
17
|
-
const mock = schmock();
|
|
18
|
-
mock("GET /first", { x: 1 }, sharedConfig);
|
|
19
|
-
mock("GET /second", "hello", sharedConfig);
|
|
20
|
-
// sharedConfig must still be clean — neither call should have written to it
|
|
21
|
-
expect(Object.keys(sharedConfig)).toHaveLength(0);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("mutating caller's config after registration does not change the registered route response", async () => {
|
|
25
|
-
const config: Schmock.RouteConfig = {};
|
|
26
|
-
const mock = schmock();
|
|
27
|
-
mock("GET /item", { value: 99 }, config);
|
|
28
|
-
|
|
29
|
-
// After registration, mutate the caller's config
|
|
30
|
-
config.status = 503;
|
|
31
|
-
config.contentType = "text/html";
|
|
32
|
-
|
|
33
|
-
const response = await mock.handle("GET", "/item");
|
|
34
|
-
// The registered route must use its own cloned config — not 503
|
|
35
|
-
expect(response.status).toBe(200);
|
|
36
|
-
const ct =
|
|
37
|
-
response.headers?.["content-type"] ?? response.headers?.["Content-Type"];
|
|
38
|
-
expect(ct).toContain("application/json");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("each route keeps its own detected contentType even when sharing the same config object", async () => {
|
|
42
|
-
const sharedConfig: Schmock.RouteConfig = {};
|
|
43
|
-
const mock = schmock();
|
|
44
|
-
// First route: object → application/json
|
|
45
|
-
mock("GET /obj", { value: 1 }, sharedConfig);
|
|
46
|
-
// Second route: primitive string → text/plain
|
|
47
|
-
mock("GET /str", "hello", sharedConfig);
|
|
48
|
-
|
|
49
|
-
const objResp = await mock.handle("GET", "/obj");
|
|
50
|
-
const strResp = await mock.handle("GET", "/str");
|
|
51
|
-
|
|
52
|
-
const ctObj =
|
|
53
|
-
objResp.headers?.["content-type"] ?? objResp.headers?.["Content-Type"];
|
|
54
|
-
const ctStr =
|
|
55
|
-
strResp.headers?.["content-type"] ?? strResp.headers?.["Content-Type"];
|
|
56
|
-
|
|
57
|
-
expect(ctObj).toContain("application/json");
|
|
58
|
-
expect(ctStr).toContain("text/plain");
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// ── FIX 2.2: duplicate route detection must normalize trailing slash ──────────
|
|
63
|
-
|
|
64
|
-
describe("FIX 2.2 — duplicate-route detection uses normalized paths", () => {
|
|
65
|
-
it("GET /users/ is treated as duplicate of GET /users", () => {
|
|
66
|
-
const mock = schmock();
|
|
67
|
-
mock("GET /users", [{ id: 1 }], {});
|
|
68
|
-
// Registering with trailing slash must not add a second route
|
|
69
|
-
mock("GET /users/", [{ id: 2 }], {});
|
|
70
|
-
|
|
71
|
-
const routes = mock.getRoutes();
|
|
72
|
-
const userRoutes = routes.filter(
|
|
73
|
-
(r) =>
|
|
74
|
-
r.method === "GET" && (r.path === "/users" || r.path === "/users/"),
|
|
75
|
-
);
|
|
76
|
-
expect(userRoutes).toHaveLength(1);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("first registration wins — trailing-slash variant does not overwrite", async () => {
|
|
80
|
-
const mock = schmock();
|
|
81
|
-
mock("GET /items", [{ id: 1 }], {});
|
|
82
|
-
mock("GET /items/", [{ id: 999 }], {});
|
|
83
|
-
|
|
84
|
-
// Should still get the first registration's data
|
|
85
|
-
const resp = await mock.handle("GET", "/items");
|
|
86
|
-
expect(resp.status).toBe(200);
|
|
87
|
-
expect(Array.isArray(resp.body)).toBe(true);
|
|
88
|
-
expect((resp.body as { id: number }[])[0].id).toBe(1);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("registering verbatim duplicate also produces only one route", () => {
|
|
92
|
-
const mock = schmock();
|
|
93
|
-
mock("GET /same", { ok: true }, {});
|
|
94
|
-
mock("GET /same", { ok: false }, {});
|
|
95
|
-
|
|
96
|
-
const routes = mock.getRoutes();
|
|
97
|
-
const sameRoutes = routes.filter(
|
|
98
|
-
(r) => r.method === "GET" && r.path === "/same",
|
|
99
|
-
);
|
|
100
|
-
expect(sameRoutes).toHaveLength(1);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// ── FIX 3.3: reset()/resetState() must reassign, not delete keys ─────────────
|
|
105
|
-
|
|
106
|
-
describe("FIX 3.3 — reset()/resetState() must not mutate caller's state", () => {
|
|
107
|
-
it("reset() leaves the external state object untouched", () => {
|
|
108
|
-
const st = { a: 1 };
|
|
109
|
-
const mock = schmock({ state: st });
|
|
110
|
-
mock.reset();
|
|
111
|
-
expect(st).toEqual({ a: 1 });
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("resetState() leaves the external state object untouched", () => {
|
|
115
|
-
const st = { b: 2 };
|
|
116
|
-
const mock = schmock({ state: st });
|
|
117
|
-
mock.resetState();
|
|
118
|
-
expect(st).toEqual({ b: 2 });
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("mock.getState() is empty after reset()", () => {
|
|
122
|
-
const st = { x: 10 };
|
|
123
|
-
const mock = schmock({ state: st });
|
|
124
|
-
mock.reset();
|
|
125
|
-
expect(mock.getState()).toEqual({});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("mock.getState() is empty after resetState()", () => {
|
|
129
|
-
const st = { y: 20 };
|
|
130
|
-
const mock = schmock({ state: st });
|
|
131
|
-
mock.resetState();
|
|
132
|
-
expect(mock.getState()).toEqual({});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("generator sees empty state after resetState", async () => {
|
|
136
|
-
const st: Record<string, unknown> = { counter: 0 };
|
|
137
|
-
const mock = schmock({ state: st });
|
|
138
|
-
mock("GET /stateful", (ctx: Schmock.RequestContext) => {
|
|
139
|
-
return { stateKeys: Object.keys(ctx.state) };
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
mock.resetState();
|
|
143
|
-
|
|
144
|
-
const resp = await mock.handle("GET", "/stateful");
|
|
145
|
-
expect(resp.status).toBe(200);
|
|
146
|
-
// After resetState, the generator should see an empty state
|
|
147
|
-
expect((resp.body as { stateKeys: string[] }).stateKeys).toHaveLength(0);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// ── FIX 2.3: history()/lastRequest() must return deep clones ─────────────────
|
|
152
|
-
|
|
153
|
-
describe("FIX 2.3 — history() and lastRequest() return deep clones", () => {
|
|
154
|
-
it("mutating a history() record's nested body does not corrupt internal history", async () => {
|
|
155
|
-
const mock = schmock();
|
|
156
|
-
mock("GET /nested", { deep: { value: "original" } }, {});
|
|
157
|
-
|
|
158
|
-
await mock.handle("GET", "/nested");
|
|
159
|
-
|
|
160
|
-
// First read: mutate the returned record's body
|
|
161
|
-
const records1 = mock.history();
|
|
162
|
-
const firstRecord = records1[0];
|
|
163
|
-
(firstRecord.response.body as { deep: { value: string } }).deep.value =
|
|
164
|
-
"MUTATED";
|
|
165
|
-
|
|
166
|
-
// Second read: internal history must be unchanged
|
|
167
|
-
const records2 = mock.history();
|
|
168
|
-
expect(
|
|
169
|
-
(records2[0].response.body as { deep: { value: string } }).deep.value,
|
|
170
|
-
).toBe("original");
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("mutating filtered history() record does not corrupt internal history", async () => {
|
|
174
|
-
const mock = schmock();
|
|
175
|
-
mock("GET /data", { info: { count: 42 } }, {});
|
|
176
|
-
await mock.handle("GET", "/data");
|
|
177
|
-
|
|
178
|
-
const filtered = mock.history("GET", "/data");
|
|
179
|
-
(filtered[0].response.body as { info: { count: number } }).info.count = 0;
|
|
180
|
-
|
|
181
|
-
const again = mock.history("GET", "/data");
|
|
182
|
-
expect(
|
|
183
|
-
(again[0].response.body as { info: { count: number } }).info.count,
|
|
184
|
-
).toBe(42);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("mutating lastRequest() record does not corrupt internal history", async () => {
|
|
188
|
-
const mock = schmock();
|
|
189
|
-
mock("GET /last", { meta: { tag: "safe" } }, {});
|
|
190
|
-
await mock.handle("GET", "/last");
|
|
191
|
-
|
|
192
|
-
const last = mock.lastRequest();
|
|
193
|
-
if (!last) throw new Error("expected a record");
|
|
194
|
-
(last.response.body as { meta: { tag: string } }).meta.tag = "corrupted";
|
|
195
|
-
|
|
196
|
-
const again = mock.lastRequest();
|
|
197
|
-
if (!again) throw new Error("expected a record");
|
|
198
|
-
expect((again.response.body as { meta: { tag: string } }).meta.tag).toBe(
|
|
199
|
-
"safe",
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("mutating filtered lastRequest() record does not corrupt internal history", async () => {
|
|
204
|
-
const mock = schmock();
|
|
205
|
-
mock("POST /echo", { msg: { text: "hello" } }, {});
|
|
206
|
-
await mock.handle("POST", "/echo");
|
|
207
|
-
|
|
208
|
-
const last = mock.lastRequest("POST", "/echo");
|
|
209
|
-
if (!last) throw new Error("expected a record");
|
|
210
|
-
(last.response.body as { msg: { text: string } }).msg.text = "CHANGED";
|
|
211
|
-
|
|
212
|
-
const again = mock.lastRequest("POST", "/echo");
|
|
213
|
-
if (!again) throw new Error("expected a record");
|
|
214
|
-
expect((again.response.body as { msg: { text: string } }).msg.text).toBe(
|
|
215
|
-
"hello",
|
|
216
|
-
);
|
|
217
|
-
});
|
|
218
|
-
});
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { schmock } from "./index";
|
|
3
|
-
|
|
4
|
-
describe("isResponseObject guard — status type check", () => {
|
|
5
|
-
describe("non-numeric status must NOT be treated as response envelope", () => {
|
|
6
|
-
it("returns status 200 (default) when generator returns { status: 'ok', body: 'hello' }", async () => {
|
|
7
|
-
const mock = schmock();
|
|
8
|
-
mock("GET /bad-envelope", () => ({ status: "ok", body: "hello" }));
|
|
9
|
-
|
|
10
|
-
const response = await mock.handle("GET", "/bad-envelope");
|
|
11
|
-
|
|
12
|
-
// The string status must never leak into response.status
|
|
13
|
-
expect(response.status).toBe(200);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("returns the whole object as body when generator returns { status: 'ok', body: 'hello' }", async () => {
|
|
17
|
-
const mock = schmock();
|
|
18
|
-
mock("GET /bad-envelope-body", () => ({ status: "ok", body: "hello" }));
|
|
19
|
-
|
|
20
|
-
const response = await mock.handle("GET", "/bad-envelope-body");
|
|
21
|
-
|
|
22
|
-
// The original object must be treated as plain body, not unwrapped
|
|
23
|
-
expect(response.body).toEqual({ status: "ok", body: "hello" });
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe("numeric status IS correctly treated as response envelope", () => {
|
|
28
|
-
it("returns status 201 and unwrapped body when generator returns { status: 201, body: { id: 1 } }", async () => {
|
|
29
|
-
const mock = schmock();
|
|
30
|
-
mock("POST /create", () => ({ status: 201, body: { id: 1 } }));
|
|
31
|
-
|
|
32
|
-
const response = await mock.handle("POST", "/create");
|
|
33
|
-
|
|
34
|
-
expect(response.status).toBe(201);
|
|
35
|
-
expect(response.body).toEqual({ id: 1 });
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
});
|
package/src/binary.test.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { isBinaryBody } from "./binary.js";
|
|
3
|
-
import { schmock } from "./index.js";
|
|
4
|
-
import { parseResponse } from "./response-parser.js";
|
|
5
|
-
|
|
6
|
-
function binaryBytes(value: unknown): number[] {
|
|
7
|
-
if (value instanceof ArrayBuffer) {
|
|
8
|
-
return [...new Uint8Array(value)];
|
|
9
|
-
}
|
|
10
|
-
if (ArrayBuffer.isView(value)) {
|
|
11
|
-
return [
|
|
12
|
-
...new Uint8Array(value.buffer, value.byteOffset, value.byteLength),
|
|
13
|
-
];
|
|
14
|
-
}
|
|
15
|
-
throw new Error("Expected a binary response body");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("isBinaryBody", () => {
|
|
19
|
-
it("recognizes browser binary values without a Node Buffer global", () => {
|
|
20
|
-
expect(isBinaryBody(new Uint8Array([1, 2, 3]))).toBe(true);
|
|
21
|
-
expect(isBinaryBody(new ArrayBuffer(3))).toBe(true);
|
|
22
|
-
expect(isBinaryBody(new DataView(new SharedArrayBuffer(3)))).toBe(true);
|
|
23
|
-
expect(isBinaryBody({ 0: 1, length: 1 })).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe("parseResponse binary handling", () => {
|
|
28
|
-
it("stringifies text objects when Buffer is not available", () => {
|
|
29
|
-
const originalBuffer = globalThis.Buffer;
|
|
30
|
-
Reflect.deleteProperty(globalThis, "Buffer");
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
const response = parseResponse(
|
|
34
|
-
{ message: "browser" },
|
|
35
|
-
{ contentType: "text/plain" },
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
expect(response.body).toBe('{"message":"browser"}');
|
|
39
|
-
} finally {
|
|
40
|
-
globalThis.Buffer = originalBuffer;
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("detects binary returned by a dynamic generator", async () => {
|
|
45
|
-
const mock = schmock();
|
|
46
|
-
mock("GET /dynamic-binary", () => new Uint8Array([1, 2, 3]));
|
|
47
|
-
|
|
48
|
-
const response = await mock.handle("GET", "/dynamic-binary");
|
|
49
|
-
|
|
50
|
-
expect(response.headers["content-type"]).toBe("application/octet-stream");
|
|
51
|
-
expect(binaryBytes(response.body)).toEqual([1, 2, 3]);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("detects binary in a status tuple", async () => {
|
|
55
|
-
const mock = schmock();
|
|
56
|
-
const bytes = new Uint8Array([4, 5, 6]);
|
|
57
|
-
mock(
|
|
58
|
-
"GET /tuple-binary",
|
|
59
|
-
() => [206, new DataView(bytes.buffer)] satisfies [number, unknown],
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const response = await mock.handle("GET", "/tuple-binary");
|
|
63
|
-
|
|
64
|
-
expect(response.status).toBe(206);
|
|
65
|
-
expect(response.headers["content-type"]).toBe("application/octet-stream");
|
|
66
|
-
expect(binaryBytes(response.body)).toEqual([4, 5, 6]);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("detects binary in a response object", () => {
|
|
70
|
-
const response = parseResponse(
|
|
71
|
-
{ status: 206, body: new Uint8Array([7, 8]), headers: {} },
|
|
72
|
-
{},
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
expect(response.status).toBe(206);
|
|
76
|
-
expect(response.headers["content-type"]).toBe("application/octet-stream");
|
|
77
|
-
expect(binaryBytes(response.body)).toEqual([7, 8]);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("preserves an explicit binary content type", async () => {
|
|
81
|
-
const mock = schmock();
|
|
82
|
-
mock("GET /image", () => new Uint8Array([137, 80, 78, 71]), {
|
|
83
|
-
contentType: "image/png",
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const response = await mock.handle("GET", "/image");
|
|
87
|
-
|
|
88
|
-
expect(response.headers["content-type"]).toBe("image/png");
|
|
89
|
-
expect(binaryBytes(response.body)).toEqual([137, 80, 78, 71]);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("preserves a tuple content-type header without adding a duplicate", () => {
|
|
93
|
-
const response = parseResponse(
|
|
94
|
-
[
|
|
95
|
-
200,
|
|
96
|
-
new Uint8Array([1]),
|
|
97
|
-
{ "Content-Type": "application/custom-binary" },
|
|
98
|
-
],
|
|
99
|
-
{ contentType: "application/json" },
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
expect(response.headers).toEqual({
|
|
103
|
-
"Content-Type": "application/custom-binary",
|
|
104
|
-
});
|
|
105
|
-
expect(binaryBytes(response.body)).toEqual([1]);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe("browser runtime compatibility", () => {
|
|
110
|
-
it("defines and serves object routes when Buffer is not available", async () => {
|
|
111
|
-
const originalBuffer = globalThis.Buffer;
|
|
112
|
-
Reflect.deleteProperty(globalThis, "Buffer");
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const mock = schmock();
|
|
116
|
-
mock("GET /browser", { runtime: "browser" });
|
|
117
|
-
|
|
118
|
-
const response = await mock.handle("GET", "/browser");
|
|
119
|
-
expect(response.status).toBe(200);
|
|
120
|
-
expect(response.body).toEqual({ runtime: "browser" });
|
|
121
|
-
expect(response.headers["content-type"]).toBe("application/json");
|
|
122
|
-
} finally {
|
|
123
|
-
globalThis.Buffer = originalBuffer;
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
});
|
package/src/binary.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser-safe binary body detection.
|
|
3
|
-
*
|
|
4
|
-
* Every ArrayBuffer view is accepted, including DataView, typed arrays backed
|
|
5
|
-
* by SharedArrayBuffer, and Node.js Buffer without referencing its global.
|
|
6
|
-
*/
|
|
7
|
-
export function isBinaryBody(
|
|
8
|
-
value: unknown,
|
|
9
|
-
): value is ArrayBuffer | ArrayBufferView {
|
|
10
|
-
return value instanceof ArrayBuffer || ArrayBuffer.isView(value);
|
|
11
|
-
}
|