@esportsplus/routing 0.7.3 → 0.7.4
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/README.md +10 -14
- package/build/client/index.js +10 -10
- package/build/client/router/index.test.d.ts +1 -0
- package/build/client/router/index.test.js +245 -0
- package/build/client/router/node.test.d.ts +1 -0
- package/build/client/router/node.test.js +214 -0
- package/package.json +6 -2
- package/src/client/router/index.test.ts +392 -0
- package/src/client/router/node.test.ts +363 -0
- package/vitest.config.ts +14 -0
- /package/{test → tests}/dist/test.js +0 -0
- /package/{test → tests}/dist/test.js.map +0 -0
- /package/{test → tests}/index.ts +0 -0
- /package/{test → tests}/vite.config.ts +0 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Router } from './index';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
type Mw = (input: unknown, next: Responder) => string;
|
|
6
|
+
type Responder = (input: unknown) => string;
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
function responder(label: string): Responder {
|
|
10
|
+
return () => label;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function mw(label: string): Mw {
|
|
14
|
+
return (_input, next) => label + ':' + next(_input);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
describe('Router', () => {
|
|
19
|
+
describe('match()', () => {
|
|
20
|
+
it('matches static GET path', () => {
|
|
21
|
+
let router = new Router<string>();
|
|
22
|
+
|
|
23
|
+
router.get({ path: '/home', responder: responder('home') });
|
|
24
|
+
|
|
25
|
+
let result = router.match('GET', '/home');
|
|
26
|
+
|
|
27
|
+
expect(result.route).toBeDefined();
|
|
28
|
+
expect(result.route!.path).toBe('/home');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('matches dynamic path with parameters', () => {
|
|
32
|
+
let router = new Router<string>();
|
|
33
|
+
|
|
34
|
+
router.get({ path: '/users/:id', responder: responder('user') });
|
|
35
|
+
|
|
36
|
+
let result = router.match('GET', '/users/42');
|
|
37
|
+
|
|
38
|
+
expect(result.route).toBeDefined();
|
|
39
|
+
expect(result.parameters).toEqual({ id: '42' });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns empty for unregistered method', () => {
|
|
43
|
+
let router = new Router<string>();
|
|
44
|
+
|
|
45
|
+
router.get({ path: '/home', responder: responder('home') });
|
|
46
|
+
|
|
47
|
+
let result = router.match('POST', '/home');
|
|
48
|
+
|
|
49
|
+
expect(result.route).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns empty for unregistered path', () => {
|
|
53
|
+
let router = new Router<string>();
|
|
54
|
+
|
|
55
|
+
router.get({ path: '/home', responder: responder('home') });
|
|
56
|
+
|
|
57
|
+
let result = router.match('GET', '/missing');
|
|
58
|
+
|
|
59
|
+
expect(result.route).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('normalizes path (adds leading /, strips trailing /)', () => {
|
|
63
|
+
let router = new Router<string>();
|
|
64
|
+
|
|
65
|
+
router.get({ path: '/users', responder: responder('users') });
|
|
66
|
+
|
|
67
|
+
let noLeading = router.match('GET', 'users'),
|
|
68
|
+
trailing = router.match('GET', '/users/');
|
|
69
|
+
|
|
70
|
+
expect(noLeading.route).toBeDefined();
|
|
71
|
+
expect(noLeading.route!.path).toBe('/users');
|
|
72
|
+
expect(trailing.route).toBeDefined();
|
|
73
|
+
expect(trailing.route!.path).toBe('/users');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('matches with subdomain bucketing', () => {
|
|
77
|
+
let router = new Router<string>();
|
|
78
|
+
|
|
79
|
+
router.get({ path: '/api', responder: responder('api'), subdomain: 'api' });
|
|
80
|
+
|
|
81
|
+
let withSubdomain = router.match('GET', '/api', 'api'),
|
|
82
|
+
withoutSubdomain = router.match('GET', '/api');
|
|
83
|
+
|
|
84
|
+
expect(withSubdomain.route).toBeDefined();
|
|
85
|
+
expect(withoutSubdomain.route).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('static match takes priority over tree match', () => {
|
|
89
|
+
let router = new Router<string>(),
|
|
90
|
+
dynamicResponder = responder('dynamic'),
|
|
91
|
+
staticResponder = responder('static');
|
|
92
|
+
|
|
93
|
+
router.get({ path: '/users/:id', responder: dynamicResponder });
|
|
94
|
+
router.get({ path: '/users/all', responder: staticResponder });
|
|
95
|
+
|
|
96
|
+
let result = router.match('GET', '/users/all');
|
|
97
|
+
|
|
98
|
+
expect(result.route).toBeDefined();
|
|
99
|
+
expect(result.route!.path).toBe('/users/all');
|
|
100
|
+
expect(result.route!.middleware).toContain(staticResponder);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
describe('on()', () => {
|
|
106
|
+
it('registers route for single method', () => {
|
|
107
|
+
let router = new Router<string>();
|
|
108
|
+
|
|
109
|
+
router.on(['GET'], { path: '/test', responder: responder('test') });
|
|
110
|
+
|
|
111
|
+
let result = router.match('GET', '/test');
|
|
112
|
+
|
|
113
|
+
expect(result.route).toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('registers route for multiple methods', () => {
|
|
117
|
+
let router = new Router<string>();
|
|
118
|
+
|
|
119
|
+
router.on(['GET', 'POST'], { path: '/test', responder: responder('test') });
|
|
120
|
+
|
|
121
|
+
let getResult = router.match('GET', '/test'),
|
|
122
|
+
postResult = router.match('POST', '/test');
|
|
123
|
+
|
|
124
|
+
expect(getResult.route).toBeDefined();
|
|
125
|
+
expect(postResult.route).toBeDefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('throws on duplicate route name', () => {
|
|
129
|
+
let router = new Router<string>();
|
|
130
|
+
|
|
131
|
+
router.on(['GET'], { name: 'home', path: '/home', responder: responder('home') });
|
|
132
|
+
|
|
133
|
+
expect(() => {
|
|
134
|
+
router.on(['GET'], { name: 'home', path: '/home2', responder: responder('home2') });
|
|
135
|
+
}).toThrow("@esportsplus/routing: 'home' is already in use");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('throws on duplicate static path', () => {
|
|
139
|
+
let router = new Router<string>();
|
|
140
|
+
|
|
141
|
+
router.on(['GET'], { path: '/home', responder: responder('home1') });
|
|
142
|
+
|
|
143
|
+
expect(() => {
|
|
144
|
+
router.on(['GET'], { path: '/home', responder: responder('home2') });
|
|
145
|
+
}).toThrow("@esportsplus/routing: static path '/home' is already in use");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('expands optional parameters', () => {
|
|
149
|
+
let router = new Router<string>();
|
|
150
|
+
|
|
151
|
+
router.on(['GET'], { path: '/users?:id', responder: responder('users') });
|
|
152
|
+
|
|
153
|
+
let base = router.match('GET', '/users'),
|
|
154
|
+
withParam = router.match('GET', '/users/42');
|
|
155
|
+
|
|
156
|
+
expect(base.route).toBeDefined();
|
|
157
|
+
expect(withParam.route).toBeDefined();
|
|
158
|
+
expect(withParam.parameters).toEqual({ id: '42' });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('registers subdomain (lowercased)', () => {
|
|
162
|
+
let router = new Router<string>();
|
|
163
|
+
|
|
164
|
+
router.on(['GET'], { path: '/test', responder: responder('test'), subdomain: 'API' });
|
|
165
|
+
|
|
166
|
+
expect(router.subdomains).toContain('api');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('normalizes www subdomain to empty string', () => {
|
|
170
|
+
let router = new Router<string>();
|
|
171
|
+
|
|
172
|
+
router.on(['GET'], { path: '/test', responder: responder('test'), subdomain: 'www' });
|
|
173
|
+
|
|
174
|
+
let result = router.match('GET', '/test', '');
|
|
175
|
+
|
|
176
|
+
expect(result.route).toBeDefined();
|
|
177
|
+
expect(router.subdomains).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('expands multiple optional parameters', () => {
|
|
181
|
+
let router = new Router<string>();
|
|
182
|
+
|
|
183
|
+
router.on(['GET'], { path: '/items?:a?:b', responder: responder('items') });
|
|
184
|
+
|
|
185
|
+
let base = router.match('GET', '/items'),
|
|
186
|
+
oneParam = router.match('GET', '/items/x'),
|
|
187
|
+
twoParams = router.match('GET', '/items/x/y');
|
|
188
|
+
|
|
189
|
+
expect(base.route).toBeDefined();
|
|
190
|
+
expect(oneParam.route).toBeDefined();
|
|
191
|
+
expect(oneParam.parameters).toEqual({ a: 'x' });
|
|
192
|
+
expect(twoParams.route).toBeDefined();
|
|
193
|
+
expect(twoParams.parameters).toEqual({ a: 'x', b: 'y' });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('stores named route in routes registry', () => {
|
|
197
|
+
let router = new Router<string>();
|
|
198
|
+
|
|
199
|
+
router.on(['GET'], { name: 'dashboard', path: '/dashboard', responder: responder('dash') });
|
|
200
|
+
|
|
201
|
+
expect(router.routes['dashboard']).toBeDefined();
|
|
202
|
+
expect(router.routes['dashboard'].path).toBe('/dashboard');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
describe('uri()', () => {
|
|
208
|
+
it('generates static URI (no params)', () => {
|
|
209
|
+
let router = new Router<string>();
|
|
210
|
+
|
|
211
|
+
router.get({ name: 'home', path: '/home', responder: responder('home') });
|
|
212
|
+
|
|
213
|
+
let uri = (router as any).uri('home');
|
|
214
|
+
|
|
215
|
+
expect(uri).toBe('/home');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('generates URI with required params', () => {
|
|
219
|
+
let router = new Router<string>();
|
|
220
|
+
|
|
221
|
+
router.get({ name: 'user', path: '/users/:id', responder: responder('user') });
|
|
222
|
+
|
|
223
|
+
let uri = (router as any).uri('user', [42]);
|
|
224
|
+
|
|
225
|
+
expect(uri).toBe('/users/42');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('generates URI with optional params present', () => {
|
|
229
|
+
let router = new Router<string>();
|
|
230
|
+
|
|
231
|
+
router.get({ name: 'users', path: '/users/?:id', responder: responder('users') });
|
|
232
|
+
|
|
233
|
+
let uri = (router as any).uri('users', [7]);
|
|
234
|
+
|
|
235
|
+
expect(uri).toBe('/users/7');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('generates URI with optional params absent (stops at first missing)', () => {
|
|
239
|
+
let router = new Router<string>();
|
|
240
|
+
|
|
241
|
+
router.get({ name: 'users', path: '/users/?:id', responder: responder('users') });
|
|
242
|
+
|
|
243
|
+
let uri = (router as any).uri('users');
|
|
244
|
+
|
|
245
|
+
expect(uri).toBe('/users');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('generates URI with wildcard params', () => {
|
|
249
|
+
let router = new Router<string>();
|
|
250
|
+
|
|
251
|
+
router.get({ name: 'files', path: '/files/*:path', responder: responder('files') });
|
|
252
|
+
|
|
253
|
+
let uri = (router as any).uri('files', ['docs', 'readme.txt']);
|
|
254
|
+
|
|
255
|
+
expect(uri).toBe('/files/docs/readme.txt');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('throws for non-existent route name', () => {
|
|
259
|
+
let router = new Router<string>();
|
|
260
|
+
|
|
261
|
+
expect(() => {
|
|
262
|
+
(router as any).uri('missing');
|
|
263
|
+
}).toThrow("@esportsplus/routing: route name 'missing' does not exist or it does not provide a path");
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
describe('group()', () => {
|
|
269
|
+
it('prefixes path to child routes', () => {
|
|
270
|
+
let router = new Router<string>();
|
|
271
|
+
|
|
272
|
+
router.group({ path: '/api' }).routes((r) => {
|
|
273
|
+
r.get({ path: '/users', responder: responder('users') });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
let result = router.match('GET', '/api/users');
|
|
277
|
+
|
|
278
|
+
expect(result.route).toBeDefined();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('cascades middleware to child routes', () => {
|
|
282
|
+
let router = new Router<string>(),
|
|
283
|
+
authMw = mw('auth');
|
|
284
|
+
|
|
285
|
+
router.group({ middleware: [authMw as any] }).routes((r) => {
|
|
286
|
+
r.get({ path: '/protected', responder: responder('protected') });
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
let result = router.match('GET', '/protected');
|
|
290
|
+
|
|
291
|
+
expect(result.route).toBeDefined();
|
|
292
|
+
expect(result.route!.middleware).toContain(authMw);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('applies subdomain to child routes', () => {
|
|
296
|
+
let router = new Router<string>();
|
|
297
|
+
|
|
298
|
+
router.group({ subdomain: 'api' }).routes((r) => {
|
|
299
|
+
r.get({ path: '/data', responder: responder('data') });
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
let withSubdomain = router.match('GET', '/data', 'api'),
|
|
303
|
+
withoutSubdomain = router.match('GET', '/data');
|
|
304
|
+
|
|
305
|
+
expect(withSubdomain.route).toBeDefined();
|
|
306
|
+
expect(withoutSubdomain.route).toBeUndefined();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('cleans up group stack after callback', () => {
|
|
310
|
+
let router = new Router<string>();
|
|
311
|
+
|
|
312
|
+
router.group({ path: '/api' }).routes((r) => {
|
|
313
|
+
r.get({ path: '/inner', responder: responder('inner') });
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
router.get({ path: '/outer', responder: responder('outer') });
|
|
317
|
+
|
|
318
|
+
let inner = router.match('GET', '/api/inner'),
|
|
319
|
+
outer = router.match('GET', '/outer'),
|
|
320
|
+
wrongOuter = router.match('GET', '/api/outer');
|
|
321
|
+
|
|
322
|
+
expect(inner.route).toBeDefined();
|
|
323
|
+
expect(outer.route).toBeDefined();
|
|
324
|
+
expect(wrongOuter.route).toBeUndefined();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('handles nested groups (path accumulation)', () => {
|
|
328
|
+
let router = new Router<string>();
|
|
329
|
+
|
|
330
|
+
router.group({ path: '/api' }).routes((r) => {
|
|
331
|
+
r.group({ path: '/v1' }).routes((r2) => {
|
|
332
|
+
r2.get({ path: '/users', responder: responder('users') });
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
let result = router.match('GET', '/api/v1/users');
|
|
337
|
+
|
|
338
|
+
expect(result.route).toBeDefined();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
describe('HTTP method shortcuts', () => {
|
|
344
|
+
it('get() registers for GET method', () => {
|
|
345
|
+
let router = new Router<string>();
|
|
346
|
+
|
|
347
|
+
router.get({ path: '/test', responder: responder('test') });
|
|
348
|
+
|
|
349
|
+
let getResult = router.match('GET', '/test'),
|
|
350
|
+
postResult = router.match('POST', '/test');
|
|
351
|
+
|
|
352
|
+
expect(getResult.route).toBeDefined();
|
|
353
|
+
expect(postResult.route).toBeUndefined();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('post() registers for POST method', () => {
|
|
357
|
+
let router = new Router<string>();
|
|
358
|
+
|
|
359
|
+
router.post({ path: '/test', responder: responder('test') });
|
|
360
|
+
|
|
361
|
+
let getResult = router.match('GET', '/test'),
|
|
362
|
+
postResult = router.match('POST', '/test');
|
|
363
|
+
|
|
364
|
+
expect(postResult.route).toBeDefined();
|
|
365
|
+
expect(getResult.route).toBeUndefined();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('put() registers for PUT method', () => {
|
|
369
|
+
let router = new Router<string>();
|
|
370
|
+
|
|
371
|
+
router.put({ path: '/test', responder: responder('test') });
|
|
372
|
+
|
|
373
|
+
let putResult = router.match('PUT', '/test'),
|
|
374
|
+
getResult = router.match('GET', '/test');
|
|
375
|
+
|
|
376
|
+
expect(putResult.route).toBeDefined();
|
|
377
|
+
expect(getResult.route).toBeUndefined();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('delete() registers for DELETE method', () => {
|
|
381
|
+
let router = new Router<string>();
|
|
382
|
+
|
|
383
|
+
router.delete({ path: '/test', responder: responder('test') });
|
|
384
|
+
|
|
385
|
+
let deleteResult = router.match('DELETE', '/test'),
|
|
386
|
+
getResult = router.match('GET', '/test');
|
|
387
|
+
|
|
388
|
+
expect(deleteResult.route).toBeDefined();
|
|
389
|
+
expect(getResult.route).toBeUndefined();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|