context-router 0.0.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 +7 -0
- package/README.md +7 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/router.d.ts +21 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +71 -0
- package/dist/router.js.map +1 -0
- package/dist/router.test.d.ts +2 -0
- package/dist/router.test.d.ts.map +1 -0
- package/dist/router.test.js +669 -0
- package/dist/router.test.js.map +1 -0
- package/package.json +40 -0
- package/src/router.test.ts +880 -0
- package/src/router.ts +102 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { Router, type Handler, type HandlerParams } from "./router";
|
|
3
|
+
|
|
4
|
+
describe("Router", () => {
|
|
5
|
+
describe("basic routing", () => {
|
|
6
|
+
let router: Router;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
router = new Router();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should route GET requests to matching handler", async () => {
|
|
13
|
+
const handler: Handler = () => new Response("Hello");
|
|
14
|
+
router.add("GET", "/hello", handler);
|
|
15
|
+
|
|
16
|
+
const request = new Request("http://localhost/hello", { method: "GET" });
|
|
17
|
+
const response = await router.match(request, {});
|
|
18
|
+
|
|
19
|
+
expect(response.status).toBe(200);
|
|
20
|
+
expect(await response.text()).toBe("Hello");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should route POST requests to matching handler", async () => {
|
|
24
|
+
const handler: Handler = () => new Response("Created", { status: 201 });
|
|
25
|
+
router.add("POST", "/items", handler);
|
|
26
|
+
|
|
27
|
+
const request = new Request("http://localhost/items", { method: "POST" });
|
|
28
|
+
const response = await router.match(request, {});
|
|
29
|
+
|
|
30
|
+
expect(response.status).toBe(201);
|
|
31
|
+
expect(await response.text()).toBe("Created");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should route DELETE requests to matching handler", async () => {
|
|
35
|
+
const handler: Handler = () => new Response(null, { status: 204 });
|
|
36
|
+
router.add("DELETE", "/items/:id", handler);
|
|
37
|
+
|
|
38
|
+
const request = new Request("http://localhost/items/123", {
|
|
39
|
+
method: "DELETE",
|
|
40
|
+
});
|
|
41
|
+
const response = await router.match(request, {});
|
|
42
|
+
|
|
43
|
+
expect(response.status).toBe(204);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should route PATCH requests to matching handler", async () => {
|
|
47
|
+
const handler: Handler = () => new Response("Updated");
|
|
48
|
+
router.add("PATCH", "/items/:id", handler);
|
|
49
|
+
|
|
50
|
+
const request = new Request("http://localhost/items/123", {
|
|
51
|
+
method: "PATCH",
|
|
52
|
+
});
|
|
53
|
+
const response = await router.match(request, {});
|
|
54
|
+
|
|
55
|
+
expect(response.status).toBe(200);
|
|
56
|
+
expect(await response.text()).toBe("Updated");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should return 404 for unmatched routes", async () => {
|
|
60
|
+
const request = new Request("http://localhost/unknown", {
|
|
61
|
+
method: "GET",
|
|
62
|
+
});
|
|
63
|
+
const response = await router.match(request, {});
|
|
64
|
+
|
|
65
|
+
expect(response.status).toBe(404);
|
|
66
|
+
expect(await response.text()).toBe("Not Found");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should return 404 for unmatched HTTP methods", async () => {
|
|
70
|
+
router.add("GET", "/hello", () => new Response("Hello"));
|
|
71
|
+
|
|
72
|
+
const request = new Request("http://localhost/hello", { method: "POST" });
|
|
73
|
+
const response = await router.match(request, {});
|
|
74
|
+
|
|
75
|
+
expect(response.status).toBe(404);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("URL patterns", () => {
|
|
80
|
+
let router: Router;
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
router = new Router();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should extract URL parameters", async () => {
|
|
87
|
+
let capturedParams: Record<string, string | undefined> = {};
|
|
88
|
+
const handler: Handler = (params) => {
|
|
89
|
+
capturedParams = params.urlParams;
|
|
90
|
+
return new Response("OK");
|
|
91
|
+
};
|
|
92
|
+
router.add("GET", "/users/:id", handler);
|
|
93
|
+
|
|
94
|
+
const request = new Request("http://localhost/users/42", {
|
|
95
|
+
method: "GET",
|
|
96
|
+
});
|
|
97
|
+
await router.match(request, {});
|
|
98
|
+
|
|
99
|
+
expect(capturedParams["id"]).toBe("42");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should extract multiple URL parameters", async () => {
|
|
103
|
+
let capturedParams: Record<string, string | undefined> = {};
|
|
104
|
+
const handler: Handler = (params) => {
|
|
105
|
+
capturedParams = params.urlParams;
|
|
106
|
+
return new Response("OK");
|
|
107
|
+
};
|
|
108
|
+
router.add("GET", "/posts/:postId/comments/:commentId", handler);
|
|
109
|
+
|
|
110
|
+
const request = new Request("http://localhost/posts/123/comments/456", {
|
|
111
|
+
method: "GET",
|
|
112
|
+
});
|
|
113
|
+
await router.match(request, {});
|
|
114
|
+
|
|
115
|
+
expect(capturedParams["postId"]).toBe("123");
|
|
116
|
+
expect(capturedParams["commentId"]).toBe("456");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should handle wildcard patterns", async () => {
|
|
120
|
+
let capturedParams: Record<string, string | undefined> = {};
|
|
121
|
+
const handler: Handler = (params) => {
|
|
122
|
+
capturedParams = params.urlParams;
|
|
123
|
+
return new Response("OK");
|
|
124
|
+
};
|
|
125
|
+
router.add("GET", "/files/*", handler);
|
|
126
|
+
|
|
127
|
+
const request = new Request("http://localhost/files/anything.txt", {
|
|
128
|
+
method: "GET",
|
|
129
|
+
});
|
|
130
|
+
await router.match(request, {});
|
|
131
|
+
|
|
132
|
+
expect(capturedParams["0"]).toBe("anything.txt");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("handler parameters", () => {
|
|
137
|
+
let router: Router;
|
|
138
|
+
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
router = new Router();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should pass request body to handler", async () => {
|
|
144
|
+
let capturedBody = "";
|
|
145
|
+
const handler: Handler = (params) => {
|
|
146
|
+
capturedBody = params.body;
|
|
147
|
+
return new Response("OK");
|
|
148
|
+
};
|
|
149
|
+
router.add("POST", "/data", handler);
|
|
150
|
+
|
|
151
|
+
const request = new Request("http://localhost/data", {
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: "test body",
|
|
154
|
+
});
|
|
155
|
+
await router.match(request, {});
|
|
156
|
+
|
|
157
|
+
expect(capturedBody).toBe("test body");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should pass search params to handler", async () => {
|
|
161
|
+
let capturedSearchParams: URLSearchParams = new URLSearchParams();
|
|
162
|
+
const handler: Handler = (params) => {
|
|
163
|
+
capturedSearchParams = params.searchParams;
|
|
164
|
+
return new Response("OK");
|
|
165
|
+
};
|
|
166
|
+
router.add("GET", "/search", handler);
|
|
167
|
+
|
|
168
|
+
const request = new Request("http://localhost/search?q=test&page=2", {
|
|
169
|
+
method: "GET",
|
|
170
|
+
});
|
|
171
|
+
await router.match(request, {});
|
|
172
|
+
|
|
173
|
+
expect(capturedSearchParams.get("q")).toBe("test");
|
|
174
|
+
expect(capturedSearchParams.get("page")).toBe("2");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should pass empty string for body when no body present", async () => {
|
|
178
|
+
let capturedBody: string = "";
|
|
179
|
+
const handler: Handler = (params) => {
|
|
180
|
+
capturedBody = params.body;
|
|
181
|
+
return new Response("OK");
|
|
182
|
+
};
|
|
183
|
+
router.add("GET", "/test", handler);
|
|
184
|
+
|
|
185
|
+
const request = new Request("http://localhost/test", { method: "GET" });
|
|
186
|
+
await router.match(request, {});
|
|
187
|
+
|
|
188
|
+
expect(capturedBody).toBe("");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("context passing", () => {
|
|
193
|
+
it("should pass context object to handler", async () => {
|
|
194
|
+
type MyContext = {
|
|
195
|
+
userId: string;
|
|
196
|
+
database: string;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let capturedContext: Partial<MyContext> = {};
|
|
200
|
+
const router = new Router<MyContext>();
|
|
201
|
+
const handler: Handler<MyContext> = (params) => {
|
|
202
|
+
capturedContext = { userId: params.userId, database: params.database };
|
|
203
|
+
return new Response("OK");
|
|
204
|
+
};
|
|
205
|
+
router.add("GET", "/test", handler);
|
|
206
|
+
|
|
207
|
+
const request = new Request("http://localhost/test", { method: "GET" });
|
|
208
|
+
await router.match(request, { userId: "user123", database: "mydb" });
|
|
209
|
+
|
|
210
|
+
expect(capturedContext.userId).toBe("user123");
|
|
211
|
+
expect(capturedContext.database).toBe("mydb");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should work with empty context", async () => {
|
|
215
|
+
const router = new Router();
|
|
216
|
+
const handler: Handler = (params) => {
|
|
217
|
+
return new Response(JSON.stringify(params.urlParams));
|
|
218
|
+
};
|
|
219
|
+
router.add("GET", "/test", handler);
|
|
220
|
+
|
|
221
|
+
const request = new Request("http://localhost/test", { method: "GET" });
|
|
222
|
+
const response = await router.match(request, {});
|
|
223
|
+
|
|
224
|
+
expect(response.status).toBe(200);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should spread all context properties into handler params", async () => {
|
|
228
|
+
type CustomContext = {
|
|
229
|
+
user: { id: string; name: string };
|
|
230
|
+
config: { debug: boolean };
|
|
231
|
+
apiKey: string;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
let capturedParams: Partial<HandlerParams<CustomContext>> = {};
|
|
235
|
+
const router = new Router<CustomContext>();
|
|
236
|
+
const handler: Handler<CustomContext> = (params) => {
|
|
237
|
+
capturedParams = params;
|
|
238
|
+
return new Response("OK");
|
|
239
|
+
};
|
|
240
|
+
router.add("GET", "/test", handler);
|
|
241
|
+
|
|
242
|
+
const context: CustomContext = {
|
|
243
|
+
user: { id: "123", name: "Test" },
|
|
244
|
+
config: { debug: true },
|
|
245
|
+
apiKey: "secret",
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const request = new Request("http://localhost/test", { method: "GET" });
|
|
249
|
+
await router.match(request, context);
|
|
250
|
+
|
|
251
|
+
expect(capturedParams.user?.id).toBe("123");
|
|
252
|
+
expect(capturedParams.user?.name).toBe("Test");
|
|
253
|
+
expect(capturedParams.config?.debug).toBe(true);
|
|
254
|
+
expect(capturedParams.apiKey).toBe("secret");
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("route chaining", () => {
|
|
259
|
+
it("should allow method chaining with add", async () => {
|
|
260
|
+
const router = new Router();
|
|
261
|
+
const handler: Handler = () => new Response("OK");
|
|
262
|
+
|
|
263
|
+
const result = router
|
|
264
|
+
.add("GET", "/one", handler)
|
|
265
|
+
.add("POST", "/two", handler)
|
|
266
|
+
.add("DELETE", "/three", handler);
|
|
267
|
+
|
|
268
|
+
expect(result).toBe(router);
|
|
269
|
+
|
|
270
|
+
// Verify routes were actually added by testing they work
|
|
271
|
+
const getRequest = new Request("http://localhost/one", { method: "GET" });
|
|
272
|
+
const getResponse = await router.match(getRequest, {});
|
|
273
|
+
expect(getResponse.status).toBe(200);
|
|
274
|
+
|
|
275
|
+
const postRequest = new Request("http://localhost/two", {
|
|
276
|
+
method: "POST",
|
|
277
|
+
});
|
|
278
|
+
const postResponse = await router.match(postRequest, {});
|
|
279
|
+
expect(postResponse.status).toBe(200);
|
|
280
|
+
|
|
281
|
+
const deleteRequest = new Request("http://localhost/three", {
|
|
282
|
+
method: "DELETE",
|
|
283
|
+
});
|
|
284
|
+
const deleteResponse = await router.match(deleteRequest, {});
|
|
285
|
+
expect(deleteResponse.status).toBe(200);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe("route priority", () => {
|
|
290
|
+
let router: Router;
|
|
291
|
+
|
|
292
|
+
beforeEach(() => {
|
|
293
|
+
router = new Router();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("should match routes in order they were added", async () => {
|
|
297
|
+
let matchedRoute = "";
|
|
298
|
+
|
|
299
|
+
router.add("GET", "/test/*", () => {
|
|
300
|
+
matchedRoute = "first";
|
|
301
|
+
return new Response("First");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
router.add("GET", "/test/:id", () => {
|
|
305
|
+
matchedRoute = "second";
|
|
306
|
+
return new Response("Second");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const request = new Request("http://localhost/test/123", {
|
|
310
|
+
method: "GET",
|
|
311
|
+
});
|
|
312
|
+
await router.match(request, {});
|
|
313
|
+
|
|
314
|
+
expect(matchedRoute).toBe("first");
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe("async handlers", () => {
|
|
319
|
+
it("should handle async handler functions", async () => {
|
|
320
|
+
const router = new Router();
|
|
321
|
+
const handler: Handler = async () => {
|
|
322
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
323
|
+
return new Response("Async response");
|
|
324
|
+
};
|
|
325
|
+
router.add("GET", "/async", handler);
|
|
326
|
+
|
|
327
|
+
const request = new Request("http://localhost/async", { method: "GET" });
|
|
328
|
+
const response = await router.match(request, {});
|
|
329
|
+
|
|
330
|
+
expect(response.status).toBe(200);
|
|
331
|
+
expect(await response.text()).toBe("Async response");
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe("multiple routes", () => {
|
|
336
|
+
let router: Router;
|
|
337
|
+
|
|
338
|
+
beforeEach(() => {
|
|
339
|
+
router = new Router();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("should handle multiple routes for same method", async () => {
|
|
343
|
+
router.add("GET", "/users", () => new Response("Users list"));
|
|
344
|
+
router.add("GET", "/posts", () => new Response("Posts list"));
|
|
345
|
+
router.add("GET", "/comments", () => new Response("Comments list"));
|
|
346
|
+
|
|
347
|
+
const request1 = new Request("http://localhost/users", { method: "GET" });
|
|
348
|
+
const response1 = await router.match(request1, {});
|
|
349
|
+
expect(await response1.text()).toBe("Users list");
|
|
350
|
+
|
|
351
|
+
const request2 = new Request("http://localhost/posts", { method: "GET" });
|
|
352
|
+
const response2 = await router.match(request2, {});
|
|
353
|
+
expect(await response2.text()).toBe("Posts list");
|
|
354
|
+
|
|
355
|
+
const request3 = new Request("http://localhost/comments", {
|
|
356
|
+
method: "GET",
|
|
357
|
+
});
|
|
358
|
+
const response3 = await router.match(request3, {});
|
|
359
|
+
expect(await response3.text()).toBe("Comments list");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("should handle same path with different methods", async () => {
|
|
363
|
+
router.add("GET", "/resource", () => new Response("GET"));
|
|
364
|
+
router.add("POST", "/resource", () => new Response("POST"));
|
|
365
|
+
router.add("DELETE", "/resource", () => new Response("DELETE"));
|
|
366
|
+
router.add("PATCH", "/resource", () => new Response("PATCH"));
|
|
367
|
+
|
|
368
|
+
const getRequest = new Request("http://localhost/resource", {
|
|
369
|
+
method: "GET",
|
|
370
|
+
});
|
|
371
|
+
const getResponse = await router.match(getRequest, {});
|
|
372
|
+
expect(await getResponse.text()).toBe("GET");
|
|
373
|
+
|
|
374
|
+
const postRequest = new Request("http://localhost/resource", {
|
|
375
|
+
method: "POST",
|
|
376
|
+
});
|
|
377
|
+
const postResponse = await router.match(postRequest, {});
|
|
378
|
+
expect(await postResponse.text()).toBe("POST");
|
|
379
|
+
|
|
380
|
+
const deleteRequest = new Request("http://localhost/resource", {
|
|
381
|
+
method: "DELETE",
|
|
382
|
+
});
|
|
383
|
+
const deleteResponse = await router.match(deleteRequest, {});
|
|
384
|
+
expect(await deleteResponse.text()).toBe("DELETE");
|
|
385
|
+
|
|
386
|
+
const patchRequest = new Request("http://localhost/resource", {
|
|
387
|
+
method: "PATCH",
|
|
388
|
+
});
|
|
389
|
+
const patchResponse = await router.match(patchRequest, {});
|
|
390
|
+
expect(await patchResponse.text()).toBe("PATCH");
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe("edge cases", () => {
|
|
395
|
+
let router: Router;
|
|
396
|
+
|
|
397
|
+
beforeEach(() => {
|
|
398
|
+
router = new Router();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("should handle routes with query parameters", async () => {
|
|
402
|
+
const handler: Handler = (params) => {
|
|
403
|
+
const q = params.searchParams.get("q");
|
|
404
|
+
return new Response(`Search: ${q}`);
|
|
405
|
+
};
|
|
406
|
+
router.add("GET", "/search", handler);
|
|
407
|
+
|
|
408
|
+
const request = new Request(
|
|
409
|
+
"http://localhost/search?q=hello+world&filter=new",
|
|
410
|
+
{
|
|
411
|
+
method: "GET",
|
|
412
|
+
},
|
|
413
|
+
);
|
|
414
|
+
const response = await router.match(request, {});
|
|
415
|
+
|
|
416
|
+
expect(await response.text()).toBe("Search: hello world");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("should handle routes with hash fragments", async () => {
|
|
420
|
+
const handler: Handler = () => new Response("OK");
|
|
421
|
+
router.add("GET", "/page", handler);
|
|
422
|
+
|
|
423
|
+
const request = new Request("http://localhost/page#section", {
|
|
424
|
+
method: "GET",
|
|
425
|
+
});
|
|
426
|
+
const response = await router.match(request, {});
|
|
427
|
+
|
|
428
|
+
expect(response.status).toBe(200);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("should handle empty path", async () => {
|
|
432
|
+
const handler: Handler = () => new Response("Home");
|
|
433
|
+
router.add("GET", "/", handler);
|
|
434
|
+
|
|
435
|
+
const request = new Request("http://localhost/", { method: "GET" });
|
|
436
|
+
const response = await router.match(request, {});
|
|
437
|
+
|
|
438
|
+
expect(await response.text()).toBe("Home");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("should handle JSON body", async () => {
|
|
442
|
+
let capturedBody = "";
|
|
443
|
+
const handler: Handler = (params) => {
|
|
444
|
+
capturedBody = params.body;
|
|
445
|
+
const data = JSON.parse(params.body);
|
|
446
|
+
return new Response(JSON.stringify({ received: data }));
|
|
447
|
+
};
|
|
448
|
+
router.add("POST", "/api/data", handler);
|
|
449
|
+
|
|
450
|
+
const payload = { name: "test", value: 123 };
|
|
451
|
+
const request = new Request("http://localhost/api/data", {
|
|
452
|
+
method: "POST",
|
|
453
|
+
headers: { "Content-Type": "application/json" },
|
|
454
|
+
body: JSON.stringify(payload),
|
|
455
|
+
});
|
|
456
|
+
await router.match(request, {});
|
|
457
|
+
|
|
458
|
+
expect(JSON.parse(capturedBody)).toEqual(payload);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe("HTTP helper methods", () => {
|
|
463
|
+
let router: Router;
|
|
464
|
+
|
|
465
|
+
beforeEach(() => {
|
|
466
|
+
router = new Router();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("should register GET route using get() helper", async () => {
|
|
470
|
+
const handler: Handler = () => new Response("GET response");
|
|
471
|
+
router.get("/test", handler);
|
|
472
|
+
|
|
473
|
+
const request = new Request("http://localhost/test", { method: "GET" });
|
|
474
|
+
const response = await router.match(request, {});
|
|
475
|
+
|
|
476
|
+
expect(response.status).toBe(200);
|
|
477
|
+
expect(await response.text()).toBe("GET response");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("should register POST route using post() helper", async () => {
|
|
481
|
+
const handler: Handler = () => new Response("POST response");
|
|
482
|
+
router.post("/test", handler);
|
|
483
|
+
|
|
484
|
+
const request = new Request("http://localhost/test", { method: "POST" });
|
|
485
|
+
const response = await router.match(request, {});
|
|
486
|
+
|
|
487
|
+
expect(response.status).toBe(200);
|
|
488
|
+
expect(await response.text()).toBe("POST response");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("should register PUT route using put() helper", async () => {
|
|
492
|
+
const handler: Handler = () => new Response("PUT response");
|
|
493
|
+
router.put("/test", handler);
|
|
494
|
+
|
|
495
|
+
const request = new Request("http://localhost/test", { method: "PUT" });
|
|
496
|
+
const response = await router.match(request, {});
|
|
497
|
+
|
|
498
|
+
expect(response.status).toBe(200);
|
|
499
|
+
expect(await response.text()).toBe("PUT response");
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("should register PATCH route using patch() helper", async () => {
|
|
503
|
+
const handler: Handler = () => new Response("PATCH response");
|
|
504
|
+
router.patch("/test", handler);
|
|
505
|
+
|
|
506
|
+
const request = new Request("http://localhost/test", { method: "PATCH" });
|
|
507
|
+
const response = await router.match(request, {});
|
|
508
|
+
|
|
509
|
+
expect(response.status).toBe(200);
|
|
510
|
+
expect(await response.text()).toBe("PATCH response");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("should register DELETE route using delete() helper", async () => {
|
|
514
|
+
const handler: Handler = () => new Response("DELETE response");
|
|
515
|
+
router.delete("/test", handler);
|
|
516
|
+
|
|
517
|
+
const request = new Request("http://localhost/test", {
|
|
518
|
+
method: "DELETE",
|
|
519
|
+
});
|
|
520
|
+
const response = await router.match(request, {});
|
|
521
|
+
|
|
522
|
+
expect(response.status).toBe(200);
|
|
523
|
+
expect(await response.text()).toBe("DELETE response");
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("should support method chaining with helper methods", async () => {
|
|
527
|
+
const handler: Handler = (params) => new Response(params.urlParams.id);
|
|
528
|
+
|
|
529
|
+
const result = router
|
|
530
|
+
.get("/users/:id", handler)
|
|
531
|
+
.post("/users", handler)
|
|
532
|
+
.put("/users/:id", handler)
|
|
533
|
+
.patch("/users/:id", handler)
|
|
534
|
+
.delete("/users/:id", handler);
|
|
535
|
+
|
|
536
|
+
expect(result).toBe(router);
|
|
537
|
+
|
|
538
|
+
// Verify all routes work
|
|
539
|
+
const getRequest = new Request("http://localhost/users/1", {
|
|
540
|
+
method: "GET",
|
|
541
|
+
});
|
|
542
|
+
const getResponse = await router.match(getRequest, {});
|
|
543
|
+
expect(await getResponse.text()).toBe("1");
|
|
544
|
+
|
|
545
|
+
const postRequest = new Request("http://localhost/users", {
|
|
546
|
+
method: "POST",
|
|
547
|
+
});
|
|
548
|
+
const postResponse = await router.match(postRequest, {});
|
|
549
|
+
expect(postResponse.status).toBe(200);
|
|
550
|
+
|
|
551
|
+
const putRequest = new Request("http://localhost/users/2", {
|
|
552
|
+
method: "PUT",
|
|
553
|
+
});
|
|
554
|
+
const putResponse = await router.match(putRequest, {});
|
|
555
|
+
expect(await putResponse.text()).toBe("2");
|
|
556
|
+
|
|
557
|
+
const patchRequest = new Request("http://localhost/users/3", {
|
|
558
|
+
method: "PATCH",
|
|
559
|
+
});
|
|
560
|
+
const patchResponse = await router.match(patchRequest, {});
|
|
561
|
+
expect(await patchResponse.text()).toBe("3");
|
|
562
|
+
|
|
563
|
+
const deleteRequest = new Request("http://localhost/users/4", {
|
|
564
|
+
method: "DELETE",
|
|
565
|
+
});
|
|
566
|
+
const deleteResponse = await router.match(deleteRequest, {});
|
|
567
|
+
expect(await deleteResponse.text()).toBe("4");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("should work with URL parameters in helper methods", async () => {
|
|
571
|
+
let capturedParams: Record<string, string | undefined> = {};
|
|
572
|
+
const handler: Handler = (params) => {
|
|
573
|
+
capturedParams = params.urlParams;
|
|
574
|
+
return new Response("OK");
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
router.get("/posts/:postId", handler);
|
|
578
|
+
|
|
579
|
+
const request = new Request("http://localhost/posts/123", {
|
|
580
|
+
method: "GET",
|
|
581
|
+
});
|
|
582
|
+
await router.match(request, {});
|
|
583
|
+
|
|
584
|
+
expect(capturedParams["postId"]).toBe("123");
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it("should work with context in helper methods", async () => {
|
|
588
|
+
type MyContext = { userId: string };
|
|
589
|
+
const contextRouter = new Router<MyContext>();
|
|
590
|
+
|
|
591
|
+
let capturedUserId = "";
|
|
592
|
+
const handler: Handler<MyContext> = (params) => {
|
|
593
|
+
capturedUserId = params.userId;
|
|
594
|
+
return new Response("OK");
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
contextRouter.post("/items", handler);
|
|
598
|
+
|
|
599
|
+
const request = new Request("http://localhost/items", { method: "POST" });
|
|
600
|
+
await contextRouter.match(request, { userId: "user123" });
|
|
601
|
+
|
|
602
|
+
expect(capturedUserId).toBe("user123");
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it("should work with async handlers in helper methods", async () => {
|
|
606
|
+
const handler: Handler = async () => {
|
|
607
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
608
|
+
return new Response("Async GET");
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
router.get("/async", handler);
|
|
612
|
+
|
|
613
|
+
const request = new Request("http://localhost/async", { method: "GET" });
|
|
614
|
+
const response = await router.match(request, {});
|
|
615
|
+
|
|
616
|
+
expect(await response.text()).toBe("Async GET");
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it("should mix helper methods with add() method", async () => {
|
|
620
|
+
router.get("/route1", () => new Response("GET"));
|
|
621
|
+
router.add("POST", "/route2", () => new Response("POST"));
|
|
622
|
+
router.put("/route3", () => new Response("PUT"));
|
|
623
|
+
router.add("PATCH", "/route4", () => new Response("PATCH"));
|
|
624
|
+
router.delete("/route5", () => new Response("DELETE"));
|
|
625
|
+
|
|
626
|
+
const request1 = new Request("http://localhost/route1", {
|
|
627
|
+
method: "GET",
|
|
628
|
+
});
|
|
629
|
+
const response1 = await router.match(request1, {});
|
|
630
|
+
expect(await response1.text()).toBe("GET");
|
|
631
|
+
|
|
632
|
+
const request2 = new Request("http://localhost/route2", {
|
|
633
|
+
method: "POST",
|
|
634
|
+
});
|
|
635
|
+
const response2 = await router.match(request2, {});
|
|
636
|
+
expect(await response2.text()).toBe("POST");
|
|
637
|
+
|
|
638
|
+
const request3 = new Request("http://localhost/route3", {
|
|
639
|
+
method: "PUT",
|
|
640
|
+
});
|
|
641
|
+
const response3 = await router.match(request3, {});
|
|
642
|
+
expect(await response3.text()).toBe("PUT");
|
|
643
|
+
|
|
644
|
+
const request4 = new Request("http://localhost/route4", {
|
|
645
|
+
method: "PATCH",
|
|
646
|
+
});
|
|
647
|
+
const response4 = await router.match(request4, {});
|
|
648
|
+
expect(await response4.text()).toBe("PATCH");
|
|
649
|
+
|
|
650
|
+
const request5 = new Request("http://localhost/route5", {
|
|
651
|
+
method: "DELETE",
|
|
652
|
+
});
|
|
653
|
+
const response5 = await router.match(request5, {});
|
|
654
|
+
expect(await response5.text()).toBe("DELETE");
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
describe("setNotFoundHandler", () => {
|
|
659
|
+
let router: Router;
|
|
660
|
+
|
|
661
|
+
beforeEach(() => {
|
|
662
|
+
router = new Router();
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it("should use custom not found handler for unmatched routes", async () => {
|
|
666
|
+
const notFoundHandler: Handler = () =>
|
|
667
|
+
new Response("Custom Not Found", { status: 404 });
|
|
668
|
+
router.setNotFoundHandler(notFoundHandler);
|
|
669
|
+
|
|
670
|
+
const request = new Request("http://localhost/unknown", {
|
|
671
|
+
method: "GET",
|
|
672
|
+
});
|
|
673
|
+
const response = await router.match(request, {});
|
|
674
|
+
|
|
675
|
+
expect(response.status).toBe(404);
|
|
676
|
+
expect(await response.text()).toBe("Custom Not Found");
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("should use custom not found handler for unmatched HTTP methods", async () => {
|
|
680
|
+
const notFoundHandler: Handler = () =>
|
|
681
|
+
new Response("Method Not Allowed", { status: 405 });
|
|
682
|
+
router.setNotFoundHandler(notFoundHandler);
|
|
683
|
+
router.get("/resource", () => new Response("OK"));
|
|
684
|
+
|
|
685
|
+
const request = new Request("http://localhost/resource", {
|
|
686
|
+
method: "POST",
|
|
687
|
+
});
|
|
688
|
+
const response = await router.match(request, {});
|
|
689
|
+
|
|
690
|
+
expect(response.status).toBe(405);
|
|
691
|
+
expect(await response.text()).toBe("Method Not Allowed");
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it("should use default not found handler when not overridden", async () => {
|
|
695
|
+
const request = new Request("http://localhost/unknown", {
|
|
696
|
+
method: "GET",
|
|
697
|
+
});
|
|
698
|
+
const response = await router.match(request, {});
|
|
699
|
+
|
|
700
|
+
expect(response.status).toBe(404);
|
|
701
|
+
expect(await response.text()).toBe("Not Found");
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("should support method chaining with setNotFoundHandler", async () => {
|
|
705
|
+
const notFoundHandler: Handler = () =>
|
|
706
|
+
new Response("Not Found", { status: 404 });
|
|
707
|
+
const result = router.setNotFoundHandler(notFoundHandler);
|
|
708
|
+
|
|
709
|
+
expect(result).toBe(router);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it("should allow chaining setNotFoundHandler with other methods", async () => {
|
|
713
|
+
router
|
|
714
|
+
.setNotFoundHandler(() => new Response("Custom 404", { status: 404 }))
|
|
715
|
+
.get("/test", () => new Response("OK"))
|
|
716
|
+
.post("/items", () => new Response("Created", { status: 201 }));
|
|
717
|
+
|
|
718
|
+
const matchRequest = new Request("http://localhost/test", {
|
|
719
|
+
method: "GET",
|
|
720
|
+
});
|
|
721
|
+
const matchResponse = await router.match(matchRequest, {});
|
|
722
|
+
expect(await matchResponse.text()).toBe("OK");
|
|
723
|
+
|
|
724
|
+
const notFoundRequest = new Request("http://localhost/unknown", {
|
|
725
|
+
method: "GET",
|
|
726
|
+
});
|
|
727
|
+
const notFoundResponse = await router.match(notFoundRequest, {});
|
|
728
|
+
expect(await notFoundResponse.text()).toBe("Custom 404");
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it("should pass request data to custom not found handler", async () => {
|
|
732
|
+
let capturedBody = "";
|
|
733
|
+
let capturedSearchParams: URLSearchParams = new URLSearchParams();
|
|
734
|
+
|
|
735
|
+
const notFoundHandler: Handler = (params) => {
|
|
736
|
+
capturedBody = params.body;
|
|
737
|
+
capturedSearchParams = params.searchParams;
|
|
738
|
+
return new Response("Not Found", { status: 404 });
|
|
739
|
+
};
|
|
740
|
+
router.setNotFoundHandler(notFoundHandler);
|
|
741
|
+
|
|
742
|
+
const request = new Request("http://localhost/unknown?key=value", {
|
|
743
|
+
method: "POST",
|
|
744
|
+
body: "test data",
|
|
745
|
+
});
|
|
746
|
+
await router.match(request, {});
|
|
747
|
+
|
|
748
|
+
expect(capturedBody).toBe("test data");
|
|
749
|
+
expect(capturedSearchParams.get("key")).toBe("value");
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it("should pass context to custom not found handler", async () => {
|
|
753
|
+
type MyContext = { userId: string; role: string };
|
|
754
|
+
const contextRouter = new Router<MyContext>();
|
|
755
|
+
|
|
756
|
+
let capturedUserId = "";
|
|
757
|
+
let capturedRole = "";
|
|
758
|
+
|
|
759
|
+
const notFoundHandler: Handler<MyContext> = (params) => {
|
|
760
|
+
capturedUserId = params.userId;
|
|
761
|
+
capturedRole = params.role;
|
|
762
|
+
return new Response("Not Found", { status: 404 });
|
|
763
|
+
};
|
|
764
|
+
contextRouter.setNotFoundHandler(notFoundHandler);
|
|
765
|
+
|
|
766
|
+
const request = new Request("http://localhost/unknown", {
|
|
767
|
+
method: "GET",
|
|
768
|
+
});
|
|
769
|
+
await contextRouter.match(request, { userId: "user123", role: "admin" });
|
|
770
|
+
|
|
771
|
+
expect(capturedUserId).toBe("user123");
|
|
772
|
+
expect(capturedRole).toBe("admin");
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it("should support async custom not found handler", async () => {
|
|
776
|
+
const notFoundHandler: Handler = async () => {
|
|
777
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
778
|
+
return new Response("Async Not Found", { status: 404 });
|
|
779
|
+
};
|
|
780
|
+
router.setNotFoundHandler(notFoundHandler);
|
|
781
|
+
|
|
782
|
+
const request = new Request("http://localhost/unknown", {
|
|
783
|
+
method: "GET",
|
|
784
|
+
});
|
|
785
|
+
const response = await router.match(request, {});
|
|
786
|
+
|
|
787
|
+
expect(response.status).toBe(404);
|
|
788
|
+
expect(await response.text()).toBe("Async Not Found");
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it("should allow overriding not found handler multiple times", async () => {
|
|
792
|
+
router.setNotFoundHandler(
|
|
793
|
+
() => new Response("First Handler", { status: 404 }),
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
let request = new Request("http://localhost/unknown", {
|
|
797
|
+
method: "GET",
|
|
798
|
+
});
|
|
799
|
+
let response = await router.match(request, {});
|
|
800
|
+
expect(await response.text()).toBe("First Handler");
|
|
801
|
+
|
|
802
|
+
router.setNotFoundHandler(
|
|
803
|
+
() => new Response("Second Handler", { status: 404 }),
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
request = new Request("http://localhost/unknown", {
|
|
807
|
+
method: "GET",
|
|
808
|
+
});
|
|
809
|
+
response = await router.match(request, {});
|
|
810
|
+
expect(await response.text()).toBe("Second Handler");
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it("should use custom not found handler with different status codes", async () => {
|
|
814
|
+
router.setNotFoundHandler(
|
|
815
|
+
() => new Response("Not Found", { status: 404 }),
|
|
816
|
+
);
|
|
817
|
+
|
|
818
|
+
const request = new Request("http://localhost/unknown", {
|
|
819
|
+
method: "GET",
|
|
820
|
+
});
|
|
821
|
+
const response = await router.match(request, {});
|
|
822
|
+
|
|
823
|
+
expect(response.status).toBe(404);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it("should use custom not found handler with JSON response", async () => {
|
|
827
|
+
const notFoundHandler: Handler = () =>
|
|
828
|
+
new Response(
|
|
829
|
+
JSON.stringify({ error: "Resource not found", code: "NOT_FOUND" }),
|
|
830
|
+
{ status: 404, headers: { "Content-Type": "application/json" } },
|
|
831
|
+
);
|
|
832
|
+
router.setNotFoundHandler(notFoundHandler);
|
|
833
|
+
|
|
834
|
+
const request = new Request("http://localhost/unknown", {
|
|
835
|
+
method: "GET",
|
|
836
|
+
});
|
|
837
|
+
const response = await router.match(request, {});
|
|
838
|
+
|
|
839
|
+
expect(response.status).toBe(404);
|
|
840
|
+
expect(response.headers.get("Content-Type")).toBe("application/json");
|
|
841
|
+
const body = JSON.parse(await response.text());
|
|
842
|
+
expect(body.error).toBe("Resource not found");
|
|
843
|
+
expect(body.code).toBe("NOT_FOUND");
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
it("should use custom not found handler when route path matches but method does not", async () => {
|
|
847
|
+
const notFoundHandler: Handler = () =>
|
|
848
|
+
new Response("Resource exists but method not allowed", { status: 405 });
|
|
849
|
+
router.setNotFoundHandler(notFoundHandler);
|
|
850
|
+
router.get("/resource", () => new Response("GET response"));
|
|
851
|
+
|
|
852
|
+
const request = new Request("http://localhost/resource", {
|
|
853
|
+
method: "DELETE",
|
|
854
|
+
});
|
|
855
|
+
const response = await router.match(request, {});
|
|
856
|
+
|
|
857
|
+
expect(response.status).toBe(405);
|
|
858
|
+
expect(await response.text()).toBe(
|
|
859
|
+
"Resource exists but method not allowed",
|
|
860
|
+
);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it("should work with URL parameters in not found handler", async () => {
|
|
864
|
+
let capturedUrlParams: Record<string, string | undefined> = {};
|
|
865
|
+
|
|
866
|
+
const notFoundHandler: Handler = (params) => {
|
|
867
|
+
capturedUrlParams = params.urlParams;
|
|
868
|
+
return new Response("Not Found", { status: 404 });
|
|
869
|
+
};
|
|
870
|
+
router.setNotFoundHandler(notFoundHandler);
|
|
871
|
+
|
|
872
|
+
const request = new Request("http://localhost/api/v1/resource", {
|
|
873
|
+
method: "GET",
|
|
874
|
+
});
|
|
875
|
+
await router.match(request, {});
|
|
876
|
+
|
|
877
|
+
expect(capturedUrlParams).toEqual({});
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
});
|