@onebun/core 0.1.23 → 0.2.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/package.json +8 -8
- package/src/application/application.test.ts +519 -32
- package/src/application/application.ts +342 -377
- package/src/decorators/decorators.test.ts +47 -33
- package/src/decorators/decorators.ts +12 -0
- package/src/docs-examples.test.ts +300 -1
- package/src/index.ts +2 -0
- package/src/module/controller.ts +7 -3
- package/src/queue/docs-examples.test.ts +86 -0
- package/src/service-client/service-client.test.ts +1 -1
- package/src/types.ts +18 -2
- package/src/validation/schemas.test.ts +0 -2
- package/src/websocket/ws-base-gateway.ts +2 -2
- package/src/websocket/ws-handler.ts +4 -3
- package/src/websocket/ws.types.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onebun/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Core package for OneBun framework - decorators, DI, modules, controllers",
|
|
5
5
|
"license": "LGPL-3.0",
|
|
6
6
|
"author": "RemRyahirev",
|
|
@@ -41,17 +41,17 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"effect": "^3.13.10",
|
|
43
43
|
"arktype": "^2.0.0",
|
|
44
|
-
"@onebun/logger": "^0.
|
|
45
|
-
"@onebun/envs": "^0.
|
|
46
|
-
"@onebun/metrics": "^0.
|
|
47
|
-
"@onebun/requests": "^0.
|
|
48
|
-
"@onebun/trace": "^0.
|
|
44
|
+
"@onebun/logger": "^0.2.0",
|
|
45
|
+
"@onebun/envs": "^0.2.0",
|
|
46
|
+
"@onebun/metrics": "^0.2.0",
|
|
47
|
+
"@onebun/requests": "^0.2.0",
|
|
48
|
+
"@onebun/trace": "^0.2.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"bun-types": "1.
|
|
51
|
+
"bun-types": "^1.3.8",
|
|
52
52
|
"testcontainers": "^11.7.1"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
|
-
"bun": "1.2.
|
|
55
|
+
"bun": ">=1.2.12"
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
Query,
|
|
22
22
|
Body,
|
|
23
23
|
Header,
|
|
24
|
+
Cookie,
|
|
25
|
+
Req,
|
|
24
26
|
} from '../decorators/decorators';
|
|
25
27
|
import { Controller as BaseController } from '../module/controller';
|
|
26
28
|
import { makeMockLoggerLayer } from '../testing/test-utils';
|
|
@@ -38,6 +40,99 @@ function createTestApp(
|
|
|
38
40
|
});
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Match a request path against a route pattern with :param support.
|
|
45
|
+
* Returns extracted params or null if no match.
|
|
46
|
+
*/
|
|
47
|
+
function matchRoutePattern(pattern: string, path: string): Record<string, string> | null {
|
|
48
|
+
if (pattern === path) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
if (!pattern.includes(':')) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const patternParts = pattern.split('/');
|
|
56
|
+
const pathParts = path.split('/');
|
|
57
|
+
|
|
58
|
+
if (patternParts.length !== pathParts.length) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const params: Record<string, string> = {};
|
|
63
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
64
|
+
if (patternParts[i].startsWith(':')) {
|
|
65
|
+
params[patternParts[i].slice(1)] = pathParts[i];
|
|
66
|
+
} else if (patternParts[i] !== pathParts[i]) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return params;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a mock Bun.serve that captures routes and emulates Bun route matching.
|
|
76
|
+
* The mock's fetchHandler resolves routes by pattern matching and creates
|
|
77
|
+
* BunRequest-like objects with params and cookies.
|
|
78
|
+
*/
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
|
+
function createRoutesAwareMock(mockServer: any) {
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
82
|
+
return mock((options: any) => {
|
|
83
|
+
const routes = options.routes || {};
|
|
84
|
+
const fetchFallback = options.fetch;
|
|
85
|
+
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
+
(mockServer as any).fetchHandler = async (request: Request) => {
|
|
88
|
+
const url = new URL(request.url);
|
|
89
|
+
const path = url.pathname;
|
|
90
|
+
const method = request.method;
|
|
91
|
+
|
|
92
|
+
// Try to find matching route (iterate all registered patterns)
|
|
93
|
+
for (const [pattern, handlers] of Object.entries(routes)) {
|
|
94
|
+
const params = matchRoutePattern(pattern, path);
|
|
95
|
+
if (params !== null) {
|
|
96
|
+
const routeHandlers = typeof handlers === 'function'
|
|
97
|
+
? { [method]: handlers as Function }
|
|
98
|
+
: handlers as Record<string, Function>;
|
|
99
|
+
const handler = routeHandlers[method];
|
|
100
|
+
if (handler) {
|
|
101
|
+
// Create BunRequest-like object with params and cookies
|
|
102
|
+
// Parse Cookie header into a Map to emulate CookieMap
|
|
103
|
+
const cookieHeader = request.headers.get('cookie') || '';
|
|
104
|
+
const cookieMap = new Map<string, string>();
|
|
105
|
+
if (cookieHeader) {
|
|
106
|
+
for (const pair of cookieHeader.split(';')) {
|
|
107
|
+
const [key, ...rest] = pair.split('=');
|
|
108
|
+
if (key) {
|
|
109
|
+
cookieMap.set(key.trim(), rest.join('=').trim());
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const bunReq = Object.assign(request, {
|
|
115
|
+
params,
|
|
116
|
+
cookies: cookieMap,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return handler(bunReq, mockServer);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Fall through to fetch handler
|
|
125
|
+
if (fetchFallback) {
|
|
126
|
+
return fetchFallback(request, mockServer);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return new Response('Not Found', { status: 404 });
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return mockServer;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
41
136
|
describe('OneBunApplication', () => {
|
|
42
137
|
beforeEach(() => {
|
|
43
138
|
register.clear();
|
|
@@ -145,7 +240,7 @@ describe('OneBunApplication', () => {
|
|
|
145
240
|
expect(config).toBeDefined();
|
|
146
241
|
});
|
|
147
242
|
|
|
148
|
-
test('should create config service when envSchema provided', () => {
|
|
243
|
+
test('should create config service when envSchema provided (duplicate check)', () => {
|
|
149
244
|
@Module({})
|
|
150
245
|
class TestModule {}
|
|
151
246
|
|
|
@@ -159,12 +254,9 @@ describe('OneBunApplication', () => {
|
|
|
159
254
|
|
|
160
255
|
const app = createTestApp(TestModule, { envSchema });
|
|
161
256
|
|
|
162
|
-
//
|
|
257
|
+
// Config service is created eagerly in the constructor
|
|
163
258
|
const config = app.getConfig();
|
|
164
259
|
expect(config).toBeDefined();
|
|
165
|
-
|
|
166
|
-
// The actual value access might need the config to be fully initialized
|
|
167
|
-
// which happens during runtime, not during construction
|
|
168
260
|
});
|
|
169
261
|
|
|
170
262
|
test('should provide typed access to config values via getConfig()', () => {
|
|
@@ -261,17 +353,34 @@ describe('OneBunApplication', () => {
|
|
|
261
353
|
});
|
|
262
354
|
|
|
263
355
|
describe('Layer methods', () => {
|
|
264
|
-
|
|
356
|
+
let originalServe: typeof Bun.serve;
|
|
357
|
+
|
|
358
|
+
beforeEach(() => {
|
|
359
|
+
originalServe = Bun.serve;
|
|
360
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
361
|
+
(Bun as any).serve = mock(() => ({
|
|
362
|
+
stop: mock(),
|
|
363
|
+
port: 3000,
|
|
364
|
+
}));
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
afterEach(() => {
|
|
368
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
369
|
+
(Bun as any).serve = originalServe;
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('should return layer from root module', async () => {
|
|
265
373
|
@Module({})
|
|
266
374
|
class TestModule {}
|
|
267
375
|
|
|
268
376
|
const app = createTestApp(TestModule);
|
|
377
|
+
await app.start();
|
|
269
378
|
|
|
270
379
|
const layer = app.getLayer();
|
|
271
380
|
expect(layer).toBeDefined();
|
|
272
381
|
});
|
|
273
382
|
|
|
274
|
-
test('should return layer for complex module structure', () => {
|
|
383
|
+
test('should return layer for complex module structure', async () => {
|
|
275
384
|
class TestController {}
|
|
276
385
|
class TestService {}
|
|
277
386
|
|
|
@@ -282,6 +391,7 @@ describe('OneBunApplication', () => {
|
|
|
282
391
|
class TestModule {}
|
|
283
392
|
|
|
284
393
|
const app = createTestApp(TestModule);
|
|
394
|
+
await app.start();
|
|
285
395
|
|
|
286
396
|
const layer = app.getLayer();
|
|
287
397
|
expect(layer).toBeDefined();
|
|
@@ -394,14 +504,13 @@ describe('OneBunApplication', () => {
|
|
|
394
504
|
});
|
|
395
505
|
|
|
396
506
|
describe('Module class handling', () => {
|
|
397
|
-
test('should throw error for plain class without decorator', () => {
|
|
507
|
+
test('should throw error for plain class without decorator', async () => {
|
|
398
508
|
class PlainModule {}
|
|
399
509
|
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}).toThrow('Module PlainModule does not have @Module decorator');
|
|
510
|
+
// Module creation now happens in start(), so the error is thrown there
|
|
511
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
512
|
+
const app = createTestApp(PlainModule as any);
|
|
513
|
+
await expect(app.start()).rejects.toThrow('Module PlainModule does not have @Module decorator');
|
|
405
514
|
});
|
|
406
515
|
|
|
407
516
|
test('should handle class with constructor parameters', () => {
|
|
@@ -971,12 +1080,7 @@ describe('OneBunApplication', () => {
|
|
|
971
1080
|
|
|
972
1081
|
originalServe = Bun.serve;
|
|
973
1082
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
974
|
-
(Bun as any).serve =
|
|
975
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
976
|
-
(mockServer as any).fetchHandler = options.fetch;
|
|
977
|
-
|
|
978
|
-
return mockServer;
|
|
979
|
-
});
|
|
1083
|
+
(Bun as any).serve = createRoutesAwareMock(mockServer);
|
|
980
1084
|
});
|
|
981
1085
|
|
|
982
1086
|
afterEach(() => {
|
|
@@ -1038,13 +1142,7 @@ describe('OneBunApplication', () => {
|
|
|
1038
1142
|
|
|
1039
1143
|
originalServe = Bun.serve;
|
|
1040
1144
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1041
|
-
(Bun as any).serve =
|
|
1042
|
-
// Store the fetch handler for testing
|
|
1043
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1044
|
-
(mockServer as any).fetchHandler = options.fetch;
|
|
1045
|
-
|
|
1046
|
-
return mockServer;
|
|
1047
|
-
});
|
|
1145
|
+
(Bun as any).serve = createRoutesAwareMock(mockServer);
|
|
1048
1146
|
});
|
|
1049
1147
|
|
|
1050
1148
|
afterEach(() => {
|
|
@@ -2446,12 +2544,7 @@ describe('OneBunApplication', () => {
|
|
|
2446
2544
|
|
|
2447
2545
|
originalServe = Bun.serve;
|
|
2448
2546
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2449
|
-
(Bun as any).serve =
|
|
2450
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2451
|
-
(mockServer as any).fetchHandler = options.fetch;
|
|
2452
|
-
|
|
2453
|
-
return mockServer;
|
|
2454
|
-
});
|
|
2547
|
+
(Bun as any).serve = createRoutesAwareMock(mockServer);
|
|
2455
2548
|
});
|
|
2456
2549
|
|
|
2457
2550
|
afterEach(() => {
|
|
@@ -2532,6 +2625,400 @@ describe('OneBunApplication', () => {
|
|
|
2532
2625
|
});
|
|
2533
2626
|
});
|
|
2534
2627
|
|
|
2628
|
+
describe('Cookies, Headers, and @Req()', () => {
|
|
2629
|
+
let originalServe: typeof Bun.serve;
|
|
2630
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2631
|
+
let mockServer: any;
|
|
2632
|
+
|
|
2633
|
+
beforeEach(() => {
|
|
2634
|
+
register.clear();
|
|
2635
|
+
|
|
2636
|
+
mockServer = {
|
|
2637
|
+
stop: mock(),
|
|
2638
|
+
hostname: 'localhost',
|
|
2639
|
+
port: 3000,
|
|
2640
|
+
};
|
|
2641
|
+
|
|
2642
|
+
originalServe = Bun.serve;
|
|
2643
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2644
|
+
(Bun as any).serve = createRoutesAwareMock(mockServer);
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
afterEach(() => {
|
|
2648
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2649
|
+
(Bun as any).serve = originalServe;
|
|
2650
|
+
});
|
|
2651
|
+
|
|
2652
|
+
test('should extract cookie value with @Cookie decorator', async () => {
|
|
2653
|
+
@Controller('/api')
|
|
2654
|
+
class ApiController extends BaseController {
|
|
2655
|
+
@Get('/me')
|
|
2656
|
+
async getMe(@Cookie('session') session?: string) {
|
|
2657
|
+
return { session };
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
@Module({
|
|
2662
|
+
controllers: [ApiController],
|
|
2663
|
+
})
|
|
2664
|
+
class TestModule {}
|
|
2665
|
+
|
|
2666
|
+
const app = createTestApp(TestModule);
|
|
2667
|
+
await app.start();
|
|
2668
|
+
|
|
2669
|
+
const request = new Request('http://localhost:3000/api/me', {
|
|
2670
|
+
method: 'GET',
|
|
2671
|
+
headers: {
|
|
2672
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
2673
|
+
'Cookie': 'session=abc123; theme=dark',
|
|
2674
|
+
},
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2677
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2678
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
2679
|
+
const body = await response.json();
|
|
2680
|
+
|
|
2681
|
+
expect(response.status).toBe(200);
|
|
2682
|
+
expect(body.result.session).toBe('abc123');
|
|
2683
|
+
});
|
|
2684
|
+
|
|
2685
|
+
test('should return undefined for missing cookie with @Cookie decorator', async () => {
|
|
2686
|
+
@Controller('/api')
|
|
2687
|
+
class ApiController extends BaseController {
|
|
2688
|
+
@Get('/me')
|
|
2689
|
+
async getMe(@Cookie('missing_cookie') value?: string) {
|
|
2690
|
+
return { value, isUndefined: value === undefined };
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
@Module({
|
|
2695
|
+
controllers: [ApiController],
|
|
2696
|
+
})
|
|
2697
|
+
class TestModule {}
|
|
2698
|
+
|
|
2699
|
+
const app = createTestApp(TestModule);
|
|
2700
|
+
await app.start();
|
|
2701
|
+
|
|
2702
|
+
const request = new Request('http://localhost:3000/api/me', {
|
|
2703
|
+
method: 'GET',
|
|
2704
|
+
});
|
|
2705
|
+
|
|
2706
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2707
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
2708
|
+
const body = await response.json();
|
|
2709
|
+
|
|
2710
|
+
expect(response.status).toBe(200);
|
|
2711
|
+
expect(body.result.value).toBeUndefined();
|
|
2712
|
+
expect(body.result.isUndefined).toBe(true);
|
|
2713
|
+
});
|
|
2714
|
+
|
|
2715
|
+
test('should return 500 when required cookie is missing', async () => {
|
|
2716
|
+
@Controller('/api')
|
|
2717
|
+
class ApiController extends BaseController {
|
|
2718
|
+
@Get('/auth')
|
|
2719
|
+
async auth(@Cookie('token', { required: true }) token: string) {
|
|
2720
|
+
return { token };
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
@Module({
|
|
2725
|
+
controllers: [ApiController],
|
|
2726
|
+
})
|
|
2727
|
+
class TestModule {}
|
|
2728
|
+
|
|
2729
|
+
const app = createTestApp(TestModule);
|
|
2730
|
+
await app.start();
|
|
2731
|
+
|
|
2732
|
+
// No cookies sent
|
|
2733
|
+
const request = new Request('http://localhost:3000/api/auth', {
|
|
2734
|
+
method: 'GET',
|
|
2735
|
+
});
|
|
2736
|
+
|
|
2737
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2738
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
2739
|
+
|
|
2740
|
+
expect(response.status).toBe(500);
|
|
2741
|
+
});
|
|
2742
|
+
|
|
2743
|
+
test('should extract multiple cookies with @Cookie decorator', async () => {
|
|
2744
|
+
@Controller('/api')
|
|
2745
|
+
class ApiController extends BaseController {
|
|
2746
|
+
@Get('/prefs')
|
|
2747
|
+
async prefs(
|
|
2748
|
+
@Cookie('theme') theme?: string,
|
|
2749
|
+
@Cookie('lang') lang?: string,
|
|
2750
|
+
@Cookie('session') session?: string,
|
|
2751
|
+
) {
|
|
2752
|
+
return { theme, lang, session };
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
@Module({
|
|
2757
|
+
controllers: [ApiController],
|
|
2758
|
+
})
|
|
2759
|
+
class TestModule {}
|
|
2760
|
+
|
|
2761
|
+
const app = createTestApp(TestModule);
|
|
2762
|
+
await app.start();
|
|
2763
|
+
|
|
2764
|
+
const request = new Request('http://localhost:3000/api/prefs', {
|
|
2765
|
+
method: 'GET',
|
|
2766
|
+
headers: {
|
|
2767
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
2768
|
+
'Cookie': 'theme=dark; lang=en; session=xyz789',
|
|
2769
|
+
},
|
|
2770
|
+
});
|
|
2771
|
+
|
|
2772
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2773
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
2774
|
+
const body = await response.json();
|
|
2775
|
+
|
|
2776
|
+
expect(response.status).toBe(200);
|
|
2777
|
+
expect(body.result.theme).toBe('dark');
|
|
2778
|
+
expect(body.result.lang).toBe('en');
|
|
2779
|
+
expect(body.result.session).toBe('xyz789');
|
|
2780
|
+
});
|
|
2781
|
+
|
|
2782
|
+
test('should inject BunRequest-like object with @Req() that has cookies and params', async () => {
|
|
2783
|
+
@Controller('/api')
|
|
2784
|
+
class ApiController extends BaseController {
|
|
2785
|
+
@Get('/users/:id')
|
|
2786
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2787
|
+
async getUser(@Req() req: any) {
|
|
2788
|
+
return {
|
|
2789
|
+
hasCookies: typeof req.cookies?.get === 'function',
|
|
2790
|
+
hasParams: req.params !== undefined,
|
|
2791
|
+
paramId: req.params?.id,
|
|
2792
|
+
cookieSession: req.cookies?.get('session') ?? null,
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
@Module({
|
|
2798
|
+
controllers: [ApiController],
|
|
2799
|
+
})
|
|
2800
|
+
class TestModule {}
|
|
2801
|
+
|
|
2802
|
+
const app = createTestApp(TestModule);
|
|
2803
|
+
await app.start();
|
|
2804
|
+
|
|
2805
|
+
const request = new Request('http://localhost:3000/api/users/42', {
|
|
2806
|
+
method: 'GET',
|
|
2807
|
+
headers: {
|
|
2808
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
2809
|
+
'Cookie': 'session=test-session',
|
|
2810
|
+
},
|
|
2811
|
+
});
|
|
2812
|
+
|
|
2813
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2814
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
2815
|
+
const body = await response.json();
|
|
2816
|
+
|
|
2817
|
+
expect(response.status).toBe(200);
|
|
2818
|
+
expect(body.result.hasCookies).toBe(true);
|
|
2819
|
+
expect(body.result.hasParams).toBe(true);
|
|
2820
|
+
expect(body.result.paramId).toBe('42');
|
|
2821
|
+
expect(body.result.cookieSession).toBe('test-session');
|
|
2822
|
+
});
|
|
2823
|
+
|
|
2824
|
+
test('should preserve custom headers in Response returned from handler', async () => {
|
|
2825
|
+
@Controller('/api')
|
|
2826
|
+
class ApiController extends BaseController {
|
|
2827
|
+
@Get('/custom-headers')
|
|
2828
|
+
async customHeaders() {
|
|
2829
|
+
return new Response(JSON.stringify({ message: 'ok' }), {
|
|
2830
|
+
status: 200,
|
|
2831
|
+
headers: {
|
|
2832
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
2833
|
+
'Content-Type': 'application/json',
|
|
2834
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
2835
|
+
'X-Custom-Header': 'custom-value',
|
|
2836
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
2837
|
+
'X-Request-ID': 'req-123',
|
|
2838
|
+
},
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
@Module({
|
|
2844
|
+
controllers: [ApiController],
|
|
2845
|
+
})
|
|
2846
|
+
class TestModule {}
|
|
2847
|
+
|
|
2848
|
+
const app = createTestApp(TestModule);
|
|
2849
|
+
await app.start();
|
|
2850
|
+
|
|
2851
|
+
const request = new Request('http://localhost:3000/api/custom-headers', {
|
|
2852
|
+
method: 'GET',
|
|
2853
|
+
});
|
|
2854
|
+
|
|
2855
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2856
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
2857
|
+
|
|
2858
|
+
expect(response.status).toBe(200);
|
|
2859
|
+
expect(response.headers.get('X-Custom-Header')).toBe('custom-value');
|
|
2860
|
+
expect(response.headers.get('X-Request-ID')).toBe('req-123');
|
|
2861
|
+
});
|
|
2862
|
+
|
|
2863
|
+
test('should preserve single Set-Cookie header in Response', async () => {
|
|
2864
|
+
@Controller('/api')
|
|
2865
|
+
class ApiController extends BaseController {
|
|
2866
|
+
@Get('/login')
|
|
2867
|
+
async login() {
|
|
2868
|
+
return new Response(JSON.stringify({ loggedIn: true }), {
|
|
2869
|
+
status: 200,
|
|
2870
|
+
headers: {
|
|
2871
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
2872
|
+
'Content-Type': 'application/json',
|
|
2873
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
2874
|
+
'Set-Cookie': 'session=abc123; Path=/; HttpOnly',
|
|
2875
|
+
},
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
@Module({
|
|
2881
|
+
controllers: [ApiController],
|
|
2882
|
+
})
|
|
2883
|
+
class TestModule {}
|
|
2884
|
+
|
|
2885
|
+
const app = createTestApp(TestModule);
|
|
2886
|
+
await app.start();
|
|
2887
|
+
|
|
2888
|
+
const request = new Request('http://localhost:3000/api/login', {
|
|
2889
|
+
method: 'GET',
|
|
2890
|
+
});
|
|
2891
|
+
|
|
2892
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2893
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
2894
|
+
|
|
2895
|
+
expect(response.status).toBe(200);
|
|
2896
|
+
const setCookies = response.headers.getSetCookie();
|
|
2897
|
+
expect(setCookies.length).toBeGreaterThanOrEqual(1);
|
|
2898
|
+
expect(setCookies[0]).toContain('session=abc123');
|
|
2899
|
+
});
|
|
2900
|
+
|
|
2901
|
+
test('should preserve multiple Set-Cookie headers in Response', async () => {
|
|
2902
|
+
@Controller('/api')
|
|
2903
|
+
class ApiController extends BaseController {
|
|
2904
|
+
@Get('/multi-cookie')
|
|
2905
|
+
async multiCookie() {
|
|
2906
|
+
const headers = new Headers();
|
|
2907
|
+
headers.append('Content-Type', 'application/json');
|
|
2908
|
+
headers.append('Set-Cookie', 'session=abc; Path=/; HttpOnly');
|
|
2909
|
+
headers.append('Set-Cookie', 'theme=dark; Path=/');
|
|
2910
|
+
headers.append('Set-Cookie', 'lang=en; Path=/');
|
|
2911
|
+
|
|
2912
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
2913
|
+
status: 200,
|
|
2914
|
+
headers,
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
@Module({
|
|
2920
|
+
controllers: [ApiController],
|
|
2921
|
+
})
|
|
2922
|
+
class TestModule {}
|
|
2923
|
+
|
|
2924
|
+
const app = createTestApp(TestModule);
|
|
2925
|
+
await app.start();
|
|
2926
|
+
|
|
2927
|
+
const request = new Request('http://localhost:3000/api/multi-cookie', {
|
|
2928
|
+
method: 'GET',
|
|
2929
|
+
});
|
|
2930
|
+
|
|
2931
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2932
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
2933
|
+
|
|
2934
|
+
expect(response.status).toBe(200);
|
|
2935
|
+
const setCookies = response.headers.getSetCookie();
|
|
2936
|
+
expect(setCookies.length).toBe(3);
|
|
2937
|
+
expect(setCookies).toContainEqual(expect.stringContaining('session=abc'));
|
|
2938
|
+
expect(setCookies).toContainEqual(expect.stringContaining('theme=dark'));
|
|
2939
|
+
expect(setCookies).toContainEqual(expect.stringContaining('lang=en'));
|
|
2940
|
+
});
|
|
2941
|
+
|
|
2942
|
+
test('should handle route params via req.params in @Req()', async () => {
|
|
2943
|
+
@Controller('/api')
|
|
2944
|
+
class ApiController extends BaseController {
|
|
2945
|
+
@Get('/items/:category/:id')
|
|
2946
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2947
|
+
async getItem(@Req() req: any) {
|
|
2948
|
+
return {
|
|
2949
|
+
category: req.params?.category,
|
|
2950
|
+
id: req.params?.id,
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
@Module({
|
|
2956
|
+
controllers: [ApiController],
|
|
2957
|
+
})
|
|
2958
|
+
class TestModule {}
|
|
2959
|
+
|
|
2960
|
+
const app = createTestApp(TestModule);
|
|
2961
|
+
await app.start();
|
|
2962
|
+
|
|
2963
|
+
const request = new Request('http://localhost:3000/api/items/electronics/42', {
|
|
2964
|
+
method: 'GET',
|
|
2965
|
+
});
|
|
2966
|
+
|
|
2967
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2968
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
2969
|
+
const body = await response.json();
|
|
2970
|
+
|
|
2971
|
+
expect(response.status).toBe(200);
|
|
2972
|
+
expect(body.result.category).toBe('electronics');
|
|
2973
|
+
expect(body.result.id).toBe('42');
|
|
2974
|
+
});
|
|
2975
|
+
|
|
2976
|
+
test('should combine @Cookie, @Param, @Query, and @Header in same handler', async () => {
|
|
2977
|
+
@Controller('/api')
|
|
2978
|
+
class ApiController extends BaseController {
|
|
2979
|
+
@Get('/combined/:id')
|
|
2980
|
+
async combined(
|
|
2981
|
+
@Param('id') id: string,
|
|
2982
|
+
@Query('sort') sort: string,
|
|
2983
|
+
@Header('Authorization') auth: string,
|
|
2984
|
+
@Cookie('session') session?: string,
|
|
2985
|
+
) {
|
|
2986
|
+
return {
|
|
2987
|
+
id: parseInt(id), sort, auth, session,
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
@Module({
|
|
2993
|
+
controllers: [ApiController],
|
|
2994
|
+
})
|
|
2995
|
+
class TestModule {}
|
|
2996
|
+
|
|
2997
|
+
const app = createTestApp(TestModule);
|
|
2998
|
+
await app.start();
|
|
2999
|
+
|
|
3000
|
+
const request = new Request('http://localhost:3000/api/combined/99?sort=name', {
|
|
3001
|
+
method: 'GET',
|
|
3002
|
+
headers: {
|
|
3003
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
3004
|
+
'Authorization': 'Bearer token456',
|
|
3005
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
3006
|
+
'Cookie': 'session=sess789',
|
|
3007
|
+
},
|
|
3008
|
+
});
|
|
3009
|
+
|
|
3010
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3011
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
3012
|
+
const body = await response.json();
|
|
3013
|
+
|
|
3014
|
+
expect(response.status).toBe(200);
|
|
3015
|
+
expect(body.result.id).toBe(99);
|
|
3016
|
+
expect(body.result.sort).toBe('name');
|
|
3017
|
+
expect(body.result.auth).toBe('Bearer token456');
|
|
3018
|
+
expect(body.result.session).toBe('sess789');
|
|
3019
|
+
});
|
|
3020
|
+
});
|
|
3021
|
+
|
|
2535
3022
|
describe('Graceful shutdown', () => {
|
|
2536
3023
|
let originalServe: typeof Bun.serve;
|
|
2537
3024
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|