@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,282 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
createTestDatabase,
|
|
4
|
+
TestDatabase,
|
|
5
|
+
assertTableRowCount,
|
|
6
|
+
assertTableHasRow,
|
|
7
|
+
assertTableNotHasRow,
|
|
8
|
+
assertTableExists,
|
|
9
|
+
assertTableNotExists,
|
|
10
|
+
assertTableValue,
|
|
11
|
+
} from '../../src/testing/index.ts';
|
|
12
|
+
|
|
13
|
+
describe('TestDatabase', () => {
|
|
14
|
+
test('should create in-memory database', async () => {
|
|
15
|
+
const db = new TestDatabase();
|
|
16
|
+
await db.connect();
|
|
17
|
+
expect(db.isConnected).toBe(true);
|
|
18
|
+
await db.close();
|
|
19
|
+
expect(db.isConnected).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('should create database with schema', async () => {
|
|
23
|
+
const db = await createTestDatabase({
|
|
24
|
+
schema: {
|
|
25
|
+
users: {
|
|
26
|
+
id: 'INTEGER PRIMARY KEY AUTOINCREMENT',
|
|
27
|
+
name: 'TEXT NOT NULL',
|
|
28
|
+
email: 'TEXT UNIQUE',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const tables = await db.getTables();
|
|
34
|
+
expect(tables).toContain('users');
|
|
35
|
+
await db.close();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should create database with seed data', async () => {
|
|
39
|
+
const db = await createTestDatabase({
|
|
40
|
+
schema: {
|
|
41
|
+
users: {
|
|
42
|
+
id: 'INTEGER PRIMARY KEY AUTOINCREMENT',
|
|
43
|
+
name: 'TEXT NOT NULL',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
seed: {
|
|
47
|
+
users: [{ name: 'Alice' }, { name: 'Bob' }],
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const users = await db.query<{ name: string }>('SELECT * FROM users');
|
|
52
|
+
expect(users).toHaveLength(2);
|
|
53
|
+
expect(users[0].name).toBe('Alice');
|
|
54
|
+
expect(users[1].name).toBe('Bob');
|
|
55
|
+
await db.close();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('should execute queries', async () => {
|
|
59
|
+
const db = await createTestDatabase({
|
|
60
|
+
schema: {
|
|
61
|
+
items: {
|
|
62
|
+
id: 'INTEGER PRIMARY KEY AUTOINCREMENT',
|
|
63
|
+
value: 'TEXT',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await db.execute('INSERT INTO items (value) VALUES (?)', ['test1']);
|
|
69
|
+
await db.execute('INSERT INTO items (value) VALUES (?)', ['test2']);
|
|
70
|
+
|
|
71
|
+
const items = await db.query('SELECT * FROM items');
|
|
72
|
+
expect(items).toHaveLength(2);
|
|
73
|
+
|
|
74
|
+
const oneItem = await db.queryOne<{ value: string }>(
|
|
75
|
+
'SELECT * FROM items WHERE value = ?',
|
|
76
|
+
['test1'],
|
|
77
|
+
);
|
|
78
|
+
expect(oneItem?.value).toBe('test1');
|
|
79
|
+
|
|
80
|
+
await db.close();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should track operations', async () => {
|
|
84
|
+
const db = await createTestDatabase({
|
|
85
|
+
schema: {
|
|
86
|
+
test: { id: 'INTEGER PRIMARY KEY', value: 'TEXT' },
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await db.query('SELECT * FROM test');
|
|
91
|
+
await db.execute('INSERT INTO test (id, value) VALUES (1, ?)', ['x']);
|
|
92
|
+
|
|
93
|
+
// Note: createTestDatabase internally calls execute() to create the table, adding 1 operation
|
|
94
|
+
expect(db.operations).toHaveLength(3);
|
|
95
|
+
expect(db.operations[0].type).toBe('execute'); // schema creation
|
|
96
|
+
expect(db.operations[1].type).toBe('query');
|
|
97
|
+
expect(db.operations[2].type).toBe('execute');
|
|
98
|
+
expect(db.operations[2].sql).toContain('INSERT');
|
|
99
|
+
|
|
100
|
+
await db.close();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('should support transactions', async () => {
|
|
104
|
+
const db = await createTestDatabase({
|
|
105
|
+
schema: {
|
|
106
|
+
accounts: {
|
|
107
|
+
id: 'INTEGER PRIMARY KEY',
|
|
108
|
+
balance: 'INTEGER',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
seed: {
|
|
112
|
+
accounts: [{ id: 1, balance: 100 }],
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await db.transaction(async (tx) => {
|
|
117
|
+
await tx.execute('UPDATE accounts SET balance = balance - 10 WHERE id = 1');
|
|
118
|
+
await tx.execute('UPDATE accounts SET balance = balance + 10 WHERE id = 1');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const account = await db.queryOne<{ balance: number }>(
|
|
122
|
+
'SELECT * FROM accounts WHERE id = 1',
|
|
123
|
+
);
|
|
124
|
+
expect(account?.balance).toBe(100);
|
|
125
|
+
|
|
126
|
+
await db.close();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('should rollback failed transactions', async () => {
|
|
130
|
+
const db = await createTestDatabase({
|
|
131
|
+
schema: {
|
|
132
|
+
items: {
|
|
133
|
+
id: 'INTEGER PRIMARY KEY',
|
|
134
|
+
value: 'TEXT UNIQUE',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
seed: {
|
|
138
|
+
items: [{ id: 1, value: 'unique' }],
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
await db.transaction(async (tx) => {
|
|
144
|
+
await tx.execute('INSERT INTO items (id, value) VALUES (2, ?)', ['new']);
|
|
145
|
+
// This will fail due to unique constraint
|
|
146
|
+
await tx.execute('INSERT INTO items (id, value) VALUES (3, ?)', ['unique']);
|
|
147
|
+
});
|
|
148
|
+
} catch {
|
|
149
|
+
// Expected
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const items = await db.query('SELECT * FROM items');
|
|
153
|
+
expect(items).toHaveLength(1); // Only original item, transaction rolled back
|
|
154
|
+
|
|
155
|
+
await db.close();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('should reset database', async () => {
|
|
159
|
+
const db = await createTestDatabase({
|
|
160
|
+
schema: {
|
|
161
|
+
users: { id: 'INTEGER PRIMARY KEY', name: 'TEXT' },
|
|
162
|
+
},
|
|
163
|
+
seed: {
|
|
164
|
+
users: [{ id: 1, name: 'Test' }],
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(await db.getTables()).toContain('users');
|
|
169
|
+
|
|
170
|
+
await db.reset();
|
|
171
|
+
|
|
172
|
+
// Check operations BEFORE calling getTables() (which internally calls query())
|
|
173
|
+
expect(db.operations).toHaveLength(0);
|
|
174
|
+
expect(await db.getTables()).toHaveLength(0);
|
|
175
|
+
|
|
176
|
+
await db.close();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('should truncate tables', async () => {
|
|
180
|
+
const db = await createTestDatabase({
|
|
181
|
+
schema: {
|
|
182
|
+
users: { id: 'INTEGER PRIMARY KEY', name: 'TEXT' },
|
|
183
|
+
},
|
|
184
|
+
seed: {
|
|
185
|
+
users: [{ id: 1, name: 'A' }, { id: 2, name: 'B' }],
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await db.truncate('users');
|
|
190
|
+
const count = await db.count('users');
|
|
191
|
+
expect(count).toBe(0);
|
|
192
|
+
|
|
193
|
+
await db.close();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('should use schema builder helpers', async () => {
|
|
197
|
+
const db = await createTestDatabase();
|
|
198
|
+
|
|
199
|
+
await db.createTable('posts', {
|
|
200
|
+
id: 'INTEGER PRIMARY KEY AUTOINCREMENT',
|
|
201
|
+
title: 'TEXT NOT NULL',
|
|
202
|
+
body: 'TEXT',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(await db.getTables()).toContain('posts');
|
|
206
|
+
|
|
207
|
+
await db.dropTable('posts');
|
|
208
|
+
expect(await db.getTables()).not.toContain('posts');
|
|
209
|
+
|
|
210
|
+
await db.close();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('assertions should work', async () => {
|
|
214
|
+
const db = await createTestDatabase({
|
|
215
|
+
schema: {
|
|
216
|
+
users: { id: 'INTEGER PRIMARY KEY', name: 'TEXT', email: 'TEXT' },
|
|
217
|
+
},
|
|
218
|
+
seed: {
|
|
219
|
+
users: [
|
|
220
|
+
{ id: 1, name: 'Alice', email: 'alice@test.com' },
|
|
221
|
+
{ id: 2, name: 'Bob', email: 'bob@test.com' },
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
await assertTableRowCount(db, 'users', 2);
|
|
227
|
+
await assertTableHasRow(db, 'users', 'name = ?', ['Alice']);
|
|
228
|
+
await assertTableNotHasRow(db, 'users', 'name = ?', ['Charlie']);
|
|
229
|
+
await assertTableExists(db, 'users');
|
|
230
|
+
await assertTableValue(db, 'users', 'email', 'name = ?', 'alice@test.com', ['Alice']);
|
|
231
|
+
|
|
232
|
+
await db.close();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('assertTableNotExists should work', async () => {
|
|
236
|
+
const db = await createTestDatabase();
|
|
237
|
+
|
|
238
|
+
await assertTableNotExists(db, 'nonexistent');
|
|
239
|
+
|
|
240
|
+
await db.close();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('TestDatabase with beforeEach/afterEach', () => {
|
|
245
|
+
let db: TestDatabase;
|
|
246
|
+
|
|
247
|
+
beforeEach(async () => {
|
|
248
|
+
db = await createTestDatabase({
|
|
249
|
+
schema: {
|
|
250
|
+
users: {
|
|
251
|
+
id: 'INTEGER PRIMARY KEY AUTOINCREMENT',
|
|
252
|
+
name: 'TEXT NOT NULL',
|
|
253
|
+
email: 'TEXT UNIQUE',
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
afterEach(async () => {
|
|
260
|
+
await db.close();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('should have empty users table at start', async () => {
|
|
264
|
+
const count = await db.count('users');
|
|
265
|
+
expect(count).toBe(0);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('should insert users', async () => {
|
|
269
|
+
await db.execute('INSERT INTO users (name, email) VALUES (?, ?)', [
|
|
270
|
+
'Alice',
|
|
271
|
+
'alice@test.com',
|
|
272
|
+
]);
|
|
273
|
+
const count = await db.count('users');
|
|
274
|
+
expect(count).toBe(1);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('should be isolated between tests', async () => {
|
|
278
|
+
// This test should start fresh, not affected by previous test
|
|
279
|
+
const count = await db.count('users');
|
|
280
|
+
expect(count).toBe(0);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import { TreeRouter } from '../../src/router/tree';
|
|
3
|
+
import type { HTTPMethod, RouteHandler } from '../../src/types';
|
|
4
|
+
|
|
5
|
+
describe('TreeRouter', () => {
|
|
6
|
+
let router: TreeRouter;
|
|
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 TreeRouter();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('Router Type', () => {
|
|
17
|
+
test('should return "tree" as router type', () => {
|
|
18
|
+
expect(router.getRouterType()).toBe('tree');
|
|
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 for static routes', () => {
|
|
57
|
+
router.get('/Users', mockHandler);
|
|
58
|
+
expect(router.match('GET', '/users')).toBeDefined();
|
|
59
|
+
expect(router.match('GET', '/USERS')).toBeDefined();
|
|
60
|
+
expect(router.match('GET', '/UsErS')).toBeDefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should normalize trailing slashes', () => {
|
|
64
|
+
router.get('/users', mockHandler);
|
|
65
|
+
expect(router.match('GET', '/users')).toBeDefined();
|
|
66
|
+
expect(router.match('GET', '/users/')).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should handle root path', () => {
|
|
70
|
+
router.get('/', mockHandler);
|
|
71
|
+
expect(router.match('GET', '/')).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('Dynamic Routes - Parameters', () => {
|
|
76
|
+
test('should match single path parameter', () => {
|
|
77
|
+
router.get('/users/:id', mockHandler);
|
|
78
|
+
const match = router.match('GET', '/users/123');
|
|
79
|
+
expect(match).toBeDefined();
|
|
80
|
+
expect(match?.params).toEqual({ id: '123' });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should match multiple path parameters', () => {
|
|
84
|
+
router.get('/users/:userId/posts/:postId', mockHandler);
|
|
85
|
+
const match = router.match('GET', '/users/42/posts/100');
|
|
86
|
+
expect(match?.params).toEqual({ userId: '42', postId: '100' });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should match parameters with different values', () => {
|
|
90
|
+
router.get('/api/:version/users/:id', mockHandler);
|
|
91
|
+
|
|
92
|
+
expect(router.match('GET', '/api/v1/users/123')?.params).toEqual({ version: 'v1', id: '123' });
|
|
93
|
+
expect(router.match('GET', '/api/v2/users/456')?.params).toEqual({ version: 'v2', id: '456' });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('should match regex constrained parameters', () => {
|
|
97
|
+
router.get('/users/:id<\\d+>', mockHandler);
|
|
98
|
+
expect(router.match('GET', '/users/123')).toBeDefined();
|
|
99
|
+
expect(router.match('GET', '/users/abc')).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('should match alphanumeric parameter values', () => {
|
|
103
|
+
router.get('/files/:name', mockHandler);
|
|
104
|
+
const match = router.match('GET', '/files/document-pdf');
|
|
105
|
+
expect(match?.params).toEqual({ name: 'document-pdf' });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('Dynamic Routes - Wildcards', () => {
|
|
110
|
+
test('should match wildcard routes', () => {
|
|
111
|
+
router.get('/files/*', mockHandler);
|
|
112
|
+
expect(router.match('GET', '/files/path/to/file.txt')).toBeDefined();
|
|
113
|
+
expect(router.match('GET', '/files/docs/readme.md')).toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('should capture wildcard content', () => {
|
|
117
|
+
router.get('/files/*', mockHandler);
|
|
118
|
+
const match = router.match('GET', '/files/docs/readme.md');
|
|
119
|
+
expect(match?.params['*']).toBe('docs/readme.md');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('should match wildcard at end of pattern', () => {
|
|
123
|
+
router.get('/api/*', mockHandler);
|
|
124
|
+
expect(router.match('GET', '/api/users/123/posts/456')).toBeDefined();
|
|
125
|
+
expect(router.match('GET', '/api/anything/goes/here')).toBeDefined();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('Route Priority', () => {
|
|
130
|
+
test('should match static routes before parameters', () => {
|
|
131
|
+
router.get('/users/me', handler1);
|
|
132
|
+
router.get('/users/:id', handler2);
|
|
133
|
+
|
|
134
|
+
// Static route should match first
|
|
135
|
+
const match = router.match('GET', '/users/me');
|
|
136
|
+
expect(match?.handler).toBe(handler1);
|
|
137
|
+
|
|
138
|
+
// Parameter route for other values
|
|
139
|
+
const match2 = router.match('GET', '/users/123');
|
|
140
|
+
expect(match2?.handler).toBe(handler2);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('should handle overlapping routes correctly', () => {
|
|
144
|
+
router.get('/api/users', handler1);
|
|
145
|
+
router.get('/api/users/:id', handler2);
|
|
146
|
+
router.get('/api/users/:id/settings', mockHandler);
|
|
147
|
+
|
|
148
|
+
expect(router.match('GET', '/api/users')?.handler).toBe(handler1);
|
|
149
|
+
expect(router.match('GET', '/api/users/42')?.handler).toBe(handler2);
|
|
150
|
+
expect(router.match('GET', '/api/users/42/settings')?.handler).toBe(mockHandler);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('Route Groups', () => {
|
|
155
|
+
test('should create route group with prefix', () => {
|
|
156
|
+
const api = router.group('/api');
|
|
157
|
+
api.get('/users', mockHandler);
|
|
158
|
+
|
|
159
|
+
expect(router.match('GET', '/api/users')).toBeDefined();
|
|
160
|
+
expect(router.match('GET', '/users')).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should support nested route groups', () => {
|
|
164
|
+
const api = router.group('/api');
|
|
165
|
+
const v1 = api.group('/v1');
|
|
166
|
+
v1.get('/users', mockHandler);
|
|
167
|
+
|
|
168
|
+
expect(router.match('GET', '/api/v1/users')).toBeDefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should apply middleware to route group', () => {
|
|
172
|
+
const middleware = () => new Response('middleware');
|
|
173
|
+
const api = router.group('/api', { middleware });
|
|
174
|
+
api.get('/users', mockHandler);
|
|
175
|
+
|
|
176
|
+
const match = router.match('GET', '/api/users');
|
|
177
|
+
expect(match?.middleware).toBeDefined();
|
|
178
|
+
expect(match?.middleware?.length).toBe(1);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('should support multiple nested groups with middleware', () => {
|
|
182
|
+
const mw1 = () => new Response('mw1');
|
|
183
|
+
const mw2 = () => new Response('mw2');
|
|
184
|
+
|
|
185
|
+
const api = router.group('/api', { middleware: mw1 });
|
|
186
|
+
const v1 = api.group('/v1', { middleware: mw2 });
|
|
187
|
+
v1.get('/users', mockHandler);
|
|
188
|
+
|
|
189
|
+
const match = router.match('GET', '/api/v1/users');
|
|
190
|
+
expect(match?.middleware).toHaveLength(2);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('Route Information', () => {
|
|
195
|
+
test('should list all routes', () => {
|
|
196
|
+
router.get('/users', mockHandler);
|
|
197
|
+
router.post('/users', mockHandler);
|
|
198
|
+
router.get('/users/:id', mockHandler);
|
|
199
|
+
|
|
200
|
+
const routes = router.getRoutes();
|
|
201
|
+
expect(routes.length).toBe(3);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('should include route metadata', () => {
|
|
205
|
+
router.get('/users', mockHandler, { name: 'users.list' });
|
|
206
|
+
const routes = router.getRoutes();
|
|
207
|
+
expect(routes.find(r => r.name === 'users.list')).toBeDefined();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('should return route count', () => {
|
|
211
|
+
router.get('/users', mockHandler);
|
|
212
|
+
router.get('/posts', mockHandler);
|
|
213
|
+
router.get('/users/:id', mockHandler);
|
|
214
|
+
|
|
215
|
+
expect(router.getRouteCount()).toBe(3);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('should return tree statistics', () => {
|
|
219
|
+
router.get('/users', mockHandler);
|
|
220
|
+
router.get('/users/:id', mockHandler);
|
|
221
|
+
router.get('/posts', mockHandler);
|
|
222
|
+
router.get('/posts/:postId/comments/:commentId', mockHandler);
|
|
223
|
+
|
|
224
|
+
const stats = router.getTreeStats();
|
|
225
|
+
expect(stats.routes).toBe(4);
|
|
226
|
+
expect(stats.nodes).toBeGreaterThan(0);
|
|
227
|
+
expect(stats.depth).toBeGreaterThan(0);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('Middleware', () => {
|
|
232
|
+
test('should accept handler with middleware array', () => {
|
|
233
|
+
const middleware = () => new Response('middleware');
|
|
234
|
+
router.get('/protected', mockHandler, { middleware: [middleware] });
|
|
235
|
+
|
|
236
|
+
const match = router.match('GET', '/protected');
|
|
237
|
+
expect(match?.middleware).toHaveLength(1);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('should accept single middleware', () => {
|
|
241
|
+
const middleware = () => new Response('middleware');
|
|
242
|
+
router.get('/protected', mockHandler, { middleware });
|
|
243
|
+
|
|
244
|
+
const match = router.match('GET', '/protected');
|
|
245
|
+
expect(match?.middleware).toHaveLength(1);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('Edge Cases', () => {
|
|
250
|
+
test('should return undefined for non-matching route', () => {
|
|
251
|
+
router.get('/users', mockHandler);
|
|
252
|
+
expect(router.match('GET', '/posts')).toBeUndefined();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('should return undefined for wrong method', () => {
|
|
256
|
+
router.get('/users', mockHandler);
|
|
257
|
+
expect(router.match('POST', '/users')).toBeUndefined();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('should handle "ALL" method for routes', () => {
|
|
261
|
+
router.all('/api/*', mockHandler);
|
|
262
|
+
expect(router.match('GET', '/api/users')).toBeDefined();
|
|
263
|
+
expect(router.match('POST', '/api/users')).toBeDefined();
|
|
264
|
+
expect(router.match('DELETE', '/api/users/123')).toBeDefined();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('should handle deep nesting', () => {
|
|
268
|
+
router.get('/a/b/c/d/e/f/g/h', mockHandler);
|
|
269
|
+
expect(router.match('GET', '/a/b/c/d/e/f/g/h')).toBeDefined();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('should handle many routes efficiently', () => {
|
|
273
|
+
// Add 100 routes
|
|
274
|
+
for (let i = 0; i < 100; i++) {
|
|
275
|
+
router.get(`/api/v1/resource${i}`, mockHandler);
|
|
276
|
+
router.get(`/api/v1/resource${i}/:id`, mockHandler);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Should still match correctly
|
|
280
|
+
expect(router.match('GET', '/api/v1/resource50')).toBeDefined();
|
|
281
|
+
expect(router.match('GET', '/api/v1/resource50/123')?.params).toEqual({ id: '123' });
|
|
282
|
+
expect(router.getRouteCount()).toBe(200);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('Async Handlers', () => {
|
|
287
|
+
test('should accept async handlers', async () => {
|
|
288
|
+
const asyncHandler: RouteHandler = async () => {
|
|
289
|
+
await Promise.resolve();
|
|
290
|
+
return new Response('async');
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
router.get('/async', asyncHandler);
|
|
294
|
+
const match = router.match('GET', '/async');
|
|
295
|
+
expect(match?.handler).toBe(asyncHandler);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('Complex Scenarios', () => {
|
|
300
|
+
test('should handle REST API pattern', () => {
|
|
301
|
+
router.get('/users', handler1); // List users
|
|
302
|
+
router.post('/users', handler2); // Create user
|
|
303
|
+
router.get('/users/:id', mockHandler); // Get user
|
|
304
|
+
router.put('/users/:id', mockHandler); // Update user
|
|
305
|
+
router.delete('/users/:id', mockHandler); // Delete user
|
|
306
|
+
router.get('/users/:id/posts', mockHandler); // Get user's posts
|
|
307
|
+
|
|
308
|
+
expect(router.match('GET', '/users')?.handler).toBe(handler1);
|
|
309
|
+
expect(router.match('POST', '/users')?.handler).toBe(handler2);
|
|
310
|
+
expect(router.match('GET', '/users/123')?.params).toEqual({ id: '123' });
|
|
311
|
+
expect(router.match('DELETE', '/users/456')).toBeDefined();
|
|
312
|
+
expect(router.match('GET', '/users/123/posts')).toBeDefined();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('should handle mixed static and dynamic at same level', () => {
|
|
316
|
+
router.get('/users/me', handler1);
|
|
317
|
+
router.get('/users/:id', handler2);
|
|
318
|
+
router.get('/users/all', mockHandler);
|
|
319
|
+
|
|
320
|
+
expect(router.match('GET', '/users/me')?.handler).toBe(handler1);
|
|
321
|
+
expect(router.match('GET', '/users/all')?.handler).toBe(mockHandler);
|
|
322
|
+
expect(router.match('GET', '/users/123')?.handler).toBe(handler2);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|