@schmock/core 2.3.0 → 2.3.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/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 +14 -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
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { schmock } from "./index";
|
|
3
|
-
|
|
4
|
-
describe("route matching", () => {
|
|
5
|
-
describe("static routes", () => {
|
|
6
|
-
it("matches exact static routes", async () => {
|
|
7
|
-
const mock = schmock();
|
|
8
|
-
mock("GET /users", "users");
|
|
9
|
-
mock("GET /posts", "posts");
|
|
10
|
-
|
|
11
|
-
const users = await mock.handle("GET", "/users");
|
|
12
|
-
const posts = await mock.handle("GET", "/posts");
|
|
13
|
-
|
|
14
|
-
expect(users.body).toBe("users");
|
|
15
|
-
expect(posts.body).toBe("posts");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("differentiates between HTTP methods", async () => {
|
|
19
|
-
const mock = schmock();
|
|
20
|
-
mock("GET /resource", "get-response");
|
|
21
|
-
mock("POST /resource", "post-response");
|
|
22
|
-
mock("PUT /resource", "put-response");
|
|
23
|
-
mock("DELETE /resource", "delete-response");
|
|
24
|
-
|
|
25
|
-
const getRes = await mock.handle("GET", "/resource");
|
|
26
|
-
const postRes = await mock.handle("POST", "/resource");
|
|
27
|
-
const putRes = await mock.handle("PUT", "/resource");
|
|
28
|
-
const deleteRes = await mock.handle("DELETE", "/resource");
|
|
29
|
-
|
|
30
|
-
expect(getRes.body).toBe("get-response");
|
|
31
|
-
expect(postRes.body).toBe("post-response");
|
|
32
|
-
expect(putRes.body).toBe("put-response");
|
|
33
|
-
expect(deleteRes.body).toBe("delete-response");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("handles root path correctly", async () => {
|
|
37
|
-
const mock = schmock();
|
|
38
|
-
mock("GET /", "root");
|
|
39
|
-
|
|
40
|
-
const response = await mock.handle("GET", "/");
|
|
41
|
-
expect(response.body).toBe("root");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("case sensitive path matching", async () => {
|
|
45
|
-
const mock = schmock();
|
|
46
|
-
mock("GET /Users", "capital-users");
|
|
47
|
-
|
|
48
|
-
const response1 = await mock.handle("GET", "/Users");
|
|
49
|
-
const response2 = await mock.handle("GET", "/users");
|
|
50
|
-
|
|
51
|
-
expect(response1.body).toBe("capital-users");
|
|
52
|
-
expect(response2.status).toBe(404);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("parameterized routes", () => {
|
|
57
|
-
it("matches single parameter routes", async () => {
|
|
58
|
-
const mock = schmock();
|
|
59
|
-
mock("GET /users/:id", ({ params }) => ({ userId: params.id }));
|
|
60
|
-
|
|
61
|
-
const response = await mock.handle("GET", "/users/123");
|
|
62
|
-
|
|
63
|
-
expect(response.body).toEqual({ userId: "123" });
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("matches multiple parameter routes", async () => {
|
|
67
|
-
const mock = schmock();
|
|
68
|
-
mock("GET /users/:userId/posts/:postId", ({ params }) => ({
|
|
69
|
-
user: params.userId,
|
|
70
|
-
post: params.postId,
|
|
71
|
-
}));
|
|
72
|
-
|
|
73
|
-
const response = await mock.handle("GET", "/users/456/posts/789");
|
|
74
|
-
|
|
75
|
-
expect(response.body).toEqual({ user: "456", post: "789" });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("handles parameters with special characters", async () => {
|
|
79
|
-
const mock = schmock();
|
|
80
|
-
mock("GET /files/:filename", ({ params }) => ({ file: params.filename }));
|
|
81
|
-
|
|
82
|
-
const response = await mock.handle("GET", "/files/test-file.txt");
|
|
83
|
-
|
|
84
|
-
expect(response.body).toEqual({ file: "test-file.txt" });
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("handles parameters with numbers", async () => {
|
|
88
|
-
const mock = schmock();
|
|
89
|
-
mock("GET /items/:id", ({ params }) => ({ itemId: params.id }));
|
|
90
|
-
|
|
91
|
-
const response = await mock.handle("GET", "/items/12345");
|
|
92
|
-
|
|
93
|
-
expect(response.body).toEqual({ itemId: "12345" });
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("handles parameters with underscores and hyphens", async () => {
|
|
97
|
-
const mock = schmock();
|
|
98
|
-
mock("GET /api/:snake_case/:kebab-case", ({ params }) => params);
|
|
99
|
-
|
|
100
|
-
const response = await mock.handle(
|
|
101
|
-
"GET",
|
|
102
|
-
"/api/test_value/another-value",
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
expect(response.body).toEqual({
|
|
106
|
-
snake_case: "test_value",
|
|
107
|
-
"kebab-case": "another-value",
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("doesn't match if parameter segment is empty", async () => {
|
|
112
|
-
const mock = schmock();
|
|
113
|
-
mock("GET /users/:id", "found");
|
|
114
|
-
|
|
115
|
-
const response = await mock.handle("GET", "/users/");
|
|
116
|
-
|
|
117
|
-
expect(response.status).toBe(404);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("doesn't match parameters across path segments", async () => {
|
|
121
|
-
const mock = schmock();
|
|
122
|
-
mock("GET /users/:id", "found");
|
|
123
|
-
|
|
124
|
-
const response = await mock.handle("GET", "/users/123/extra");
|
|
125
|
-
|
|
126
|
-
expect(response.status).toBe(404);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe("route precedence and conflicts", () => {
|
|
131
|
-
it("prioritizes static routes over parameterized routes", async () => {
|
|
132
|
-
const mock = schmock();
|
|
133
|
-
mock("GET /users/:id", "parameterized");
|
|
134
|
-
mock("GET /users/special", "static");
|
|
135
|
-
|
|
136
|
-
const paramResponse = await mock.handle("GET", "/users/123");
|
|
137
|
-
const staticResponse = await mock.handle("GET", "/users/special");
|
|
138
|
-
|
|
139
|
-
// Static routes should always be checked before parameterized routes
|
|
140
|
-
expect(paramResponse.body).toBe("parameterized");
|
|
141
|
-
expect(staticResponse.body).toBe("static");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("handles exact vs parameterized route conflicts", async () => {
|
|
145
|
-
const mock = schmock();
|
|
146
|
-
mock("GET /api/:version/users", "versioned");
|
|
147
|
-
mock("GET /api/v1/users", "v1-specific");
|
|
148
|
-
|
|
149
|
-
const versionedResponse = await mock.handle("GET", "/api/v2/users");
|
|
150
|
-
const v1Response = await mock.handle("GET", "/api/v1/users");
|
|
151
|
-
|
|
152
|
-
expect(versionedResponse.body).toBe("versioned");
|
|
153
|
-
expect(v1Response.body).toBe("v1-specific");
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("matches routes in registration order (first registered wins)", async () => {
|
|
157
|
-
const mock = schmock();
|
|
158
|
-
mock("GET /:type/items", "first");
|
|
159
|
-
mock("GET /shop/:category", "second");
|
|
160
|
-
|
|
161
|
-
const response = await mock.handle("GET", "/shop/items");
|
|
162
|
-
|
|
163
|
-
// Both routes match, but the first registered route should win
|
|
164
|
-
// This matches the behavior of Express, Hono, Fastify, etc.
|
|
165
|
-
expect(response.body).toBe("first");
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("matches specific routes before wildcard when registered in natural order", async () => {
|
|
169
|
-
// Bug report reproduction: natural order (specific before wildcard)
|
|
170
|
-
const mock = schmock();
|
|
171
|
-
mock("GET /api/items/special", () => ({ type: "special" }));
|
|
172
|
-
mock("GET /api/items/:id", () => ({ type: "generic" }));
|
|
173
|
-
|
|
174
|
-
const specialResult = await mock.handle("GET", "/api/items/special");
|
|
175
|
-
const genericResult = await mock.handle("GET", "/api/items/123");
|
|
176
|
-
|
|
177
|
-
// Static route should match for /api/items/special
|
|
178
|
-
expect(specialResult.body).toEqual({ type: "special" });
|
|
179
|
-
// Parameterized route should match for /api/items/123
|
|
180
|
-
expect(genericResult.body).toEqual({ type: "generic" });
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("matches multiple specific routes before wildcard", async () => {
|
|
184
|
-
// Bug report scenario with multiple specific routes
|
|
185
|
-
const mock = schmock();
|
|
186
|
-
mock("GET /api/vulns/aggregated", "aggregated");
|
|
187
|
-
mock("GET /api/vulns/count", "count");
|
|
188
|
-
mock("GET /api/vulns/familyList", "familyList");
|
|
189
|
-
mock("GET /api/vulns/:vulnId", "byId");
|
|
190
|
-
|
|
191
|
-
const aggregatedRes = await mock.handle("GET", "/api/vulns/aggregated");
|
|
192
|
-
const countRes = await mock.handle("GET", "/api/vulns/count");
|
|
193
|
-
const familyListRes = await mock.handle("GET", "/api/vulns/familyList");
|
|
194
|
-
const byIdRes = await mock.handle("GET", "/api/vulns/CVE-2024-1234");
|
|
195
|
-
|
|
196
|
-
expect(aggregatedRes.body).toBe("aggregated");
|
|
197
|
-
expect(countRes.body).toBe("count");
|
|
198
|
-
expect(familyListRes.body).toBe("familyList");
|
|
199
|
-
expect(byIdRes.body).toBe("byId");
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("matches overlapping parameterized routes in registration order", async () => {
|
|
203
|
-
const mock = schmock();
|
|
204
|
-
mock("GET /api/:org/users/:id", "first-pattern");
|
|
205
|
-
mock("GET /api/:version/users/:userId", "second-pattern");
|
|
206
|
-
|
|
207
|
-
const response = await mock.handle("GET", "/api/acme/users/123");
|
|
208
|
-
|
|
209
|
-
// When both routes are parameterized and match, first registered wins
|
|
210
|
-
expect(response.body).toBe("first-pattern");
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
describe("complex path patterns", () => {
|
|
215
|
-
it("handles deeply nested parameterized routes", async () => {
|
|
216
|
-
const mock = schmock();
|
|
217
|
-
mock(
|
|
218
|
-
"GET /api/:version/users/:userId/posts/:postId/comments/:commentId",
|
|
219
|
-
({ params }) => params,
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
const response = await mock.handle(
|
|
223
|
-
"GET",
|
|
224
|
-
"/api/v1/users/123/posts/456/comments/789",
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
expect(response.body).toEqual({
|
|
228
|
-
version: "v1",
|
|
229
|
-
userId: "123",
|
|
230
|
-
postId: "456",
|
|
231
|
-
commentId: "789",
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("handles mixed static and parameterized segments", async () => {
|
|
236
|
-
const mock = schmock();
|
|
237
|
-
mock(
|
|
238
|
-
"GET /api/v1/users/:userId/profile/settings/:setting",
|
|
239
|
-
({ params }) => params,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
const response = await mock.handle(
|
|
243
|
-
"GET",
|
|
244
|
-
"/api/v1/users/789/profile/settings/privacy",
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
expect(response.body).toEqual({
|
|
248
|
-
userId: "789",
|
|
249
|
-
setting: "privacy",
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("handles paths with similar prefixes", async () => {
|
|
254
|
-
const mock = schmock();
|
|
255
|
-
mock("GET /user", "single-user");
|
|
256
|
-
mock("GET /users", "all-users");
|
|
257
|
-
mock("GET /users/:id", "specific-user");
|
|
258
|
-
|
|
259
|
-
const singleResponse = await mock.handle("GET", "/user");
|
|
260
|
-
const allResponse = await mock.handle("GET", "/users");
|
|
261
|
-
const specificResponse = await mock.handle("GET", "/users/123");
|
|
262
|
-
|
|
263
|
-
expect(singleResponse.body).toBe("single-user");
|
|
264
|
-
expect(allResponse.body).toBe("all-users");
|
|
265
|
-
expect(specificResponse.body).toBe("specific-user");
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
describe("edge cases", () => {
|
|
270
|
-
it("handles empty parameter values correctly", async () => {
|
|
271
|
-
const mock = schmock();
|
|
272
|
-
mock("GET /search/:query", ({ params }) => ({ query: params.query }));
|
|
273
|
-
|
|
274
|
-
// This should not match because parameter is empty
|
|
275
|
-
const response = await mock.handle("GET", "/search/");
|
|
276
|
-
|
|
277
|
-
expect(response.status).toBe(404);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("handles special characters in paths", async () => {
|
|
281
|
-
const mock = schmock();
|
|
282
|
-
mock("GET /files/:filename", ({ params }) => ({ file: params.filename }));
|
|
283
|
-
|
|
284
|
-
const response = await mock.handle("GET", "/files/my-file.test.json");
|
|
285
|
-
|
|
286
|
-
expect(response.body).toEqual({ file: "my-file.test.json" });
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("handles URL encoded characters in parameters", async () => {
|
|
290
|
-
const mock = schmock();
|
|
291
|
-
mock("GET /search/:query", ({ params }) => ({ query: params.query }));
|
|
292
|
-
|
|
293
|
-
// Note: This tests the raw parameter, URL decoding would happen at HTTP layer
|
|
294
|
-
const response = await mock.handle("GET", "/search/hello%20world");
|
|
295
|
-
|
|
296
|
-
expect(response.body).toEqual({ query: "hello%20world" });
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("handles very long parameter values", async () => {
|
|
300
|
-
const mock = schmock();
|
|
301
|
-
mock("GET /data/:id", ({ params }) => ({ length: params.id.length }));
|
|
302
|
-
|
|
303
|
-
const longId = "a".repeat(1000);
|
|
304
|
-
const response = await mock.handle("GET", `/data/${longId}`);
|
|
305
|
-
|
|
306
|
-
expect(response.body).toEqual({ length: 1000 });
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("handles numeric parameter values", async () => {
|
|
310
|
-
const mock = schmock();
|
|
311
|
-
mock("GET /items/:id", ({ params }) => ({
|
|
312
|
-
id: params.id,
|
|
313
|
-
type: typeof params.id,
|
|
314
|
-
parsed: Number.parseInt(params.id, 10),
|
|
315
|
-
}));
|
|
316
|
-
|
|
317
|
-
const response = await mock.handle("GET", "/items/12345");
|
|
318
|
-
|
|
319
|
-
expect(response.body).toEqual({
|
|
320
|
-
id: "12345",
|
|
321
|
-
type: "string",
|
|
322
|
-
parsed: 12345,
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
describe("no route found scenarios", () => {
|
|
328
|
-
it("returns 404 for completely unmatched paths", async () => {
|
|
329
|
-
const mock = schmock();
|
|
330
|
-
mock("GET /existing", "found");
|
|
331
|
-
|
|
332
|
-
const response = await mock.handle("GET", "/nonexistent");
|
|
333
|
-
|
|
334
|
-
expect(response.status).toBe(404);
|
|
335
|
-
expect(response.body.error).toContain("Route not found");
|
|
336
|
-
expect(response.body.code).toBe("ROUTE_NOT_FOUND");
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it("returns 404 for wrong HTTP method", async () => {
|
|
340
|
-
const mock = schmock();
|
|
341
|
-
mock("GET /resource", "get-only");
|
|
342
|
-
|
|
343
|
-
const response = await mock.handle("POST", "/resource");
|
|
344
|
-
|
|
345
|
-
expect(response.status).toBe(404);
|
|
346
|
-
expect(response.body.error).toContain("POST /resource");
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it("returns 404 for partial path matches", async () => {
|
|
350
|
-
const mock = schmock();
|
|
351
|
-
mock("GET /api/users", "found");
|
|
352
|
-
|
|
353
|
-
const response1 = await mock.handle("GET", "/api");
|
|
354
|
-
const response2 = await mock.handle("GET", "/api/users/extra");
|
|
355
|
-
|
|
356
|
-
expect(response1.status).toBe(404);
|
|
357
|
-
expect(response2.status).toBe(404);
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
describe("regex pattern validation", () => {
|
|
362
|
-
it("escapes special regex characters in static paths", async () => {
|
|
363
|
-
const mock = schmock();
|
|
364
|
-
mock("GET /api/test.json", "json-file");
|
|
365
|
-
mock("GET /api/test*json", "wildcard-file");
|
|
366
|
-
|
|
367
|
-
const jsonResponse = await mock.handle("GET", "/api/test.json");
|
|
368
|
-
const wildcardResponse = await mock.handle("GET", "/api/test*json");
|
|
369
|
-
const dotResponse = await mock.handle("GET", "/api/testXjson"); // Should not match
|
|
370
|
-
|
|
371
|
-
expect(jsonResponse.body).toBe("json-file");
|
|
372
|
-
expect(wildcardResponse.body).toBe("wildcard-file");
|
|
373
|
-
expect(dotResponse.status).toBe(404);
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
it("handles paths with parentheses", async () => {
|
|
377
|
-
const mock = schmock();
|
|
378
|
-
mock("GET /api/(v1)/users", "versioned");
|
|
379
|
-
|
|
380
|
-
const response = await mock.handle("GET", "/api/(v1)/users");
|
|
381
|
-
|
|
382
|
-
expect(response.body).toBe("versioned");
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it("handles paths with square brackets", async () => {
|
|
386
|
-
const mock = schmock();
|
|
387
|
-
mock("GET /api/[admin]/users", "admin-users");
|
|
388
|
-
|
|
389
|
-
const response = await mock.handle("GET", "/api/[admin]/users");
|
|
390
|
-
|
|
391
|
-
expect(response.body).toBe("admin-users");
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
});
|
package/src/server.test.ts
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
2
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
-
import { schmock } from "./index";
|
|
4
|
-
|
|
5
|
-
describe("Standalone Server", () => {
|
|
6
|
-
let mock: Schmock.CallableMockInstance;
|
|
7
|
-
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
mock?.close();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("returns actual port when port=0", async () => {
|
|
13
|
-
mock = schmock();
|
|
14
|
-
mock("GET /test", { ok: true });
|
|
15
|
-
const info = await mock.listen(0);
|
|
16
|
-
expect(info.port).toBeGreaterThan(0);
|
|
17
|
-
expect(info.hostname).toBe("127.0.0.1");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("propagates request headers", async () => {
|
|
21
|
-
mock = schmock();
|
|
22
|
-
mock("GET /headers", ({ headers }) => ({
|
|
23
|
-
auth: headers.authorization,
|
|
24
|
-
}));
|
|
25
|
-
const info = await mock.listen(0);
|
|
26
|
-
|
|
27
|
-
const res = await fetch(`http://127.0.0.1:${info.port}/headers`, {
|
|
28
|
-
headers: { authorization: "Bearer secret" },
|
|
29
|
-
});
|
|
30
|
-
const body = await res.json();
|
|
31
|
-
expect(body.auth).toBe("Bearer secret");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("handles concurrent requests", async () => {
|
|
35
|
-
mock = schmock();
|
|
36
|
-
mock("GET /slow", () => ({ value: "done" }));
|
|
37
|
-
const info = await mock.listen(0);
|
|
38
|
-
|
|
39
|
-
const results = await Promise.all(
|
|
40
|
-
Array.from({ length: 10 }, () =>
|
|
41
|
-
fetch(`http://127.0.0.1:${info.port}/slow`).then((r) => r.json()),
|
|
42
|
-
),
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
for (const body of results) {
|
|
46
|
-
expect(body.value).toBe("done");
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("parses text body when content-type is not JSON", async () => {
|
|
51
|
-
mock = schmock();
|
|
52
|
-
mock("POST /text", ({ body: reqBody }) => ({ received: reqBody }));
|
|
53
|
-
const info = await mock.listen(0);
|
|
54
|
-
|
|
55
|
-
const res = await fetch(`http://127.0.0.1:${info.port}/text`, {
|
|
56
|
-
method: "POST",
|
|
57
|
-
headers: { "content-type": "text/plain" },
|
|
58
|
-
body: "hello world",
|
|
59
|
-
});
|
|
60
|
-
const resBody = await res.json();
|
|
61
|
-
expect(resBody.received).toBe("hello world");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("returns 204 with no body for undefined response", async () => {
|
|
65
|
-
mock = schmock();
|
|
66
|
-
mock("DELETE /item", () => undefined);
|
|
67
|
-
const info = await mock.listen(0);
|
|
68
|
-
|
|
69
|
-
const res = await fetch(`http://127.0.0.1:${info.port}/item`, {
|
|
70
|
-
method: "DELETE",
|
|
71
|
-
});
|
|
72
|
-
expect(res.status).toBe(204);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("writes binary response bodies without JSON serialization", async () => {
|
|
76
|
-
mock = schmock();
|
|
77
|
-
mock("GET /binary", new Uint8Array([0, 1, 255]));
|
|
78
|
-
const info = await mock.listen(0);
|
|
79
|
-
|
|
80
|
-
const response = await fetch(`http://127.0.0.1:${info.port}/binary`);
|
|
81
|
-
expect(response.headers.get("content-type")).toBe(
|
|
82
|
-
"application/octet-stream",
|
|
83
|
-
);
|
|
84
|
-
expect(new Uint8Array(await response.arrayBuffer())).toEqual(
|
|
85
|
-
new Uint8Array([0, 1, 255]),
|
|
86
|
-
);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("writes dynamic and tuple binary responses with binary content type", async () => {
|
|
90
|
-
mock = schmock();
|
|
91
|
-
const dynamicBytes = new Uint8Array([1, 2, 3]);
|
|
92
|
-
const tupleBytes = new Uint8Array([4, 5, 6]);
|
|
93
|
-
mock("GET /dynamic-binary", () => dynamicBytes.buffer);
|
|
94
|
-
mock(
|
|
95
|
-
"GET /tuple-binary",
|
|
96
|
-
() => [206, new DataView(tupleBytes.buffer)] satisfies [number, unknown],
|
|
97
|
-
);
|
|
98
|
-
const info = await mock.listen(0);
|
|
99
|
-
|
|
100
|
-
const dynamicResponse = await fetch(
|
|
101
|
-
`http://127.0.0.1:${info.port}/dynamic-binary`,
|
|
102
|
-
);
|
|
103
|
-
expect(dynamicResponse.headers.get("content-type")).toBe(
|
|
104
|
-
"application/octet-stream",
|
|
105
|
-
);
|
|
106
|
-
expect(new Uint8Array(await dynamicResponse.arrayBuffer())).toEqual(
|
|
107
|
-
dynamicBytes,
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const tupleResponse = await fetch(
|
|
111
|
-
`http://127.0.0.1:${info.port}/tuple-binary`,
|
|
112
|
-
);
|
|
113
|
-
expect(tupleResponse.status).toBe(206);
|
|
114
|
-
expect(tupleResponse.headers.get("content-type")).toBe(
|
|
115
|
-
"application/octet-stream",
|
|
116
|
-
);
|
|
117
|
-
expect(new Uint8Array(await tupleResponse.arrayBuffer())).toEqual(
|
|
118
|
-
tupleBytes,
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("reset stops the server", async () => {
|
|
123
|
-
mock = schmock();
|
|
124
|
-
mock("GET /test", { ok: true });
|
|
125
|
-
const info = await mock.listen(0);
|
|
126
|
-
|
|
127
|
-
const res = await fetch(`http://127.0.0.1:${info.port}/test`);
|
|
128
|
-
expect(res.status).toBe(200);
|
|
129
|
-
|
|
130
|
-
mock.reset();
|
|
131
|
-
|
|
132
|
-
await expect(
|
|
133
|
-
fetch(`http://127.0.0.1:${info.port}/test`),
|
|
134
|
-
).rejects.toBeDefined();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("handles route params in server mode", async () => {
|
|
138
|
-
mock = schmock();
|
|
139
|
-
mock("GET /users/:id", ({ params }) => ({ id: params.id }));
|
|
140
|
-
const info = await mock.listen(0);
|
|
141
|
-
|
|
142
|
-
const res = await fetch(`http://127.0.0.1:${info.port}/users/42`);
|
|
143
|
-
const body = await res.json();
|
|
144
|
-
expect(body.id).toBe("42");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("double listen throws", async () => {
|
|
148
|
-
mock = schmock();
|
|
149
|
-
mock("GET /test", { ok: true });
|
|
150
|
-
await mock.listen(0);
|
|
151
|
-
expect(() => mock.listen(0)).toThrow("Server is already running");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("can retry listening after an occupied-port failure", async () => {
|
|
155
|
-
const occupiedServer = createServer();
|
|
156
|
-
await new Promise<void>((resolve, reject) => {
|
|
157
|
-
occupiedServer.once("error", reject);
|
|
158
|
-
occupiedServer.listen(0, "127.0.0.1", resolve);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
const address = occupiedServer.address();
|
|
163
|
-
if (address === null || typeof address === "string") {
|
|
164
|
-
throw new Error("Expected an address with a numeric port");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
mock = schmock();
|
|
168
|
-
await expect(mock.listen(address.port)).rejects.toMatchObject({
|
|
169
|
-
code: "EADDRINUSE",
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const retry = await mock.listen(0);
|
|
173
|
-
expect(retry.port).toBeGreaterThan(0);
|
|
174
|
-
} finally {
|
|
175
|
-
await new Promise<void>((resolve) =>
|
|
176
|
-
occupiedServer.close(() => resolve()),
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("close is idempotent", async () => {
|
|
182
|
-
mock = schmock();
|
|
183
|
-
mock("GET /test", { ok: true });
|
|
184
|
-
await mock.listen(0);
|
|
185
|
-
mock.close();
|
|
186
|
-
expect(() => mock.close()).not.toThrow();
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("generator that throws synchronously returns 500", async () => {
|
|
190
|
-
mock = schmock();
|
|
191
|
-
mock("GET /boom", () => {
|
|
192
|
-
throw new Error("sync kaboom");
|
|
193
|
-
});
|
|
194
|
-
const info = await mock.listen(0);
|
|
195
|
-
|
|
196
|
-
const res = await fetch(`http://127.0.0.1:${info.port}/boom`);
|
|
197
|
-
expect(res.status).toBe(500);
|
|
198
|
-
const body = await res.json();
|
|
199
|
-
expect(body.error).toBe("sync kaboom");
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("generator that returns rejected Promise returns 500", async () => {
|
|
203
|
-
mock = schmock();
|
|
204
|
-
mock("GET /reject", async () => {
|
|
205
|
-
throw new Error("async rejection");
|
|
206
|
-
});
|
|
207
|
-
const info = await mock.listen(0);
|
|
208
|
-
|
|
209
|
-
const res = await fetch(`http://127.0.0.1:${info.port}/reject`);
|
|
210
|
-
expect(res.status).toBe(500);
|
|
211
|
-
const body = await res.json();
|
|
212
|
-
expect(body.error).toBe("async rejection");
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("close terminates keep-alive connections immediately", async () => {
|
|
216
|
-
mock = schmock();
|
|
217
|
-
mock("GET /test", { ok: true });
|
|
218
|
-
const info = await mock.listen(0);
|
|
219
|
-
|
|
220
|
-
// Make a keep-alive request to establish a persistent connection
|
|
221
|
-
const res = await fetch(`http://127.0.0.1:${info.port}/test`, {
|
|
222
|
-
headers: { connection: "keep-alive" },
|
|
223
|
-
});
|
|
224
|
-
expect(res.status).toBe(200);
|
|
225
|
-
|
|
226
|
-
// Close the server — should terminate all connections
|
|
227
|
-
mock.close();
|
|
228
|
-
|
|
229
|
-
// Subsequent request should fail immediately (not hang on keep-alive)
|
|
230
|
-
await expect(
|
|
231
|
-
fetch(`http://127.0.0.1:${info.port}/test`),
|
|
232
|
-
).rejects.toBeDefined();
|
|
233
|
-
});
|
|
234
|
-
});
|