@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,775 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
import {
|
|
4
|
+
UseGuards,
|
|
5
|
+
Roles,
|
|
6
|
+
getGuardsMetadata,
|
|
7
|
+
getRolesMetadata,
|
|
8
|
+
} from '../../../src/security/guards/decorators';
|
|
9
|
+
import {
|
|
10
|
+
GUARDS_METADATA_KEY,
|
|
11
|
+
ROLES_METADATA_KEY,
|
|
12
|
+
type CanActivate,
|
|
13
|
+
type ExecutionContext,
|
|
14
|
+
} from '../../../src/security/guards/types';
|
|
15
|
+
import { GuardRegistry } from '../../../src/security/guards/guard-registry';
|
|
16
|
+
import { ExecutionContextImpl } from '../../../src/security/guards/execution-context';
|
|
17
|
+
import { Reflector } from '../../../src/security/guards/reflector';
|
|
18
|
+
import { AuthGuard, OptionalAuthGuard } from '../../../src/security/guards/builtin/auth-guard';
|
|
19
|
+
import { RolesGuard, createRolesGuard } from '../../../src/security/guards/builtin/roles-guard';
|
|
20
|
+
import { Container } from '../../../src/di/container';
|
|
21
|
+
import { Context } from '../../../src/core/context';
|
|
22
|
+
import { SecurityContextHolder, SecurityContextImpl } from '../../../src/security/context';
|
|
23
|
+
import { Controller } from '../../../src/controller';
|
|
24
|
+
import { GET, POST } from '../../../src/router/decorators';
|
|
25
|
+
import { Injectable } from '../../../src/di/decorators';
|
|
26
|
+
|
|
27
|
+
// 测试用守卫
|
|
28
|
+
class TestGuard implements CanActivate {
|
|
29
|
+
public canActivate(context: ExecutionContext): boolean {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class RejectGuard implements CanActivate {
|
|
35
|
+
public canActivate(context: ExecutionContext): boolean {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Injectable()
|
|
41
|
+
class AsyncGuard implements CanActivate {
|
|
42
|
+
public async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('Guards Decorators', () => {
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
// 清理元数据
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('@UseGuards should add guards metadata to class', () => {
|
|
54
|
+
@UseGuards(TestGuard)
|
|
55
|
+
class TestController {}
|
|
56
|
+
|
|
57
|
+
const guards = getGuardsMetadata(TestController);
|
|
58
|
+
expect(guards).toHaveLength(1);
|
|
59
|
+
expect(guards[0]).toBe(TestGuard);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('@UseGuards should add guards metadata to method', () => {
|
|
63
|
+
class TestController {
|
|
64
|
+
@UseGuards(TestGuard)
|
|
65
|
+
public testMethod() {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const guards = getGuardsMetadata(TestController.prototype, 'testMethod');
|
|
69
|
+
expect(guards).toHaveLength(1);
|
|
70
|
+
expect(guards[0]).toBe(TestGuard);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('@UseGuards should support multiple guards', () => {
|
|
74
|
+
@UseGuards(TestGuard, RejectGuard)
|
|
75
|
+
class TestController {}
|
|
76
|
+
|
|
77
|
+
const guards = getGuardsMetadata(TestController);
|
|
78
|
+
expect(guards).toHaveLength(2);
|
|
79
|
+
expect(guards[0]).toBe(TestGuard);
|
|
80
|
+
expect(guards[1]).toBe(RejectGuard);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('@UseGuards should support guard instances', () => {
|
|
84
|
+
const guardInstance = new TestGuard();
|
|
85
|
+
|
|
86
|
+
@UseGuards(guardInstance)
|
|
87
|
+
class TestController {}
|
|
88
|
+
|
|
89
|
+
const guards = getGuardsMetadata(TestController);
|
|
90
|
+
expect(guards).toHaveLength(1);
|
|
91
|
+
expect(guards[0]).toBe(guardInstance);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('@Roles should add roles metadata to method', () => {
|
|
95
|
+
class TestController {
|
|
96
|
+
@Roles('admin', 'user')
|
|
97
|
+
public testMethod() {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const roles = getRolesMetadata(TestController.prototype, 'testMethod');
|
|
101
|
+
expect(roles).toEqual(['admin', 'user']);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('@Roles should add roles metadata to class', () => {
|
|
105
|
+
@Roles('admin')
|
|
106
|
+
class TestController {}
|
|
107
|
+
|
|
108
|
+
const roles = getRolesMetadata(TestController);
|
|
109
|
+
expect(roles).toEqual(['admin']);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('GuardRegistry', () => {
|
|
114
|
+
let registry: GuardRegistry;
|
|
115
|
+
let container: Container;
|
|
116
|
+
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
registry = new GuardRegistry();
|
|
119
|
+
container = new Container();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('should add and get global guards', () => {
|
|
123
|
+
registry.addGlobalGuards(TestGuard);
|
|
124
|
+
|
|
125
|
+
const guards = registry.getGlobalGuards();
|
|
126
|
+
expect(guards).toHaveLength(1);
|
|
127
|
+
expect(guards[0]).toBe(TestGuard);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('should clear global guards', () => {
|
|
131
|
+
registry.addGlobalGuards(TestGuard);
|
|
132
|
+
registry.clearGlobalGuards();
|
|
133
|
+
|
|
134
|
+
const guards = registry.getGlobalGuards();
|
|
135
|
+
expect(guards).toHaveLength(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should collect guards from class and method', () => {
|
|
139
|
+
@UseGuards(TestGuard)
|
|
140
|
+
@Controller('/test')
|
|
141
|
+
class TestController {
|
|
142
|
+
@UseGuards(RejectGuard)
|
|
143
|
+
@GET('/')
|
|
144
|
+
public testMethod() {}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const guards = registry.collectGuards(TestController, 'testMethod');
|
|
148
|
+
expect(guards).toHaveLength(2);
|
|
149
|
+
expect(guards[0]).toBe(TestGuard);
|
|
150
|
+
expect(guards[1]).toBe(RejectGuard);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('should execute guards in order', async () => {
|
|
154
|
+
const executionOrder: string[] = [];
|
|
155
|
+
|
|
156
|
+
class Guard1 implements CanActivate {
|
|
157
|
+
public canActivate(): boolean {
|
|
158
|
+
executionOrder.push('guard1');
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class Guard2 implements CanActivate {
|
|
164
|
+
public canActivate(): boolean {
|
|
165
|
+
executionOrder.push('guard2');
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@UseGuards(Guard1, Guard2)
|
|
171
|
+
@Controller('/test')
|
|
172
|
+
class TestController {
|
|
173
|
+
@GET('/')
|
|
174
|
+
public testMethod() {}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const request = new Request('http://localhost/test');
|
|
178
|
+
const ctx = new Context(request);
|
|
179
|
+
const executionContext = new ExecutionContextImpl(
|
|
180
|
+
ctx,
|
|
181
|
+
TestController,
|
|
182
|
+
'testMethod',
|
|
183
|
+
TestController.prototype.testMethod,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await registry.executeGuards(executionContext, container);
|
|
187
|
+
|
|
188
|
+
expect(executionOrder).toEqual(['guard1', 'guard2']);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('should throw ForbiddenException when guard returns false', async () => {
|
|
192
|
+
@UseGuards(RejectGuard)
|
|
193
|
+
@Controller('/test')
|
|
194
|
+
class TestController {
|
|
195
|
+
@GET('/')
|
|
196
|
+
public testMethod() {}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const request = new Request('http://localhost/test');
|
|
200
|
+
const ctx = new Context(request);
|
|
201
|
+
const executionContext = new ExecutionContextImpl(
|
|
202
|
+
ctx,
|
|
203
|
+
TestController,
|
|
204
|
+
'testMethod',
|
|
205
|
+
TestController.prototype.testMethod,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
await expect(registry.executeGuards(executionContext, container)).rejects.toThrow(
|
|
209
|
+
'Access denied by guard: RejectGuard',
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('should resolve guard from DI container', async () => {
|
|
214
|
+
@Injectable()
|
|
215
|
+
class InjectedGuard implements CanActivate {
|
|
216
|
+
public canActivate(): boolean {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
container.register(InjectedGuard, InjectedGuard);
|
|
222
|
+
|
|
223
|
+
@UseGuards(InjectedGuard)
|
|
224
|
+
@Controller('/test')
|
|
225
|
+
class TestController {
|
|
226
|
+
@GET('/')
|
|
227
|
+
public testMethod() {}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const request = new Request('http://localhost/test');
|
|
231
|
+
const ctx = new Context(request);
|
|
232
|
+
const executionContext = new ExecutionContextImpl(
|
|
233
|
+
ctx,
|
|
234
|
+
TestController,
|
|
235
|
+
'testMethod',
|
|
236
|
+
TestController.prototype.testMethod,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const result = await registry.executeGuards(executionContext, container);
|
|
240
|
+
expect(result).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('should support async guards', async () => {
|
|
244
|
+
@UseGuards(AsyncGuard)
|
|
245
|
+
@Controller('/test')
|
|
246
|
+
class TestController {
|
|
247
|
+
@GET('/')
|
|
248
|
+
public testMethod() {}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const request = new Request('http://localhost/test');
|
|
252
|
+
const ctx = new Context(request);
|
|
253
|
+
const executionContext = new ExecutionContextImpl(
|
|
254
|
+
ctx,
|
|
255
|
+
TestController,
|
|
256
|
+
'testMethod',
|
|
257
|
+
TestController.prototype.testMethod,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const result = await registry.executeGuards(executionContext, container);
|
|
261
|
+
expect(result).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('ExecutionContext', () => {
|
|
266
|
+
test('should provide HTTP context', () => {
|
|
267
|
+
const request = new Request('http://localhost/test');
|
|
268
|
+
const ctx = new Context(request);
|
|
269
|
+
|
|
270
|
+
@Controller('/test')
|
|
271
|
+
class TestController {
|
|
272
|
+
@GET('/')
|
|
273
|
+
public testMethod() {}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const executionContext = new ExecutionContextImpl(
|
|
277
|
+
ctx,
|
|
278
|
+
TestController,
|
|
279
|
+
'testMethod',
|
|
280
|
+
TestController.prototype.testMethod,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const httpHost = executionContext.switchToHttp();
|
|
284
|
+
expect(httpHost.getRequest()).toBe(ctx);
|
|
285
|
+
expect(httpHost.getResponse()).toBeUndefined();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('should return controller class', () => {
|
|
289
|
+
const request = new Request('http://localhost/test');
|
|
290
|
+
const ctx = new Context(request);
|
|
291
|
+
|
|
292
|
+
@Controller('/test')
|
|
293
|
+
class TestController {
|
|
294
|
+
@GET('/')
|
|
295
|
+
public testMethod() {}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const executionContext = new ExecutionContextImpl(
|
|
299
|
+
ctx,
|
|
300
|
+
TestController,
|
|
301
|
+
'testMethod',
|
|
302
|
+
TestController.prototype.testMethod,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
expect(executionContext.getClass()).toBe(TestController);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('should return method name', () => {
|
|
309
|
+
const request = new Request('http://localhost/test');
|
|
310
|
+
const ctx = new Context(request);
|
|
311
|
+
|
|
312
|
+
@Controller('/test')
|
|
313
|
+
class TestController {
|
|
314
|
+
@GET('/')
|
|
315
|
+
public testMethod() {}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const executionContext = new ExecutionContextImpl(
|
|
319
|
+
ctx,
|
|
320
|
+
TestController,
|
|
321
|
+
'testMethod',
|
|
322
|
+
TestController.prototype.testMethod,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
expect(executionContext.getMethodName()).toBe('testMethod');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('should get metadata from method', () => {
|
|
329
|
+
const request = new Request('http://localhost/test');
|
|
330
|
+
const ctx = new Context(request);
|
|
331
|
+
|
|
332
|
+
@Controller('/test')
|
|
333
|
+
class TestController {
|
|
334
|
+
@Roles('admin')
|
|
335
|
+
@GET('/')
|
|
336
|
+
public testMethod() {}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const executionContext = new ExecutionContextImpl(
|
|
340
|
+
ctx,
|
|
341
|
+
TestController,
|
|
342
|
+
'testMethod',
|
|
343
|
+
TestController.prototype.testMethod,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const roles = executionContext.getMetadata<string[]>(ROLES_METADATA_KEY);
|
|
347
|
+
expect(roles).toEqual(['admin']);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('should get metadata from class if not on method', () => {
|
|
351
|
+
const request = new Request('http://localhost/test');
|
|
352
|
+
const ctx = new Context(request);
|
|
353
|
+
|
|
354
|
+
@Roles('admin')
|
|
355
|
+
@Controller('/test')
|
|
356
|
+
class TestController {
|
|
357
|
+
@GET('/')
|
|
358
|
+
public testMethod() {}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const executionContext = new ExecutionContextImpl(
|
|
362
|
+
ctx,
|
|
363
|
+
TestController,
|
|
364
|
+
'testMethod',
|
|
365
|
+
TestController.prototype.testMethod,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const roles = executionContext.getMetadata<string[]>(ROLES_METADATA_KEY);
|
|
369
|
+
expect(roles).toEqual(['admin']);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('should throw error when accessing WebSocket context if not set', () => {
|
|
373
|
+
const request = new Request('http://localhost/test');
|
|
374
|
+
const ctx = new Context(request);
|
|
375
|
+
|
|
376
|
+
@Controller('/test')
|
|
377
|
+
class TestController {
|
|
378
|
+
@GET('/')
|
|
379
|
+
public testMethod() {}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const executionContext = new ExecutionContextImpl(
|
|
383
|
+
ctx,
|
|
384
|
+
TestController,
|
|
385
|
+
'testMethod',
|
|
386
|
+
TestController.prototype.testMethod,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
expect(() => executionContext.switchToWs()).toThrow('WebSocket context is not available');
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe('Reflector', () => {
|
|
394
|
+
let reflector: Reflector;
|
|
395
|
+
|
|
396
|
+
beforeEach(() => {
|
|
397
|
+
reflector = new Reflector();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test('should get metadata from class', () => {
|
|
401
|
+
@Roles('admin')
|
|
402
|
+
class TestController {}
|
|
403
|
+
|
|
404
|
+
const roles = reflector.getFromClass<string[]>(ROLES_METADATA_KEY, TestController);
|
|
405
|
+
expect(roles).toEqual(['admin']);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('should get metadata from method', () => {
|
|
409
|
+
class TestController {
|
|
410
|
+
@Roles('user')
|
|
411
|
+
public testMethod() {}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const roles = reflector.getFromMethod<string[]>(
|
|
415
|
+
ROLES_METADATA_KEY,
|
|
416
|
+
TestController.prototype,
|
|
417
|
+
'testMethod',
|
|
418
|
+
);
|
|
419
|
+
expect(roles).toEqual(['user']);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('should merge metadata from class and method', () => {
|
|
423
|
+
@Roles('admin')
|
|
424
|
+
class TestController {
|
|
425
|
+
@Roles('user')
|
|
426
|
+
public testMethod() {}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const roles = reflector.getAllAndMerge<string[]>(
|
|
430
|
+
ROLES_METADATA_KEY,
|
|
431
|
+
TestController,
|
|
432
|
+
'testMethod',
|
|
433
|
+
);
|
|
434
|
+
expect(roles).toEqual(['admin', 'user']);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test('should override class metadata with method metadata', () => {
|
|
438
|
+
@Roles('admin')
|
|
439
|
+
class TestController {
|
|
440
|
+
@Roles('user')
|
|
441
|
+
public testMethod() {}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const roles = reflector.getAllAndOverride<string[]>(
|
|
445
|
+
ROLES_METADATA_KEY,
|
|
446
|
+
TestController,
|
|
447
|
+
'testMethod',
|
|
448
|
+
);
|
|
449
|
+
expect(roles).toEqual(['user']);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test('should return class metadata when method has none', () => {
|
|
453
|
+
@Roles('admin')
|
|
454
|
+
class TestController {
|
|
455
|
+
public testMethod() {}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const roles = reflector.getAllAndOverride<string[]>(
|
|
459
|
+
ROLES_METADATA_KEY,
|
|
460
|
+
TestController,
|
|
461
|
+
'testMethod',
|
|
462
|
+
);
|
|
463
|
+
expect(roles).toEqual(['admin']);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
describe('AuthGuard', () => {
|
|
468
|
+
let guard: AuthGuard;
|
|
469
|
+
|
|
470
|
+
beforeEach(() => {
|
|
471
|
+
guard = new AuthGuard();
|
|
472
|
+
SecurityContextHolder.clearContext();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('should throw UnauthorizedException when not authenticated', () => {
|
|
476
|
+
const request = new Request('http://localhost/test');
|
|
477
|
+
const ctx = new Context(request);
|
|
478
|
+
|
|
479
|
+
@Controller('/test')
|
|
480
|
+
class TestController {
|
|
481
|
+
@GET('/')
|
|
482
|
+
public testMethod() {}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const executionContext = new ExecutionContextImpl(
|
|
486
|
+
ctx,
|
|
487
|
+
TestController,
|
|
488
|
+
'testMethod',
|
|
489
|
+
TestController.prototype.testMethod,
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
expect(() => guard.canActivate(executionContext)).toThrow('Authentication required');
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test('should return true when authenticated', async () => {
|
|
496
|
+
// 在 SecurityContextHolder 的上下文中运行
|
|
497
|
+
await SecurityContextHolder.runWithContext(async () => {
|
|
498
|
+
const securityContext = SecurityContextHolder.getContext();
|
|
499
|
+
securityContext.setAuthentication({
|
|
500
|
+
authenticated: true,
|
|
501
|
+
principal: { id: '1', username: 'test' },
|
|
502
|
+
credentials: null,
|
|
503
|
+
authorities: [],
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const request = new Request('http://localhost/test');
|
|
507
|
+
const ctx = new Context(request);
|
|
508
|
+
|
|
509
|
+
@Controller('/test')
|
|
510
|
+
class TestController {
|
|
511
|
+
@GET('/')
|
|
512
|
+
public testMethod() {}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const executionContext = new ExecutionContextImpl(
|
|
516
|
+
ctx,
|
|
517
|
+
TestController,
|
|
518
|
+
'testMethod',
|
|
519
|
+
TestController.prototype.testMethod,
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const result = guard.canActivate(executionContext);
|
|
523
|
+
expect(result).toBe(true);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
describe('OptionalAuthGuard', () => {
|
|
529
|
+
test('should always return true', () => {
|
|
530
|
+
const guard = new OptionalAuthGuard();
|
|
531
|
+
const request = new Request('http://localhost/test');
|
|
532
|
+
const ctx = new Context(request);
|
|
533
|
+
|
|
534
|
+
@Controller('/test')
|
|
535
|
+
class TestController {
|
|
536
|
+
@GET('/')
|
|
537
|
+
public testMethod() {}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const executionContext = new ExecutionContextImpl(
|
|
541
|
+
ctx,
|
|
542
|
+
TestController,
|
|
543
|
+
'testMethod',
|
|
544
|
+
TestController.prototype.testMethod,
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
const result = guard.canActivate(executionContext);
|
|
548
|
+
expect(result).toBe(true);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
describe('RolesGuard', () => {
|
|
553
|
+
let guard: RolesGuard;
|
|
554
|
+
|
|
555
|
+
beforeEach(() => {
|
|
556
|
+
guard = new RolesGuard();
|
|
557
|
+
SecurityContextHolder.clearContext();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test('should return true when no roles required', async () => {
|
|
561
|
+
await SecurityContextHolder.runWithContext(async () => {
|
|
562
|
+
const request = new Request('http://localhost/test');
|
|
563
|
+
const ctx = new Context(request);
|
|
564
|
+
|
|
565
|
+
@Controller('/test')
|
|
566
|
+
class TestController {
|
|
567
|
+
@GET('/')
|
|
568
|
+
public testMethod() {}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const executionContext = new ExecutionContextImpl(
|
|
572
|
+
ctx,
|
|
573
|
+
TestController,
|
|
574
|
+
'testMethod',
|
|
575
|
+
TestController.prototype.testMethod,
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const result = guard.canActivate(executionContext);
|
|
579
|
+
expect(result).toBe(true);
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test('should return true when user has required role', async () => {
|
|
584
|
+
await SecurityContextHolder.runWithContext(async () => {
|
|
585
|
+
const securityContext = SecurityContextHolder.getContext();
|
|
586
|
+
securityContext.setAuthentication({
|
|
587
|
+
authenticated: true,
|
|
588
|
+
principal: { id: '1', username: 'test' },
|
|
589
|
+
credentials: null,
|
|
590
|
+
authorities: ['admin'],
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const request = new Request('http://localhost/test');
|
|
594
|
+
const ctx = new Context(request);
|
|
595
|
+
|
|
596
|
+
@Controller('/test')
|
|
597
|
+
class TestController {
|
|
598
|
+
@Roles('admin')
|
|
599
|
+
@GET('/')
|
|
600
|
+
public testMethod() {}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const executionContext = new ExecutionContextImpl(
|
|
604
|
+
ctx,
|
|
605
|
+
TestController,
|
|
606
|
+
'testMethod',
|
|
607
|
+
TestController.prototype.testMethod,
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
const result = guard.canActivate(executionContext);
|
|
611
|
+
expect(result).toBe(true);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test('should throw ForbiddenException when user lacks required role', async () => {
|
|
616
|
+
await SecurityContextHolder.runWithContext(async () => {
|
|
617
|
+
const securityContext = SecurityContextHolder.getContext();
|
|
618
|
+
securityContext.setAuthentication({
|
|
619
|
+
authenticated: true,
|
|
620
|
+
principal: { id: '1', username: 'test' },
|
|
621
|
+
credentials: null,
|
|
622
|
+
authorities: ['user'],
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const request = new Request('http://localhost/test');
|
|
626
|
+
const ctx = new Context(request);
|
|
627
|
+
|
|
628
|
+
@Controller('/test')
|
|
629
|
+
class TestController {
|
|
630
|
+
@Roles('admin')
|
|
631
|
+
@GET('/')
|
|
632
|
+
public testMethod() {}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const executionContext = new ExecutionContextImpl(
|
|
636
|
+
ctx,
|
|
637
|
+
TestController,
|
|
638
|
+
'testMethod',
|
|
639
|
+
TestController.prototype.testMethod,
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
expect(() => guard.canActivate(executionContext)).toThrow('Access denied');
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test('should throw ForbiddenException when not authenticated', async () => {
|
|
647
|
+
await SecurityContextHolder.runWithContext(async () => {
|
|
648
|
+
const request = new Request('http://localhost/test');
|
|
649
|
+
const ctx = new Context(request);
|
|
650
|
+
|
|
651
|
+
@Controller('/test')
|
|
652
|
+
class TestController {
|
|
653
|
+
@Roles('admin')
|
|
654
|
+
@GET('/')
|
|
655
|
+
public testMethod() {}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const executionContext = new ExecutionContextImpl(
|
|
659
|
+
ctx,
|
|
660
|
+
TestController,
|
|
661
|
+
'testMethod',
|
|
662
|
+
TestController.prototype.testMethod,
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
expect(() => guard.canActivate(executionContext)).toThrow(
|
|
666
|
+
'Access denied: authentication required for role check',
|
|
667
|
+
);
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
describe('createRolesGuard', () => {
|
|
673
|
+
test('should create custom roles guard with matchAll option', async () => {
|
|
674
|
+
const CustomGuard = createRolesGuard({ matchAll: true });
|
|
675
|
+
const guard = new CustomGuard();
|
|
676
|
+
|
|
677
|
+
await SecurityContextHolder.runWithContext(async () => {
|
|
678
|
+
const securityContext = SecurityContextHolder.getContext();
|
|
679
|
+
securityContext.setAuthentication({
|
|
680
|
+
authenticated: true,
|
|
681
|
+
principal: { id: '1', username: 'test' },
|
|
682
|
+
credentials: null,
|
|
683
|
+
authorities: ['admin', 'user'],
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const request = new Request('http://localhost/test');
|
|
687
|
+
const ctx = new Context(request);
|
|
688
|
+
|
|
689
|
+
@Controller('/test')
|
|
690
|
+
class TestController {
|
|
691
|
+
@Roles('admin', 'user')
|
|
692
|
+
@GET('/')
|
|
693
|
+
public testMethod() {}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const executionContext = new ExecutionContextImpl(
|
|
697
|
+
ctx,
|
|
698
|
+
TestController,
|
|
699
|
+
'testMethod',
|
|
700
|
+
TestController.prototype.testMethod,
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
const result = guard.canActivate(executionContext);
|
|
704
|
+
expect(result).toBe(true);
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test('should fail matchAll when missing one role', async () => {
|
|
709
|
+
const CustomGuard = createRolesGuard({ matchAll: true });
|
|
710
|
+
const guard = new CustomGuard();
|
|
711
|
+
|
|
712
|
+
await SecurityContextHolder.runWithContext(async () => {
|
|
713
|
+
const securityContext = SecurityContextHolder.getContext();
|
|
714
|
+
securityContext.setAuthentication({
|
|
715
|
+
authenticated: true,
|
|
716
|
+
principal: { id: '1', username: 'test' },
|
|
717
|
+
credentials: null,
|
|
718
|
+
authorities: ['admin'],
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const request = new Request('http://localhost/test');
|
|
722
|
+
const ctx = new Context(request);
|
|
723
|
+
|
|
724
|
+
@Controller('/test')
|
|
725
|
+
class TestController {
|
|
726
|
+
@Roles('admin', 'superadmin')
|
|
727
|
+
@GET('/')
|
|
728
|
+
public testMethod() {}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const executionContext = new ExecutionContextImpl(
|
|
732
|
+
ctx,
|
|
733
|
+
TestController,
|
|
734
|
+
'testMethod',
|
|
735
|
+
TestController.prototype.testMethod,
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
expect(() => guard.canActivate(executionContext)).toThrow('Access denied');
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test('should use custom getRoles function', async () => {
|
|
743
|
+
const CustomGuard = createRolesGuard({
|
|
744
|
+
getRoles: (context) => {
|
|
745
|
+
const ctx = context.switchToHttp().getRequest();
|
|
746
|
+
return (ctx as any).customRoles || [];
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
const guard = new CustomGuard();
|
|
750
|
+
|
|
751
|
+
await SecurityContextHolder.runWithContext(async () => {
|
|
752
|
+
const request = new Request('http://localhost/test');
|
|
753
|
+
const ctx = new Context(request);
|
|
754
|
+
(ctx as any).customRoles = ['admin'];
|
|
755
|
+
|
|
756
|
+
@Controller('/test')
|
|
757
|
+
class TestController {
|
|
758
|
+
@Roles('admin')
|
|
759
|
+
@GET('/')
|
|
760
|
+
public testMethod() {}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const executionContext = new ExecutionContextImpl(
|
|
764
|
+
ctx,
|
|
765
|
+
TestController,
|
|
766
|
+
'testMethod',
|
|
767
|
+
TestController.prototype.testMethod,
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
const result = guard.canActivate(executionContext);
|
|
771
|
+
expect(result).toBe(true);
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|