@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import { RegexRouter } from '../../src/router/regex';
|
|
3
|
+
import type { RouteHandler } from '../../src/types';
|
|
4
|
+
|
|
5
|
+
describe('RegexRouter', () => {
|
|
6
|
+
let router: RegexRouter;
|
|
7
|
+
|
|
8
|
+
const mockHandler: RouteHandler = () => new Response('OK');
|
|
9
|
+
const handler1: RouteHandler = () => new Response('handler1');
|
|
10
|
+
const handler2: RouteHandler = () => new Response('handler2');
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
router = new RegexRouter();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('Router Type', () => {
|
|
17
|
+
test('should return "regex" as router type', () => {
|
|
18
|
+
expect(router.getRouterType()).toBe('regex');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('Static Route Registration', () => {
|
|
23
|
+
test('should register a GET route', () => {
|
|
24
|
+
router.get('/users', mockHandler);
|
|
25
|
+
const match = router.match('GET', '/users');
|
|
26
|
+
expect(match).toBeDefined();
|
|
27
|
+
expect(match?.handler).toBe(mockHandler);
|
|
28
|
+
expect(match?.params).toEqual({});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should register routes for all HTTP methods', () => {
|
|
32
|
+
router.get('/get', mockHandler);
|
|
33
|
+
router.post('/post', mockHandler);
|
|
34
|
+
router.put('/put', mockHandler);
|
|
35
|
+
router.patch('/patch', mockHandler);
|
|
36
|
+
router.delete('/delete', mockHandler);
|
|
37
|
+
router.head('/head', mockHandler);
|
|
38
|
+
router.options('/options', mockHandler);
|
|
39
|
+
|
|
40
|
+
expect(router.match('GET', '/get')).toBeDefined();
|
|
41
|
+
expect(router.match('POST', '/post')).toBeDefined();
|
|
42
|
+
expect(router.match('PUT', '/put')).toBeDefined();
|
|
43
|
+
expect(router.match('PATCH', '/patch')).toBeDefined();
|
|
44
|
+
expect(router.match('DELETE', '/delete')).toBeDefined();
|
|
45
|
+
expect(router.match('HEAD', '/head')).toBeDefined();
|
|
46
|
+
expect(router.match('OPTIONS', '/options')).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should register route with all method', () => {
|
|
50
|
+
router.all('/catch-all', mockHandler);
|
|
51
|
+
expect(router.match('GET', '/catch-all')).toBeDefined();
|
|
52
|
+
expect(router.match('POST', '/catch-all')).toBeDefined();
|
|
53
|
+
expect(router.match('PUT', '/catch-all')).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should be case-insensitive by default', () => {
|
|
57
|
+
router.get('/Users', mockHandler);
|
|
58
|
+
expect(router.match('GET', '/users')).toBeDefined();
|
|
59
|
+
expect(router.match('GET', '/USERS')).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should normalize trailing slashes', () => {
|
|
63
|
+
router.get('/users', mockHandler);
|
|
64
|
+
expect(router.match('GET', '/users')).toBeDefined();
|
|
65
|
+
expect(router.match('GET', '/users/')).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('Dynamic Route Registration', () => {
|
|
70
|
+
test('should match single path parameter', () => {
|
|
71
|
+
router.get('/users/:id', mockHandler);
|
|
72
|
+
const match = router.match('GET', '/users/123');
|
|
73
|
+
expect(match).toBeDefined();
|
|
74
|
+
expect(match?.params).toEqual({ id: '123' });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should match multiple path parameters', () => {
|
|
78
|
+
router.get('/users/:userId/posts/:postId', mockHandler);
|
|
79
|
+
const match = router.match('GET', '/users/42/posts/100');
|
|
80
|
+
expect(match?.params).toEqual({ userId: '42', postId: '100' });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should match wildcard routes', () => {
|
|
84
|
+
router.get('/files/*', mockHandler);
|
|
85
|
+
expect(router.match('GET', '/files/path/to/file.txt')).toBeDefined();
|
|
86
|
+
expect(router.match('GET', '/files/')).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should capture wildcard content', () => {
|
|
90
|
+
router.get('/files/*', mockHandler);
|
|
91
|
+
const match = router.match('GET', '/files/docs/readme.md');
|
|
92
|
+
expect(match?.params['*']).toBe('docs/readme.md');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('should match optional parameters', () => {
|
|
96
|
+
router.get('/users/:id?', mockHandler);
|
|
97
|
+
expect(router.match('GET', '/users')).toBeDefined();
|
|
98
|
+
expect(router.match('GET', '/users/123')?.params).toEqual({ id: '123' });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('should match regex patterns', () => {
|
|
102
|
+
router.get('/users/:id<\\d+>', mockHandler);
|
|
103
|
+
expect(router.match('GET', '/users/123')).toBeDefined();
|
|
104
|
+
expect(router.match('GET', '/users/abc')).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('Route Priority', () => {
|
|
109
|
+
test('should match static routes before dynamic', () => {
|
|
110
|
+
router.get('/users/:id', handler1);
|
|
111
|
+
router.get('/users/me', handler2);
|
|
112
|
+
|
|
113
|
+
const match = router.match('GET', '/users/me');
|
|
114
|
+
expect(match?.handler).toBe(handler2);
|
|
115
|
+
|
|
116
|
+
const match2 = router.match('GET', '/users/123');
|
|
117
|
+
expect(match2?.handler).toBe(handler1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('should prioritize fewer parameters', () => {
|
|
121
|
+
router.get('/a/:b/:c', handler1);
|
|
122
|
+
router.get('/a/:b', handler2);
|
|
123
|
+
|
|
124
|
+
const match = router.match('GET', '/a/test');
|
|
125
|
+
expect(match?.handler).toBe(handler2);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('Route Groups', () => {
|
|
130
|
+
test('should create route group with prefix', () => {
|
|
131
|
+
const api = router.group('/api');
|
|
132
|
+
api.get('/users', mockHandler);
|
|
133
|
+
|
|
134
|
+
expect(router.match('GET', '/api/users')).toBeDefined();
|
|
135
|
+
expect(router.match('GET', '/users')).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should support nested route groups', () => {
|
|
139
|
+
const api = router.group('/api');
|
|
140
|
+
const v1 = api.group('/v1');
|
|
141
|
+
v1.get('/users', mockHandler);
|
|
142
|
+
|
|
143
|
+
expect(router.match('GET', '/api/v1/users')).toBeDefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('should apply middleware to route group', () => {
|
|
147
|
+
const middleware = () => new Response('middleware');
|
|
148
|
+
const api = router.group('/api', { middleware });
|
|
149
|
+
api.get('/users', mockHandler);
|
|
150
|
+
|
|
151
|
+
const match = router.match('GET', '/api/users');
|
|
152
|
+
expect(match?.middleware).toBeDefined();
|
|
153
|
+
expect(match?.middleware?.length).toBe(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('should support group with both static and dynamic routes', () => {
|
|
157
|
+
const api = router.group('/api');
|
|
158
|
+
api.get('/users', handler1);
|
|
159
|
+
api.get('/users/:id', handler2);
|
|
160
|
+
|
|
161
|
+
expect(router.match('GET', '/api/users')?.handler).toBe(handler1);
|
|
162
|
+
expect(router.match('GET', '/api/users/123')?.handler).toBe(handler2);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('Route Information', () => {
|
|
167
|
+
test('should list all routes', () => {
|
|
168
|
+
router.get('/users', mockHandler);
|
|
169
|
+
router.post('/users', mockHandler);
|
|
170
|
+
router.get('/users/:id', mockHandler);
|
|
171
|
+
|
|
172
|
+
const routes = router.getRoutes();
|
|
173
|
+
expect(routes.length).toBe(3);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('should include route metadata', () => {
|
|
177
|
+
router.get('/users', mockHandler, { name: 'users.list' });
|
|
178
|
+
const routes = router.getRoutes();
|
|
179
|
+
expect(routes[0].name).toBe('users.list');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('should return route count', () => {
|
|
183
|
+
router.get('/users', mockHandler);
|
|
184
|
+
router.get('/posts', mockHandler);
|
|
185
|
+
router.get('/users/:id', mockHandler);
|
|
186
|
+
|
|
187
|
+
expect(router.getRouteCount()).toBe(3);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('Middleware', () => {
|
|
192
|
+
test('should accept handler with middleware array', () => {
|
|
193
|
+
const middleware = () => new Response('middleware');
|
|
194
|
+
router.get('/protected', mockHandler, { middleware: [middleware] });
|
|
195
|
+
|
|
196
|
+
const match = router.match('GET', '/protected');
|
|
197
|
+
expect(match?.middleware).toHaveLength(1);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('should accept single middleware', () => {
|
|
201
|
+
const middleware = () => new Response('middleware');
|
|
202
|
+
router.get('/protected', mockHandler, { middleware });
|
|
203
|
+
|
|
204
|
+
const match = router.match('GET', '/protected');
|
|
205
|
+
expect(match?.middleware).toHaveLength(1);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('Edge Cases', () => {
|
|
210
|
+
test('should return undefined for non-matching route', () => {
|
|
211
|
+
router.get('/users', mockHandler);
|
|
212
|
+
expect(router.match('GET', '/posts')).toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('should return undefined for wrong method', () => {
|
|
216
|
+
router.get('/users', mockHandler);
|
|
217
|
+
expect(router.match('POST', '/users')).toBeUndefined();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('should handle root path', () => {
|
|
221
|
+
router.get('/', mockHandler);
|
|
222
|
+
expect(router.match('GET', '/')).toBeDefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('should handle empty optional param', () => {
|
|
226
|
+
router.get('/search/:query?', mockHandler);
|
|
227
|
+
const match = router.match('GET', '/search');
|
|
228
|
+
expect(match).toBeDefined();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('should handle "ALL" method for routes', () => {
|
|
232
|
+
router.all('/api/*', mockHandler);
|
|
233
|
+
expect(router.match('GET', '/api/users')).toBeDefined();
|
|
234
|
+
expect(router.match('POST', '/api/users')).toBeDefined();
|
|
235
|
+
expect(router.match('DELETE', '/api/users/123')).toBeDefined();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('Async Handlers', () => {
|
|
240
|
+
test('should accept async handlers', async () => {
|
|
241
|
+
const asyncHandler: RouteHandler = async () => {
|
|
242
|
+
await Promise.resolve();
|
|
243
|
+
return new Response('async');
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
router.get('/async', asyncHandler);
|
|
247
|
+
const match = router.match('GET', '/async');
|
|
248
|
+
expect(match?.handler).toBe(asyncHandler);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('Regex Pattern Compilation', () => {
|
|
253
|
+
test('should compile complex regex patterns', () => {
|
|
254
|
+
router.get('/users/:id<[a-f0-9]{24}>', mockHandler);
|
|
255
|
+
expect(router.match('GET', '/users/507f1f77bcf86cd799439011')).toBeDefined();
|
|
256
|
+
expect(router.match('GET', '/users/invalid')).toBeUndefined();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('should handle escaped special characters in pattern', () => {
|
|
260
|
+
router.get('/files/:name<[^/]+\\.txt>', mockHandler);
|
|
261
|
+
expect(router.match('GET', '/files/document.txt')).toBeDefined();
|
|
262
|
+
expect(router.match('GET', '/files/document.pdf')).toBeUndefined();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import { Router, createRouter, createLinearRouter, createRegexRouter, createTreeRouter, generateUrl } from '../../src/router';
|
|
3
|
+
import type { RouteHandler } from '../../src/types';
|
|
4
|
+
|
|
5
|
+
describe('Router (Auto-Selection)', () => {
|
|
6
|
+
let router: Router;
|
|
7
|
+
|
|
8
|
+
const mockHandler: RouteHandler = () => new Response('OK');
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
router = new Router();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('Auto Router Selection', () => {
|
|
15
|
+
test('should start with linear router for few routes', () => {
|
|
16
|
+
router.get('/users', mockHandler);
|
|
17
|
+
router.get('/posts', mockHandler);
|
|
18
|
+
expect(router.getRouterType()).toBe('linear');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should switch to regex router at threshold (default 10)', () => {
|
|
22
|
+
for (let i = 0; i < 11; i++) {
|
|
23
|
+
router.get(`/route${i}`, mockHandler);
|
|
24
|
+
}
|
|
25
|
+
expect(router.getRouterType()).toBe('regex');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('should switch to tree router at threshold (default 50)', () => {
|
|
29
|
+
for (let i = 0; i < 51; i++) {
|
|
30
|
+
router.get(`/route${i}`, mockHandler);
|
|
31
|
+
}
|
|
32
|
+
expect(router.getRouterType()).toBe('tree');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should respect custom linear threshold', () => {
|
|
36
|
+
const customRouter = new Router({ linearThreshold: 5 });
|
|
37
|
+
for (let i = 0; i < 6; i++) {
|
|
38
|
+
customRouter.get(`/route${i}`, mockHandler);
|
|
39
|
+
}
|
|
40
|
+
expect(customRouter.getRouterType()).toBe('regex');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should respect custom regex threshold', () => {
|
|
44
|
+
const customRouter = new Router({ regexThreshold: 20 });
|
|
45
|
+
for (let i = 0; i < 21; i++) {
|
|
46
|
+
customRouter.get(`/route${i}`, mockHandler);
|
|
47
|
+
}
|
|
48
|
+
expect(customRouter.getRouterType()).toBe('tree');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('Explicit Router Selection', () => {
|
|
53
|
+
test('should use linear router when specified', () => {
|
|
54
|
+
const linearRouter = new Router({ type: 'linear' });
|
|
55
|
+
linearRouter.get('/users', mockHandler);
|
|
56
|
+
for (let i = 0; i < 100; i++) {
|
|
57
|
+
linearRouter.get(`/route${i}`, mockHandler);
|
|
58
|
+
}
|
|
59
|
+
expect(linearRouter.getRouterType()).toBe('linear');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should use regex router when specified', () => {
|
|
63
|
+
const regexRouter = new Router({ type: 'regex' });
|
|
64
|
+
regexRouter.get('/users', mockHandler);
|
|
65
|
+
expect(regexRouter.getRouterType()).toBe('regex');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('should use tree router when specified', () => {
|
|
69
|
+
const treeRouter = new Router({ type: 'tree' });
|
|
70
|
+
treeRouter.get('/users', mockHandler);
|
|
71
|
+
expect(treeRouter.getRouterType()).toBe('tree');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('Route Registration', () => {
|
|
76
|
+
test('should register a GET route', () => {
|
|
77
|
+
router.get('/users', mockHandler);
|
|
78
|
+
const match = router.match('GET', '/users');
|
|
79
|
+
expect(match).toBeDefined();
|
|
80
|
+
expect(match?.handler).toBe(mockHandler);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should register a POST route', () => {
|
|
84
|
+
router.post('/users', mockHandler);
|
|
85
|
+
const match = router.match('POST', '/users');
|
|
86
|
+
expect(match).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should register a PUT route', () => {
|
|
90
|
+
router.put('/users/:id', mockHandler);
|
|
91
|
+
const match = router.match('PUT', '/users/123');
|
|
92
|
+
expect(match).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('should register a PATCH route', () => {
|
|
96
|
+
router.patch('/users/:id', mockHandler);
|
|
97
|
+
const match = router.match('PATCH', '/users/123');
|
|
98
|
+
expect(match).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('should register a DELETE route', () => {
|
|
102
|
+
router.delete('/users/:id', mockHandler);
|
|
103
|
+
const match = router.match('DELETE', '/users/123');
|
|
104
|
+
expect(match).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should register a HEAD route', () => {
|
|
108
|
+
router.head('/users', mockHandler);
|
|
109
|
+
const match = router.match('HEAD', '/users');
|
|
110
|
+
expect(match).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('should register an OPTIONS route', () => {
|
|
114
|
+
router.options('/users', mockHandler);
|
|
115
|
+
const match = router.match('OPTIONS', '/users');
|
|
116
|
+
expect(match).toBeDefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('should register route with all method', () => {
|
|
120
|
+
router.all('/catch-all', mockHandler);
|
|
121
|
+
expect(router.match('GET', '/catch-all')).toBeDefined();
|
|
122
|
+
expect(router.match('POST', '/catch-all')).toBeDefined();
|
|
123
|
+
expect(router.match('PUT', '/catch-all')).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('Path Matching', () => {
|
|
128
|
+
test('should match exact path', () => {
|
|
129
|
+
router.get('/users', mockHandler);
|
|
130
|
+
expect(router.match('GET', '/users')).toBeDefined();
|
|
131
|
+
expect(router.match('GET', '/users/')).toBeDefined();
|
|
132
|
+
expect(router.match('GET', '/users/extra')).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('should extract path parameters', () => {
|
|
136
|
+
router.get('/users/:id', mockHandler);
|
|
137
|
+
const match = router.match('GET', '/users/123');
|
|
138
|
+
expect(match?.params).toEqual({ id: '123' });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('should extract multiple path parameters', () => {
|
|
142
|
+
router.get('/users/:userId/posts/:postId', mockHandler);
|
|
143
|
+
const match = router.match('GET', '/users/42/posts/100');
|
|
144
|
+
expect(match?.params).toEqual({ userId: '42', postId: '100' });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('should match wildcard routes', () => {
|
|
148
|
+
router.get('/files/*', mockHandler);
|
|
149
|
+
expect(router.match('GET', '/files/path/to/file.txt')).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('should match optional parameters', () => {
|
|
153
|
+
router.get('/users/:id?', mockHandler);
|
|
154
|
+
expect(router.match('GET', '/users')).toBeDefined();
|
|
155
|
+
expect(router.match('GET', '/users/123')?.params).toEqual({ id: '123' });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('should match regex patterns', () => {
|
|
159
|
+
router.get('/users/:id<\\d+>', mockHandler);
|
|
160
|
+
expect(router.match('GET', '/users/123')).toBeDefined();
|
|
161
|
+
expect(router.match('GET', '/users/abc')).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('should be case-insensitive by default', () => {
|
|
165
|
+
router.get('/Users', mockHandler);
|
|
166
|
+
expect(router.match('GET', '/users')).toBeDefined();
|
|
167
|
+
expect(router.match('GET', '/USERS')).toBeDefined();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('Route Groups', () => {
|
|
172
|
+
test('should create route group with prefix', () => {
|
|
173
|
+
const api = router.group('/api');
|
|
174
|
+
api.get('/users', mockHandler);
|
|
175
|
+
|
|
176
|
+
expect(router.match('GET', '/api/users')).toBeDefined();
|
|
177
|
+
expect(router.match('GET', '/users')).toBeUndefined();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('should support nested route groups', () => {
|
|
181
|
+
const api = router.group('/api');
|
|
182
|
+
const v1 = api.group('/v1');
|
|
183
|
+
v1.get('/users', mockHandler);
|
|
184
|
+
|
|
185
|
+
expect(router.match('GET', '/api/v1/users')).toBeDefined();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('should apply middleware to route group', () => {
|
|
189
|
+
const middleware = () => new Response('middleware');
|
|
190
|
+
const api = router.group('/api', { middleware });
|
|
191
|
+
api.get('/users', mockHandler);
|
|
192
|
+
|
|
193
|
+
const match = router.match('GET', '/api/users');
|
|
194
|
+
expect(match?.middleware).toBeDefined();
|
|
195
|
+
expect(match?.middleware?.length).toBe(1);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('should accumulate middleware in nested groups', () => {
|
|
199
|
+
const mw1 = () => new Response('mw1');
|
|
200
|
+
const mw2 = () => new Response('mw2');
|
|
201
|
+
|
|
202
|
+
const api = router.group('/api', { middleware: mw1 });
|
|
203
|
+
const v1 = api.group('/v1', { middleware: mw2 });
|
|
204
|
+
v1.get('/users', mockHandler);
|
|
205
|
+
|
|
206
|
+
const match = router.match('GET', '/api/v1/users');
|
|
207
|
+
expect(match?.middleware).toHaveLength(2);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('Route Matching', () => {
|
|
212
|
+
test('should return undefined for non-matching route', () => {
|
|
213
|
+
router.get('/users', mockHandler);
|
|
214
|
+
expect(router.match('GET', '/posts')).toBeUndefined();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('should return undefined for wrong method', () => {
|
|
218
|
+
router.get('/users', mockHandler);
|
|
219
|
+
expect(router.match('POST', '/users')).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('should match static routes before dynamic', () => {
|
|
223
|
+
const handler1 = () => new Response('1');
|
|
224
|
+
const handler2 = () => new Response('2');
|
|
225
|
+
|
|
226
|
+
router.get('/users/:id', handler1);
|
|
227
|
+
router.get('/users/special', handler2);
|
|
228
|
+
|
|
229
|
+
const match = router.match('GET', '/users/special');
|
|
230
|
+
expect(match?.handler).toBe(handler2);
|
|
231
|
+
|
|
232
|
+
const match2 = router.match('GET', '/users/123');
|
|
233
|
+
expect(match2?.handler).toBe(handler1);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('Route Information', () => {
|
|
238
|
+
test('should list all routes', () => {
|
|
239
|
+
router.get('/users', mockHandler);
|
|
240
|
+
router.post('/users', mockHandler);
|
|
241
|
+
router.get('/users/:id', mockHandler);
|
|
242
|
+
|
|
243
|
+
const routes = router.getRoutes();
|
|
244
|
+
expect(routes.length).toBe(3);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('should include route metadata', () => {
|
|
248
|
+
router.get('/users', mockHandler, { name: 'users.list' });
|
|
249
|
+
const routes = router.getRoutes();
|
|
250
|
+
expect(routes[0].name).toBe('users.list');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('should return route count', () => {
|
|
254
|
+
router.get('/users', mockHandler);
|
|
255
|
+
router.get('/posts', mockHandler);
|
|
256
|
+
router.get('/users/:id', mockHandler);
|
|
257
|
+
|
|
258
|
+
expect(router.getRouteCount()).toBe(3);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('should return config', () => {
|
|
262
|
+
const customRouter = new Router({
|
|
263
|
+
type: 'auto',
|
|
264
|
+
linearThreshold: 5,
|
|
265
|
+
regexThreshold: 25
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const config = customRouter.getConfig();
|
|
269
|
+
expect(config.type).toBe('auto');
|
|
270
|
+
expect(config.linearThreshold).toBe(5);
|
|
271
|
+
expect(config.regexThreshold).toBe(25);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('Route Handler Types', () => {
|
|
276
|
+
test('should accept async handlers', async () => {
|
|
277
|
+
const asyncHandler: RouteHandler = async () => {
|
|
278
|
+
await Promise.resolve();
|
|
279
|
+
return new Response('async');
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
router.get('/async', asyncHandler);
|
|
283
|
+
const match = router.match('GET', '/async');
|
|
284
|
+
expect(match?.handler).toBe(asyncHandler);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('should accept handler with middleware', () => {
|
|
288
|
+
const middleware = () => new Response('middleware');
|
|
289
|
+
router.get('/protected', mockHandler, { middleware: [middleware] });
|
|
290
|
+
|
|
291
|
+
const match = router.match('GET', '/protected');
|
|
292
|
+
expect(match?.middleware).toHaveLength(1);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('should accept single middleware', () => {
|
|
296
|
+
const middleware = () => new Response('middleware');
|
|
297
|
+
router.get('/protected', mockHandler, { middleware });
|
|
298
|
+
|
|
299
|
+
const match = router.match('GET', '/protected');
|
|
300
|
+
expect(match?.middleware).toHaveLength(1);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('Factory Functions', () => {
|
|
306
|
+
const mockHandler: RouteHandler = () => new Response('OK');
|
|
307
|
+
|
|
308
|
+
test('createRouter should create auto-router', () => {
|
|
309
|
+
const router = createRouter();
|
|
310
|
+
router.get('/users', mockHandler);
|
|
311
|
+
expect(router.getRouterType()).toBe('linear');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('createRouter should accept config', () => {
|
|
315
|
+
const router = createRouter({ type: 'tree' });
|
|
316
|
+
router.get('/users', mockHandler);
|
|
317
|
+
expect(router.getRouterType()).toBe('tree');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('createLinearRouter should create linear router', () => {
|
|
321
|
+
const router = createLinearRouter();
|
|
322
|
+
router.get('/users', mockHandler);
|
|
323
|
+
expect(router.getRouterType()).toBe('linear');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('createRegexRouter should create regex router', () => {
|
|
327
|
+
const router = createRegexRouter();
|
|
328
|
+
router.get('/users', mockHandler);
|
|
329
|
+
expect(router.getRouterType()).toBe('regex');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('createTreeRouter should create tree router', () => {
|
|
333
|
+
const router = createTreeRouter();
|
|
334
|
+
router.get('/users', mockHandler);
|
|
335
|
+
expect(router.getRouterType()).toBe('tree');
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('generateUrl', () => {
|
|
340
|
+
test('should generate URL with params', () => {
|
|
341
|
+
const url = generateUrl('/users/:id', { id: '123' });
|
|
342
|
+
expect(url).toBe('/users/123');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('should generate URL with multiple params', () => {
|
|
346
|
+
const url = generateUrl('/users/:userId/posts/:postId', { userId: '42', postId: '100' });
|
|
347
|
+
expect(url).toBe('/users/42/posts/100');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('should handle wildcard', () => {
|
|
351
|
+
const url = generateUrl('/files/*', { '*': 'path/to/file.txt' });
|
|
352
|
+
expect(url).toBe('/files/path/to/file.txt');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('should handle optional params', () => {
|
|
356
|
+
const url = generateUrl('/users/:id?', { id: '123' });
|
|
357
|
+
expect(url).toBe('/users/123');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('should handle missing optional param', () => {
|
|
361
|
+
const url = generateUrl('/users/:id?', {});
|
|
362
|
+
expect(url).toBe('/users/');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('should throw for missing required param', () => {
|
|
366
|
+
expect(() => generateUrl('/users/:id', {})).toThrow('Missing required parameter: id');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('should handle regex-constrained params', () => {
|
|
370
|
+
const url = generateUrl('/users/:id<\\d+>', { id: '123' });
|
|
371
|
+
expect(url).toBe('/users/123');
|
|
372
|
+
});
|
|
373
|
+
});
|