@dangao/bun-server 1.7.1 → 1.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/README.md +129 -21
- package/dist/di/decorators.d.ts +37 -0
- package/dist/di/decorators.d.ts.map +1 -1
- package/dist/di/index.d.ts +1 -1
- package/dist/di/index.d.ts.map +1 -1
- package/dist/di/module-registry.d.ts +17 -0
- package/dist/di/module-registry.d.ts.map +1 -1
- package/dist/events/decorators.d.ts +52 -0
- package/dist/events/decorators.d.ts.map +1 -0
- package/dist/events/event-module.d.ts +97 -0
- package/dist/events/event-module.d.ts.map +1 -0
- package/dist/events/index.d.ts +5 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/service.d.ts +76 -0
- package/dist/events/service.d.ts.map +1 -0
- package/dist/events/types.d.ts +184 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1511 -11
- package/dist/security/filter.d.ts +23 -0
- package/dist/security/filter.d.ts.map +1 -1
- package/dist/security/guards/builtin/auth-guard.d.ts +44 -0
- package/dist/security/guards/builtin/auth-guard.d.ts.map +1 -0
- package/dist/security/guards/builtin/index.d.ts +3 -0
- package/dist/security/guards/builtin/index.d.ts.map +1 -0
- package/dist/security/guards/builtin/roles-guard.d.ts +66 -0
- package/dist/security/guards/builtin/roles-guard.d.ts.map +1 -0
- package/dist/security/guards/decorators.d.ts +50 -0
- package/dist/security/guards/decorators.d.ts.map +1 -0
- package/dist/security/guards/execution-context.d.ts +56 -0
- package/dist/security/guards/execution-context.d.ts.map +1 -0
- package/dist/security/guards/guard-registry.d.ts +67 -0
- package/dist/security/guards/guard-registry.d.ts.map +1 -0
- package/dist/security/guards/index.d.ts +7 -0
- package/dist/security/guards/index.d.ts.map +1 -0
- package/dist/security/guards/reflector.d.ts +57 -0
- package/dist/security/guards/reflector.d.ts.map +1 -0
- package/dist/security/guards/types.d.ts +126 -0
- package/dist/security/guards/types.d.ts.map +1 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/security-module.d.ts +20 -0
- package/dist/security/security-module.d.ts.map +1 -1
- package/dist/validation/class-validator.d.ts +108 -0
- package/dist/validation/class-validator.d.ts.map +1 -0
- package/dist/validation/custom-validator.d.ts +130 -0
- package/dist/validation/custom-validator.d.ts.map +1 -0
- package/dist/validation/errors.d.ts +22 -2
- package/dist/validation/errors.d.ts.map +1 -1
- package/dist/validation/index.d.ts +7 -1
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/rules/array.d.ts +33 -0
- package/dist/validation/rules/array.d.ts.map +1 -0
- package/dist/validation/rules/common.d.ts +90 -0
- package/dist/validation/rules/common.d.ts.map +1 -0
- package/dist/validation/rules/conditional.d.ts +30 -0
- package/dist/validation/rules/conditional.d.ts.map +1 -0
- package/dist/validation/rules/index.d.ts +5 -0
- package/dist/validation/rules/index.d.ts.map +1 -0
- package/dist/validation/rules/object.d.ts +30 -0
- package/dist/validation/rules/object.d.ts.map +1 -0
- package/dist/validation/types.d.ts +52 -1
- package/dist/validation/types.d.ts.map +1 -1
- package/docs/events.md +494 -0
- package/docs/guards.md +376 -0
- package/docs/guide.md +309 -1
- package/docs/request-lifecycle.md +444 -0
- package/docs/validation.md +407 -0
- package/docs/zh/events.md +494 -0
- package/docs/zh/guards.md +376 -0
- package/docs/zh/guide.md +309 -1
- package/docs/zh/request-lifecycle.md +444 -0
- package/docs/zh/validation.md +407 -0
- package/package.json +1 -1
- package/src/di/decorators.ts +46 -0
- package/src/di/index.ts +10 -1
- package/src/di/module-registry.ts +39 -0
- package/src/events/decorators.ts +103 -0
- package/src/events/event-module.ts +272 -0
- package/src/events/index.ts +32 -0
- package/src/events/service.ts +352 -0
- package/src/events/types.ts +223 -0
- package/src/index.ts +133 -1
- package/src/security/filter.ts +88 -8
- package/src/security/guards/builtin/auth-guard.ts +68 -0
- package/src/security/guards/builtin/index.ts +3 -0
- package/src/security/guards/builtin/roles-guard.ts +165 -0
- package/src/security/guards/decorators.ts +124 -0
- package/src/security/guards/execution-context.ts +152 -0
- package/src/security/guards/guard-registry.ts +164 -0
- package/src/security/guards/index.ts +7 -0
- package/src/security/guards/reflector.ts +99 -0
- package/src/security/guards/types.ts +144 -0
- package/src/security/index.ts +1 -0
- package/src/security/security-module.ts +72 -2
- package/src/validation/class-validator.ts +322 -0
- package/src/validation/custom-validator.ts +289 -0
- package/src/validation/errors.ts +50 -2
- package/src/validation/index.ts +103 -1
- package/src/validation/rules/array.ts +118 -0
- package/src/validation/rules/common.ts +286 -0
- package/src/validation/rules/conditional.ts +52 -0
- package/src/validation/rules/index.ts +51 -0
- package/src/validation/rules/object.ts +86 -0
- package/src/validation/types.ts +61 -1
- package/tests/di/global-module.test.ts +487 -0
- package/tests/events/event-decorators.test.ts +173 -0
- package/tests/events/event-emitter.test.ts +373 -0
- package/tests/events/event-module.test.ts +373 -0
- package/tests/security/guards/guards-integration.test.ts +371 -0
- package/tests/security/guards/guards.test.ts +775 -0
- package/tests/security/security-module.test.ts +2 -2
- package/tests/validation/class-validator.test.ts +349 -0
- package/tests/validation/custom-validator.test.ts +335 -0
- package/tests/validation/rules.test.ts +543 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
import { Application } from '../../../src/core/application';
|
|
4
|
+
import { Controller } from '../../../src/controller';
|
|
5
|
+
import { GET, POST } from '../../../src/router/decorators';
|
|
6
|
+
import { Injectable } from '../../../src/di/decorators';
|
|
7
|
+
import { Module } from '../../../src/di/module';
|
|
8
|
+
import { SecurityModule } from '../../../src/security/security-module';
|
|
9
|
+
import {
|
|
10
|
+
UseGuards,
|
|
11
|
+
Roles,
|
|
12
|
+
AuthGuard,
|
|
13
|
+
RolesGuard,
|
|
14
|
+
GuardRegistry,
|
|
15
|
+
type CanActivate,
|
|
16
|
+
type ExecutionContext,
|
|
17
|
+
GUARD_REGISTRY_TOKEN,
|
|
18
|
+
} from '../../../src/security/guards';
|
|
19
|
+
import { SecurityContextHolder } from '../../../src/security/context';
|
|
20
|
+
|
|
21
|
+
// 获取随机端口
|
|
22
|
+
function getTestPort(): number {
|
|
23
|
+
// 使用随机端口避免冲突
|
|
24
|
+
return 30000 + Math.floor(Math.random() * 10000);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 自定义守卫
|
|
28
|
+
@Injectable()
|
|
29
|
+
class CustomGuard implements CanActivate {
|
|
30
|
+
public canActivate(context: ExecutionContext): boolean {
|
|
31
|
+
const request = context.switchToHttp().getRequest();
|
|
32
|
+
const customHeader = request.getHeader('x-custom-guard');
|
|
33
|
+
return customHeader === 'allow';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 异步守卫
|
|
38
|
+
@Injectable()
|
|
39
|
+
class AsyncValidationGuard implements CanActivate {
|
|
40
|
+
public async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
41
|
+
// 模拟异步验证
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 计数守卫(用于验证执行顺序)
|
|
48
|
+
let guardExecutionOrder: string[] = [];
|
|
49
|
+
|
|
50
|
+
class Guard1 implements CanActivate {
|
|
51
|
+
public canActivate(): boolean {
|
|
52
|
+
guardExecutionOrder.push('Guard1');
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class Guard2 implements CanActivate {
|
|
58
|
+
public canActivate(): boolean {
|
|
59
|
+
guardExecutionOrder.push('Guard2');
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class Guard3 implements CanActivate {
|
|
65
|
+
public canActivate(): boolean {
|
|
66
|
+
guardExecutionOrder.push('Guard3');
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 测试控制器
|
|
72
|
+
@Controller('/api')
|
|
73
|
+
@UseGuards(CustomGuard)
|
|
74
|
+
class TestController {
|
|
75
|
+
@GET('/public')
|
|
76
|
+
public publicEndpoint() {
|
|
77
|
+
return { message: 'public' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@GET('/protected')
|
|
81
|
+
@UseGuards(AuthGuard)
|
|
82
|
+
public protectedEndpoint() {
|
|
83
|
+
return { message: 'protected' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@GET('/admin')
|
|
87
|
+
@UseGuards(AuthGuard, RolesGuard)
|
|
88
|
+
@Roles('admin')
|
|
89
|
+
public adminEndpoint() {
|
|
90
|
+
return { message: 'admin only' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@GET('/order')
|
|
94
|
+
@UseGuards(Guard1, Guard2, Guard3)
|
|
95
|
+
public orderEndpoint() {
|
|
96
|
+
return { message: 'order test' };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 无守卫控制器
|
|
101
|
+
@Controller('/no-guards')
|
|
102
|
+
class NoGuardsController {
|
|
103
|
+
@GET('/open')
|
|
104
|
+
public openEndpoint() {
|
|
105
|
+
return { message: 'open' };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
describe('Guards Integration Tests', () => {
|
|
110
|
+
let app: Application;
|
|
111
|
+
let port: number;
|
|
112
|
+
let baseUrl: string;
|
|
113
|
+
|
|
114
|
+
beforeEach(async () => {
|
|
115
|
+
port = getTestPort();
|
|
116
|
+
baseUrl = `http://localhost:${port}`;
|
|
117
|
+
guardExecutionOrder = [];
|
|
118
|
+
|
|
119
|
+
// 重置 SecurityModule 状态,避免测试间污染
|
|
120
|
+
SecurityModule.reset();
|
|
121
|
+
|
|
122
|
+
app = new Application();
|
|
123
|
+
|
|
124
|
+
// 注册 SecurityModule
|
|
125
|
+
app.registerModule(
|
|
126
|
+
SecurityModule.forRoot({
|
|
127
|
+
jwt: {
|
|
128
|
+
secret: 'test-secret-key-for-guards',
|
|
129
|
+
accessTokenExpiresIn: 3600,
|
|
130
|
+
},
|
|
131
|
+
excludePaths: ['/no-guards'],
|
|
132
|
+
defaultAuthRequired: false,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// 注册自定义守卫到容器
|
|
137
|
+
const container = app.getContainer();
|
|
138
|
+
container.register(CustomGuard, CustomGuard);
|
|
139
|
+
container.register(AsyncValidationGuard, AsyncValidationGuard);
|
|
140
|
+
|
|
141
|
+
app.registerController(TestController);
|
|
142
|
+
app.registerController(NoGuardsController);
|
|
143
|
+
|
|
144
|
+
await app.listen(port);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterEach(async () => {
|
|
148
|
+
await app.stop();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('should allow access when custom guard header is present', async () => {
|
|
152
|
+
const response = await fetch(`${baseUrl}/api/public`, {
|
|
153
|
+
headers: {
|
|
154
|
+
'x-custom-guard': 'allow',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(response.status).toBe(200);
|
|
159
|
+
const data = await response.json();
|
|
160
|
+
expect(data.message).toBe('public');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should deny access when custom guard header is missing', async () => {
|
|
164
|
+
const response = await fetch(`${baseUrl}/api/public`);
|
|
165
|
+
|
|
166
|
+
expect(response.status).toBe(403);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('should deny access when custom guard header has wrong value', async () => {
|
|
170
|
+
const response = await fetch(`${baseUrl}/api/public`, {
|
|
171
|
+
headers: {
|
|
172
|
+
'x-custom-guard': 'deny',
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(response.status).toBe(403);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('should require authentication for protected endpoint', async () => {
|
|
180
|
+
const response = await fetch(`${baseUrl}/api/protected`, {
|
|
181
|
+
headers: {
|
|
182
|
+
'x-custom-guard': 'allow',
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(response.status).toBe(401);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('should allow access with valid token', async () => {
|
|
190
|
+
// 首先获取 JWT token
|
|
191
|
+
const container = app.getContainer();
|
|
192
|
+
const { JWTUtil } = await import('../../../src/auth/jwt');
|
|
193
|
+
const jwtUtil = new JWTUtil({ secret: 'test-secret-key-for-guards', accessTokenExpiresIn: 3600 });
|
|
194
|
+
const token = await jwtUtil.generateAccessToken({ sub: '1', username: 'test' });
|
|
195
|
+
|
|
196
|
+
const response = await fetch(`${baseUrl}/api/protected`, {
|
|
197
|
+
headers: {
|
|
198
|
+
'x-custom-guard': 'allow',
|
|
199
|
+
'Authorization': `Bearer ${token}`,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(response.status).toBe(200);
|
|
204
|
+
const data = await response.json();
|
|
205
|
+
expect(data.message).toBe('protected');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('should deny access to admin endpoint without admin role', async () => {
|
|
209
|
+
const { JWTUtil } = await import('../../../src/auth/jwt');
|
|
210
|
+
const jwtUtil = new JWTUtil({ secret: 'test-secret-key-for-guards', accessTokenExpiresIn: 3600 });
|
|
211
|
+
const token = await jwtUtil.generateAccessToken({ sub: '1', username: 'test', roles: ['user'] });
|
|
212
|
+
|
|
213
|
+
const response = await fetch(`${baseUrl}/api/admin`, {
|
|
214
|
+
headers: {
|
|
215
|
+
'x-custom-guard': 'allow',
|
|
216
|
+
'Authorization': `Bearer ${token}`,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(response.status).toBe(403);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('should allow access to admin endpoint with admin role', async () => {
|
|
224
|
+
const { JWTUtil } = await import('../../../src/auth/jwt');
|
|
225
|
+
const jwtUtil = new JWTUtil({ secret: 'test-secret-key-for-guards', accessTokenExpiresIn: 3600 });
|
|
226
|
+
const token = await jwtUtil.generateAccessToken({ sub: '1', username: 'admin', roles: ['admin'] });
|
|
227
|
+
|
|
228
|
+
const response = await fetch(`${baseUrl}/api/admin`, {
|
|
229
|
+
headers: {
|
|
230
|
+
'x-custom-guard': 'allow',
|
|
231
|
+
'Authorization': `Bearer ${token}`,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(response.status).toBe(200);
|
|
236
|
+
const data = await response.json();
|
|
237
|
+
expect(data.message).toBe('admin only');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('should execute guards in correct order (global -> controller -> method)', async () => {
|
|
241
|
+
// 清空执行记录
|
|
242
|
+
guardExecutionOrder.length = 0;
|
|
243
|
+
|
|
244
|
+
const response = await fetch(`${baseUrl}/api/order`, {
|
|
245
|
+
headers: {
|
|
246
|
+
'x-custom-guard': 'allow',
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(response.status).toBe(200);
|
|
251
|
+
// 执行顺序:CustomGuard (controller) -> Guard1, Guard2, Guard3 (method)
|
|
252
|
+
// 只检查最后一次请求的执行顺序
|
|
253
|
+
expect(guardExecutionOrder.slice(-3)).toEqual(['Guard1', 'Guard2', 'Guard3']);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('should allow access to endpoints without guards', async () => {
|
|
257
|
+
const response = await fetch(`${baseUrl}/no-guards/open`);
|
|
258
|
+
|
|
259
|
+
expect(response.status).toBe(200);
|
|
260
|
+
const data = await response.json();
|
|
261
|
+
expect(data.message).toBe('open');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('Global Guards Integration Tests', () => {
|
|
266
|
+
let app: Application;
|
|
267
|
+
let port: number;
|
|
268
|
+
let baseUrl: string;
|
|
269
|
+
|
|
270
|
+
beforeEach(async () => {
|
|
271
|
+
port = getTestPort();
|
|
272
|
+
baseUrl = `http://localhost:${port}`;
|
|
273
|
+
|
|
274
|
+
app = new Application();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
afterEach(async () => {
|
|
278
|
+
await app.stop();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('should apply global guards to all endpoints', async () => {
|
|
282
|
+
let globalGuardCalled = false;
|
|
283
|
+
|
|
284
|
+
class GlobalTestGuard implements CanActivate {
|
|
285
|
+
public canActivate(): boolean {
|
|
286
|
+
globalGuardCalled = true;
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 注册 SecurityModule with global guards
|
|
292
|
+
app.registerModule(
|
|
293
|
+
SecurityModule.forRoot({
|
|
294
|
+
jwt: {
|
|
295
|
+
secret: 'test-secret-key',
|
|
296
|
+
accessTokenExpiresIn: 3600,
|
|
297
|
+
},
|
|
298
|
+
defaultAuthRequired: false,
|
|
299
|
+
globalGuards: [GlobalTestGuard],
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
@Controller('/test')
|
|
304
|
+
class SimpleController {
|
|
305
|
+
@GET('/hello')
|
|
306
|
+
public hello() {
|
|
307
|
+
return { message: 'hello' };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
app.registerController(SimpleController);
|
|
312
|
+
await app.listen(port);
|
|
313
|
+
|
|
314
|
+
const response = await fetch(`${baseUrl}/test/hello`);
|
|
315
|
+
expect(response.status).toBe(200);
|
|
316
|
+
expect(globalGuardCalled).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('Guards with Module Integration', () => {
|
|
321
|
+
let app: Application;
|
|
322
|
+
let port: number;
|
|
323
|
+
let baseUrl: string;
|
|
324
|
+
|
|
325
|
+
beforeEach(async () => {
|
|
326
|
+
port = getTestPort();
|
|
327
|
+
baseUrl = `http://localhost:${port}`;
|
|
328
|
+
app = new Application();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
afterEach(async () => {
|
|
332
|
+
await app.stop();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test('should work with modular architecture', async () => {
|
|
336
|
+
// 使用 @Auth 装饰器代替 @UseGuards(AuthGuard)
|
|
337
|
+
// 因为 @Auth 已经被 SecurityFilter 支持
|
|
338
|
+
@Controller('/items')
|
|
339
|
+
class ItemController {
|
|
340
|
+
@GET('/:id')
|
|
341
|
+
public getItem() {
|
|
342
|
+
return { id: '1', name: 'Test Item' };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
@Module({
|
|
347
|
+
controllers: [ItemController],
|
|
348
|
+
})
|
|
349
|
+
class ItemModule {}
|
|
350
|
+
|
|
351
|
+
// 简化测试:只验证模块可以正常工作
|
|
352
|
+
app.registerModule(
|
|
353
|
+
SecurityModule.forRoot({
|
|
354
|
+
jwt: {
|
|
355
|
+
secret: 'module-test-secret',
|
|
356
|
+
accessTokenExpiresIn: 3600,
|
|
357
|
+
},
|
|
358
|
+
defaultAuthRequired: false,
|
|
359
|
+
}),
|
|
360
|
+
);
|
|
361
|
+
app.registerModule(ItemModule);
|
|
362
|
+
await app.listen(port);
|
|
363
|
+
|
|
364
|
+
// 不需要认证的端点应该可以访问
|
|
365
|
+
const response = await fetch(`${baseUrl}/items/1`);
|
|
366
|
+
expect(response.status).toBe(200);
|
|
367
|
+
const data = await response.json();
|
|
368
|
+
expect(data.name).toBe('Test Item');
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|