@onebun/core 0.1.24 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -8
- package/src/application/application.test.ts +492 -19
- package/src/application/application.ts +490 -358
- package/src/decorators/decorators.test.ts +139 -0
- package/src/decorators/decorators.ts +127 -0
- package/src/docs-examples.test.ts +670 -71
- package/src/file/index.ts +8 -0
- package/src/file/onebun-file.test.ts +315 -0
- package/src/file/onebun-file.ts +304 -0
- package/src/index.ts +13 -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 +45 -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.1
|
|
3
|
+
"version": "0.2.1",
|
|
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();
|
|
@@ -985,12 +1080,7 @@ describe('OneBunApplication', () => {
|
|
|
985
1080
|
|
|
986
1081
|
originalServe = Bun.serve;
|
|
987
1082
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
988
|
-
(Bun as any).serve =
|
|
989
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
990
|
-
(mockServer as any).fetchHandler = options.fetch;
|
|
991
|
-
|
|
992
|
-
return mockServer;
|
|
993
|
-
});
|
|
1083
|
+
(Bun as any).serve = createRoutesAwareMock(mockServer);
|
|
994
1084
|
});
|
|
995
1085
|
|
|
996
1086
|
afterEach(() => {
|
|
@@ -1052,13 +1142,7 @@ describe('OneBunApplication', () => {
|
|
|
1052
1142
|
|
|
1053
1143
|
originalServe = Bun.serve;
|
|
1054
1144
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1055
|
-
(Bun as any).serve =
|
|
1056
|
-
// Store the fetch handler for testing
|
|
1057
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1058
|
-
(mockServer as any).fetchHandler = options.fetch;
|
|
1059
|
-
|
|
1060
|
-
return mockServer;
|
|
1061
|
-
});
|
|
1145
|
+
(Bun as any).serve = createRoutesAwareMock(mockServer);
|
|
1062
1146
|
});
|
|
1063
1147
|
|
|
1064
1148
|
afterEach(() => {
|
|
@@ -2460,12 +2544,7 @@ describe('OneBunApplication', () => {
|
|
|
2460
2544
|
|
|
2461
2545
|
originalServe = Bun.serve;
|
|
2462
2546
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2463
|
-
(Bun as any).serve =
|
|
2464
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2465
|
-
(mockServer as any).fetchHandler = options.fetch;
|
|
2466
|
-
|
|
2467
|
-
return mockServer;
|
|
2468
|
-
});
|
|
2547
|
+
(Bun as any).serve = createRoutesAwareMock(mockServer);
|
|
2469
2548
|
});
|
|
2470
2549
|
|
|
2471
2550
|
afterEach(() => {
|
|
@@ -2546,6 +2625,400 @@ describe('OneBunApplication', () => {
|
|
|
2546
2625
|
});
|
|
2547
2626
|
});
|
|
2548
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
|
+
|
|
2549
3022
|
describe('Graceful shutdown', () => {
|
|
2550
3023
|
let originalServe: typeof Bun.serve;
|
|
2551
3024
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|