@dangao/bun-server 1.0.0 → 1.0.3

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.
Files changed (200) hide show
  1. package/package.json +4 -2
  2. package/readme.md +163 -2
  3. package/src/auth/controller.ts +148 -0
  4. package/src/auth/decorators.ts +81 -0
  5. package/src/auth/index.ts +12 -0
  6. package/src/auth/jwt.ts +169 -0
  7. package/src/auth/oauth2.ts +244 -0
  8. package/src/auth/types.ts +248 -0
  9. package/src/cache/cache-module.ts +67 -0
  10. package/src/cache/decorators.ts +202 -0
  11. package/src/cache/index.ts +27 -0
  12. package/src/cache/service.ts +151 -0
  13. package/src/cache/types.ts +420 -0
  14. package/src/config/config-module.ts +76 -0
  15. package/src/config/index.ts +8 -0
  16. package/src/config/service.ts +93 -0
  17. package/src/config/types.ts +27 -0
  18. package/src/controller/controller.ts +251 -0
  19. package/src/controller/decorators.ts +84 -0
  20. package/src/controller/index.ts +7 -0
  21. package/src/controller/metadata.ts +27 -0
  22. package/src/controller/param-binder.ts +157 -0
  23. package/src/core/application.ts +233 -0
  24. package/src/core/context.ts +228 -0
  25. package/src/core/index.ts +4 -0
  26. package/src/core/server.ts +128 -0
  27. package/src/core/types.ts +2 -0
  28. package/src/database/connection-manager.ts +239 -0
  29. package/src/database/connection-pool.ts +322 -0
  30. package/src/database/database-extension.ts +62 -0
  31. package/src/database/database-module.ts +115 -0
  32. package/src/database/health-indicator.ts +51 -0
  33. package/src/database/index.ts +47 -0
  34. package/src/database/orm/decorators.ts +155 -0
  35. package/src/database/orm/drizzle-repository.ts +39 -0
  36. package/src/database/orm/index.ts +23 -0
  37. package/src/database/orm/repository-decorator.ts +39 -0
  38. package/src/database/orm/repository.ts +103 -0
  39. package/src/database/orm/service.ts +49 -0
  40. package/src/database/orm/transaction-decorator.ts +45 -0
  41. package/src/database/orm/transaction-interceptor.ts +243 -0
  42. package/src/database/orm/transaction-manager.ts +276 -0
  43. package/src/database/orm/transaction-types.ts +140 -0
  44. package/src/database/orm/types.ts +99 -0
  45. package/src/database/service.ts +221 -0
  46. package/src/database/types.ts +171 -0
  47. package/src/di/container.ts +398 -0
  48. package/src/di/decorators.ts +228 -0
  49. package/src/di/index.ts +4 -0
  50. package/src/di/module-registry.ts +188 -0
  51. package/src/di/module.ts +65 -0
  52. package/src/di/types.ts +67 -0
  53. package/src/error/error-codes.ts +222 -0
  54. package/src/error/filter.ts +43 -0
  55. package/src/error/handler.ts +66 -0
  56. package/src/error/http-exception.ts +115 -0
  57. package/src/error/i18n.ts +217 -0
  58. package/src/error/index.ts +16 -0
  59. package/src/extensions/index.ts +5 -0
  60. package/src/extensions/logger-extension.ts +31 -0
  61. package/src/extensions/logger-module.ts +69 -0
  62. package/src/extensions/types.ts +14 -0
  63. package/src/files/index.ts +5 -0
  64. package/src/files/static-middleware.ts +53 -0
  65. package/src/files/storage.ts +67 -0
  66. package/src/files/types.ts +33 -0
  67. package/src/files/upload-middleware.ts +45 -0
  68. package/src/health/controller.ts +76 -0
  69. package/src/health/health-module.ts +51 -0
  70. package/src/health/index.ts +12 -0
  71. package/src/health/types.ts +28 -0
  72. package/src/index.ts +270 -0
  73. package/src/metrics/collector.ts +209 -0
  74. package/src/metrics/controller.ts +40 -0
  75. package/src/metrics/index.ts +15 -0
  76. package/src/metrics/metrics-module.ts +58 -0
  77. package/src/metrics/middleware.ts +46 -0
  78. package/src/metrics/prometheus.ts +79 -0
  79. package/src/metrics/types.ts +103 -0
  80. package/src/middleware/builtin/cors.ts +60 -0
  81. package/src/middleware/builtin/error-handler.ts +90 -0
  82. package/src/middleware/builtin/file-upload.ts +42 -0
  83. package/src/middleware/builtin/index.ts +14 -0
  84. package/src/middleware/builtin/logger.ts +91 -0
  85. package/src/middleware/builtin/rate-limit.ts +252 -0
  86. package/src/middleware/builtin/static-file.ts +88 -0
  87. package/src/middleware/decorators.ts +91 -0
  88. package/src/middleware/index.ts +11 -0
  89. package/src/middleware/middleware.ts +13 -0
  90. package/src/middleware/pipeline.ts +93 -0
  91. package/src/queue/decorators.ts +110 -0
  92. package/src/queue/index.ts +26 -0
  93. package/src/queue/queue-module.ts +64 -0
  94. package/src/queue/service.ts +302 -0
  95. package/src/queue/types.ts +341 -0
  96. package/src/request/body-parser.ts +133 -0
  97. package/src/request/file-handler.ts +46 -0
  98. package/src/request/index.ts +5 -0
  99. package/src/request/request.ts +107 -0
  100. package/src/request/response.ts +150 -0
  101. package/src/router/decorators.ts +122 -0
  102. package/src/router/index.ts +6 -0
  103. package/src/router/registry.ts +98 -0
  104. package/src/router/route.ts +140 -0
  105. package/src/router/router.ts +241 -0
  106. package/src/router/types.ts +27 -0
  107. package/src/security/access-decision-manager.ts +34 -0
  108. package/src/security/authentication-manager.ts +47 -0
  109. package/src/security/context.ts +92 -0
  110. package/src/security/filter.ts +162 -0
  111. package/src/security/index.ts +8 -0
  112. package/src/security/providers/index.ts +3 -0
  113. package/src/security/providers/jwt-provider.ts +60 -0
  114. package/src/security/providers/oauth2-provider.ts +70 -0
  115. package/src/security/security-module.ts +145 -0
  116. package/src/security/types.ts +165 -0
  117. package/src/session/decorators.ts +45 -0
  118. package/src/session/index.ts +19 -0
  119. package/src/session/middleware.ts +143 -0
  120. package/src/session/service.ts +218 -0
  121. package/src/session/session-module.ts +69 -0
  122. package/src/session/types.ts +373 -0
  123. package/src/swagger/decorators.ts +133 -0
  124. package/src/swagger/generator.ts +234 -0
  125. package/src/swagger/index.ts +7 -0
  126. package/src/swagger/swagger-extension.ts +41 -0
  127. package/src/swagger/swagger-module.ts +83 -0
  128. package/src/swagger/types.ts +188 -0
  129. package/src/swagger/ui.ts +98 -0
  130. package/src/testing/harness.ts +96 -0
  131. package/src/validation/decorators.ts +95 -0
  132. package/src/validation/errors.ts +28 -0
  133. package/src/validation/index.ts +14 -0
  134. package/src/validation/types.ts +35 -0
  135. package/src/validation/validator.ts +63 -0
  136. package/src/websocket/decorators.ts +51 -0
  137. package/src/websocket/index.ts +12 -0
  138. package/src/websocket/registry.ts +133 -0
  139. package/tests/cache/cache-module.test.ts +212 -0
  140. package/tests/config/config-module.test.ts +151 -0
  141. package/tests/controller/controller.test.ts +189 -0
  142. package/tests/core/application.test.ts +57 -0
  143. package/tests/core/context-body.test.ts +44 -0
  144. package/tests/core/context.test.ts +86 -0
  145. package/tests/core/edge-cases.test.ts +432 -0
  146. package/tests/database/database-module.test.ts +385 -0
  147. package/tests/database/orm.test.ts +164 -0
  148. package/tests/database/postgres-mysql-integration.test.ts +395 -0
  149. package/tests/database/transaction.test.ts +238 -0
  150. package/tests/di/container.test.ts +264 -0
  151. package/tests/di/module.test.ts +128 -0
  152. package/tests/error/error-codes.test.ts +121 -0
  153. package/tests/error/error-handler.test.ts +68 -0
  154. package/tests/error/error-handling.test.ts +254 -0
  155. package/tests/error/http-exception.test.ts +37 -0
  156. package/tests/error/i18n-integration.test.ts +175 -0
  157. package/tests/extensions/logger-extension.test.ts +40 -0
  158. package/tests/files/static-middleware.test.ts +67 -0
  159. package/tests/files/upload-middleware.test.ts +43 -0
  160. package/tests/health/health-module.test.ts +116 -0
  161. package/tests/integration/application-router.test.ts +85 -0
  162. package/tests/integration/body-parsing.test.ts +88 -0
  163. package/tests/integration/cache-e2e.test.ts +114 -0
  164. package/tests/integration/oauth2-e2e.test.ts +615 -0
  165. package/tests/integration/session-e2e.test.ts +207 -0
  166. package/tests/metrics/metrics-module.test.ts +178 -0
  167. package/tests/middleware/builtin.test.ts +206 -0
  168. package/tests/middleware/file-upload.test.ts +41 -0
  169. package/tests/middleware/middleware.test.ts +120 -0
  170. package/tests/middleware/pipeline.test.ts +72 -0
  171. package/tests/middleware/rate-limit.test.ts +314 -0
  172. package/tests/middleware/static-file.test.ts +62 -0
  173. package/tests/perf/harness.test.ts +48 -0
  174. package/tests/perf/optimization.test.ts +183 -0
  175. package/tests/perf/regression.test.ts +120 -0
  176. package/tests/queue/queue-module.test.ts +217 -0
  177. package/tests/request/body-parser.test.ts +96 -0
  178. package/tests/request/response.test.ts +99 -0
  179. package/tests/router/decorators.test.ts +48 -0
  180. package/tests/router/registry.test.ts +51 -0
  181. package/tests/router/route.test.ts +71 -0
  182. package/tests/router/router-normalization.test.ts +106 -0
  183. package/tests/router/router.test.ts +133 -0
  184. package/tests/security/access-decision-manager.test.ts +84 -0
  185. package/tests/security/authentication-manager.test.ts +81 -0
  186. package/tests/security/context.test.ts +302 -0
  187. package/tests/security/filter.test.ts +225 -0
  188. package/tests/security/jwt-provider.test.ts +106 -0
  189. package/tests/security/oauth2-provider.test.ts +269 -0
  190. package/tests/security/security-module.test.ts +143 -0
  191. package/tests/session/session-module.test.ts +307 -0
  192. package/tests/stress/di-stress.test.ts +30 -0
  193. package/tests/swagger/decorators.test.ts +153 -0
  194. package/tests/swagger/generator.test.ts +202 -0
  195. package/tests/swagger/swagger-extension.test.ts +72 -0
  196. package/tests/swagger/swagger-module.test.ts +79 -0
  197. package/tests/utils/test-port.ts +10 -0
  198. package/tests/validation/controller-validation.test.ts +64 -0
  199. package/tests/validation/validation.test.ts +42 -0
  200. package/tests/websocket/gateway.test.ts +68 -0
@@ -0,0 +1,302 @@
1
+ import { describe, expect, test, beforeEach } from 'bun:test';
2
+ import {
3
+ SecurityContextHolder,
4
+ SecurityContextImpl,
5
+ } from '../../src/security/context';
6
+ import type { Authentication, Principal } from '../../src/security/types';
7
+
8
+ describe('SecurityContext', () => {
9
+ let context: SecurityContextImpl;
10
+
11
+ beforeEach(() => {
12
+ context = new SecurityContextImpl();
13
+ SecurityContextHolder.clearContext();
14
+ });
15
+
16
+ test('should initialize with null authentication', () => {
17
+ expect(context.authentication).toBeNull();
18
+ expect(context.isAuthenticated()).toBe(false);
19
+ expect(context.getPrincipal()).toBeNull();
20
+ expect(context.getAuthorities()).toEqual([]);
21
+ });
22
+
23
+ test('should set and get authentication', () => {
24
+ const principal: Principal = {
25
+ id: 'user-1',
26
+ username: 'testuser',
27
+ roles: ['user'],
28
+ };
29
+
30
+ const authentication: Authentication = {
31
+ authenticated: true,
32
+ principal,
33
+ credentials: { type: 'jwt', data: 'token' },
34
+ authorities: ['user'],
35
+ };
36
+
37
+ context.setAuthentication(authentication);
38
+
39
+ expect(context.authentication).toBe(authentication);
40
+ expect(context.isAuthenticated()).toBe(true);
41
+ expect(context.getPrincipal()).toBe(principal);
42
+ expect(context.getAuthorities()).toEqual(['user']);
43
+ });
44
+
45
+ test('should clear authentication', () => {
46
+ const authentication: Authentication = {
47
+ authenticated: true,
48
+ principal: { id: 'user-1', username: 'test', roles: [] },
49
+ credentials: { type: 'jwt', data: 'token' },
50
+ authorities: [],
51
+ };
52
+
53
+ context.setAuthentication(authentication);
54
+ context.clear();
55
+
56
+ expect(context.authentication).toBeNull();
57
+ expect(context.isAuthenticated()).toBe(false);
58
+ });
59
+
60
+ test('should handle null authentication', () => {
61
+ context.setAuthentication(null);
62
+ expect(context.isAuthenticated()).toBe(false);
63
+ expect(context.getPrincipal()).toBeNull();
64
+ });
65
+ });
66
+
67
+ describe('SecurityContextHolder', () => {
68
+ beforeEach(() => {
69
+ SecurityContextHolder.clearContext();
70
+ });
71
+
72
+ test('should get context instance', () => {
73
+ const context = SecurityContextHolder.getContext();
74
+ expect(context).toBeInstanceOf(SecurityContextImpl);
75
+ });
76
+
77
+ test('should return same context instance', () => {
78
+ const context1 = SecurityContextHolder.getContext();
79
+ const context2 = SecurityContextHolder.getContext();
80
+ expect(context1).toBe(context2);
81
+ });
82
+
83
+ test('should clear context', () => {
84
+ const context1 = SecurityContextHolder.getContext();
85
+ SecurityContextHolder.clearContext();
86
+ const context2 = SecurityContextHolder.getContext();
87
+ // 由于实现方式,可能返回新实例或相同实例
88
+ expect(context2).toBeInstanceOf(SecurityContextImpl);
89
+ });
90
+
91
+ test('should isolate contexts in runWithContext', async () => {
92
+ const results: Map<number, string> = new Map();
93
+
94
+ // 模拟多个并发请求,每个请求设置不同的认证信息
95
+ const promises = Array.from({ length: 10 }, (_, i) => {
96
+ const userId = `user-${i}`;
97
+ return SecurityContextHolder.runWithContext(async () => {
98
+ const context = SecurityContextHolder.getContext();
99
+ const authentication: Authentication = {
100
+ authenticated: true,
101
+ principal: {
102
+ id: userId,
103
+ username: `user${i}`,
104
+ roles: ['user'],
105
+ },
106
+ credentials: { type: 'jwt', data: `token-${i}` },
107
+ authorities: ['user'],
108
+ };
109
+ context.setAuthentication(authentication);
110
+
111
+ // 模拟异步操作
112
+ await new Promise((resolve) => setTimeout(resolve, Math.random() * 10));
113
+
114
+ // 验证上下文隔离:每个请求应该看到自己的认证信息
115
+ const currentContext = SecurityContextHolder.getContext();
116
+ const principal = currentContext.getPrincipal();
117
+
118
+ expect(currentContext.isAuthenticated()).toBe(true);
119
+ expect(principal?.id).toBe(userId);
120
+ expect(principal?.username).toBe(`user${i}`);
121
+
122
+ // 存储结果,使用索引作为键
123
+ results.set(i, principal?.id || '');
124
+ });
125
+ });
126
+
127
+ await Promise.all(promises);
128
+
129
+ // 验证所有请求都正确隔离(不依赖顺序)
130
+ expect(results.size).toBe(10);
131
+ for (let i = 0; i < 10; i++) {
132
+ expect(results.get(i)).toBe(`user-${i}`);
133
+ }
134
+ });
135
+
136
+ test('should not leak authentication between concurrent requests', async () => {
137
+ const user1Auth: Authentication = {
138
+ authenticated: true,
139
+ principal: { id: 'user-1', username: 'alice', roles: ['user'] },
140
+ credentials: { type: 'jwt', data: 'token1' },
141
+ authorities: ['user'],
142
+ };
143
+
144
+ const user2Auth: Authentication = {
145
+ authenticated: true,
146
+ principal: { id: 'user-2', username: 'bob', roles: ['admin'] },
147
+ credentials: { type: 'jwt', data: 'token2' },
148
+ authorities: ['admin'],
149
+ };
150
+
151
+ // 并发执行两个请求
152
+ const [result1, result2] = await Promise.all([
153
+ SecurityContextHolder.runWithContext(async () => {
154
+ const context = SecurityContextHolder.getContext();
155
+ context.setAuthentication(user1Auth);
156
+
157
+ // 模拟异步操作
158
+ await new Promise((resolve) => setTimeout(resolve, 50));
159
+
160
+ const currentContext = SecurityContextHolder.getContext();
161
+ return {
162
+ userId: currentContext.getPrincipal()?.id,
163
+ username: currentContext.getPrincipal()?.username,
164
+ roles: currentContext.getPrincipal()?.roles,
165
+ };
166
+ }),
167
+ SecurityContextHolder.runWithContext(async () => {
168
+ const context = SecurityContextHolder.getContext();
169
+ context.setAuthentication(user2Auth);
170
+
171
+ // 模拟异步操作
172
+ await new Promise((resolve) => setTimeout(resolve, 50));
173
+
174
+ const currentContext = SecurityContextHolder.getContext();
175
+ return {
176
+ userId: currentContext.getPrincipal()?.id,
177
+ username: currentContext.getPrincipal()?.username,
178
+ roles: currentContext.getPrincipal()?.roles,
179
+ };
180
+ }),
181
+ ]);
182
+
183
+ // 验证两个请求的认证信息没有互相干扰
184
+ expect(result1.userId).toBe('user-1');
185
+ expect(result1.username).toBe('alice');
186
+ expect(result1.roles).toEqual(['user']);
187
+
188
+ expect(result2.userId).toBe('user-2');
189
+ expect(result2.username).toBe('bob');
190
+ expect(result2.roles).toEqual(['admin']);
191
+ });
192
+
193
+ test('should maintain context isolation with nested runWithContext', async () => {
194
+ let outerUserId: string | undefined;
195
+ let innerUserId: string | undefined;
196
+
197
+ await SecurityContextHolder.runWithContext(async () => {
198
+ const outerContext = SecurityContextHolder.getContext();
199
+ const outerAuth: Authentication = {
200
+ authenticated: true,
201
+ principal: { id: 'outer-user', username: 'outer', roles: [] },
202
+ credentials: { type: 'jwt', data: 'outer-token' },
203
+ authorities: [],
204
+ };
205
+ outerContext.setAuthentication(outerAuth);
206
+
207
+ await SecurityContextHolder.runWithContext(async () => {
208
+ const innerContext = SecurityContextHolder.getContext();
209
+ const innerAuth: Authentication = {
210
+ authenticated: true,
211
+ principal: { id: 'inner-user', username: 'inner', roles: [] },
212
+ credentials: { type: 'jwt', data: 'inner-token' },
213
+ authorities: [],
214
+ };
215
+ innerContext.setAuthentication(innerAuth);
216
+
217
+ // 内层应该看到内层的认证信息
218
+ innerUserId = SecurityContextHolder.getContext().getPrincipal()?.id;
219
+ });
220
+
221
+ // 外层应该仍然看到外层的认证信息
222
+ outerUserId = SecurityContextHolder.getContext().getPrincipal()?.id;
223
+ });
224
+
225
+ expect(outerUserId).toBe('outer-user');
226
+ expect(innerUserId).toBe('inner-user');
227
+ });
228
+
229
+ test('should handle high concurrency without context pollution', async () => {
230
+ const concurrency = 100;
231
+ const results: Map<number, { success: boolean; userId: string }> = new Map();
232
+
233
+ const promises = Array.from({ length: concurrency }, (_, i) => {
234
+ const userId = `user-${i}`;
235
+ return SecurityContextHolder.runWithContext(async () => {
236
+ const context = SecurityContextHolder.getContext();
237
+ const authentication: Authentication = {
238
+ authenticated: true,
239
+ principal: {
240
+ id: userId,
241
+ username: `user${i}`,
242
+ roles: ['user'],
243
+ },
244
+ credentials: { type: 'jwt', data: `token-${i}` },
245
+ authorities: ['user'],
246
+ };
247
+ context.setAuthentication(authentication);
248
+
249
+ // 模拟一些异步操作
250
+ await new Promise((resolve) => setTimeout(resolve, Math.random() * 5));
251
+
252
+ // 验证上下文隔离
253
+ const currentContext = SecurityContextHolder.getContext();
254
+ const principal = currentContext.getPrincipal();
255
+ const success = principal?.id === userId && currentContext.isAuthenticated();
256
+
257
+ results.set(i, {
258
+ success,
259
+ userId: principal?.id || '',
260
+ });
261
+
262
+ return { index: i, success };
263
+ });
264
+ });
265
+
266
+ const allResults = await Promise.all(promises);
267
+
268
+ // 验证所有请求都成功且没有上下文污染
269
+ expect(allResults.every((r) => r.success === true)).toBe(true);
270
+ expect(results.size).toBe(concurrency);
271
+ expect(Array.from(results.values()).every((r) => r.success)).toBe(true);
272
+
273
+ // 验证每个结果都有正确的用户 ID(不依赖顺序)
274
+ for (let i = 0; i < concurrency; i++) {
275
+ const result = results.get(i);
276
+ expect(result).toBeDefined();
277
+ expect(result?.userId).toBe(`user-${i}`);
278
+ expect(result?.success).toBe(true);
279
+ }
280
+ });
281
+
282
+ test('should clear context after runWithContext completes', async () => {
283
+ await SecurityContextHolder.runWithContext(async () => {
284
+ const context = SecurityContextHolder.getContext();
285
+ const authentication: Authentication = {
286
+ authenticated: true,
287
+ principal: { id: 'temp-user', username: 'temp', roles: [] },
288
+ credentials: { type: 'jwt', data: 'temp-token' },
289
+ authorities: [],
290
+ };
291
+ context.setAuthentication(authentication);
292
+ expect(context.isAuthenticated()).toBe(true);
293
+ });
294
+
295
+ // runWithContext 完成后,外部上下文应该不受影响
296
+ const externalContext = SecurityContextHolder.getContext();
297
+ // 由于 runWithContext 使用独立的存储上下文,外部可能没有认证信息
298
+ // 这是预期的行为,因为每个 runWithContext 都有独立的上下文
299
+ expect(externalContext).toBeInstanceOf(SecurityContextImpl);
300
+ });
301
+ });
302
+
@@ -0,0 +1,225 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
2
+ import { createSecurityFilter } from '../../src/security/filter';
3
+ import { AuthenticationManager } from '../../src/security/authentication-manager';
4
+ import { JwtAuthenticationProvider } from '../../src/security/providers/jwt-provider';
5
+ import { JWTUtil } from '../../src/auth/jwt';
6
+ import { Context } from '../../src/core/context';
7
+ import { Controller } from '../../src/controller';
8
+ import { GET } from '../../src/router/decorators';
9
+ import { Auth } from '../../src/auth/decorators';
10
+ import { UnauthorizedException, ForbiddenException } from '../../src/error/http-exception';
11
+ import { SecurityContextHolder } from '../../src/security/context';
12
+
13
+ describe('SecurityFilter', () => {
14
+ let jwtUtil: JWTUtil;
15
+ let authManager: AuthenticationManager;
16
+ let filter: ReturnType<typeof createSecurityFilter>;
17
+
18
+ beforeEach(() => {
19
+ jwtUtil = new JWTUtil({
20
+ secret: 'test-secret-key',
21
+ accessTokenExpiresIn: 3600,
22
+ });
23
+ authManager = new AuthenticationManager();
24
+ authManager.registerProvider(new JwtAuthenticationProvider(jwtUtil));
25
+ SecurityContextHolder.clearContext();
26
+ });
27
+
28
+ afterEach(() => {
29
+ SecurityContextHolder.clearContext();
30
+ });
31
+
32
+ test('should allow access to excluded paths', async () => {
33
+ filter = createSecurityFilter({
34
+ authenticationManager: authManager,
35
+ excludePaths: ['/public'],
36
+ });
37
+
38
+ // filter 使用 ctx.request.url.split('?')[0] 来获取路径
39
+ // 所以需要包含协议和主机
40
+ const request = new Request('http://localhost/public');
41
+ const context = new Context(request);
42
+ let nextCalled = false;
43
+
44
+ const next = async () => {
45
+ nextCalled = true;
46
+ return context.createResponse({ ok: true });
47
+ };
48
+
49
+ const result = await filter(context, next);
50
+
51
+ expect(nextCalled).toBe(true);
52
+ expect(result).toBeDefined();
53
+ });
54
+
55
+ test('should authenticate with valid token', async () => {
56
+ filter = createSecurityFilter({
57
+ authenticationManager: authManager,
58
+ excludePaths: [],
59
+ defaultAuthRequired: false,
60
+ });
61
+
62
+ const token = jwtUtil.generateAccessToken({
63
+ sub: 'user-1',
64
+ username: 'testuser',
65
+ roles: ['user'],
66
+ });
67
+
68
+ const request = new Request('http://localhost/api/test', {
69
+ headers: { Authorization: `Bearer ${token}` },
70
+ });
71
+ const context = new Context(request);
72
+ let nextCalled = false;
73
+
74
+ const next = async () => {
75
+ nextCalled = true;
76
+ return context.createResponse({ ok: true });
77
+ };
78
+
79
+ await filter(context, next);
80
+
81
+ expect(nextCalled).toBe(true);
82
+ expect((context as any).auth.isAuthenticated).toBe(true);
83
+ expect((context as any).auth.user.id).toBe('user-1');
84
+ });
85
+
86
+ test('should throw UnauthorizedException when auth required but no token', async () => {
87
+ @Controller('/api')
88
+ class TestController {
89
+ @GET('/protected')
90
+ @Auth()
91
+ public protected() {
92
+ return { ok: true };
93
+ }
94
+ }
95
+
96
+ filter = createSecurityFilter({
97
+ authenticationManager: authManager,
98
+ excludePaths: [],
99
+ defaultAuthRequired: false,
100
+ });
101
+
102
+ const request = new Request('http://localhost/api/protected');
103
+ const context = new Context(request);
104
+ (context as any).routeHandler = {
105
+ controller: TestController.prototype,
106
+ method: 'protected',
107
+ };
108
+
109
+ const next = async () => context.createResponse({ ok: true });
110
+
111
+ await expect(filter(context, next)).rejects.toThrow(UnauthorizedException);
112
+ });
113
+
114
+ test('should throw ForbiddenException when user lacks required role', async () => {
115
+ @Controller('/api')
116
+ class TestController {
117
+ @GET('/admin')
118
+ @Auth({ roles: ['admin'] })
119
+ public admin() {
120
+ return { ok: true };
121
+ }
122
+ }
123
+
124
+ filter = createSecurityFilter({
125
+ authenticationManager: authManager,
126
+ excludePaths: [],
127
+ defaultAuthRequired: false,
128
+ });
129
+
130
+ const token = jwtUtil.generateAccessToken({
131
+ sub: 'user-1',
132
+ username: 'testuser',
133
+ roles: ['user'], // 只有 user 角色,没有 admin
134
+ });
135
+
136
+ const request = new Request('http://localhost/api/admin', {
137
+ headers: { Authorization: `Bearer ${token}` },
138
+ });
139
+ const context = new Context(request);
140
+ // routeHandler 需要在认证之前设置,因为 filter 会检查它
141
+ // 使用原型来获取元数据
142
+ (context as any).routeHandler = {
143
+ controller: TestController.prototype,
144
+ method: 'admin',
145
+ };
146
+
147
+ const next = async () => context.createResponse({ ok: true });
148
+
149
+ await expect(filter(context, next)).rejects.toThrow(ForbiddenException);
150
+ });
151
+
152
+ test('should allow access when user has required role', async () => {
153
+ @Controller('/api')
154
+ class TestController {
155
+ @GET('/admin')
156
+ @Auth({ roles: ['admin'] })
157
+ public admin() {
158
+ return { ok: true };
159
+ }
160
+ }
161
+
162
+ filter = createSecurityFilter({
163
+ authenticationManager: authManager,
164
+ excludePaths: [],
165
+ defaultAuthRequired: false,
166
+ });
167
+
168
+ const token = jwtUtil.generateAccessToken({
169
+ sub: 'user-1',
170
+ username: 'admin',
171
+ roles: ['admin'],
172
+ });
173
+
174
+ const request = new Request('http://localhost/api/admin', {
175
+ headers: { Authorization: `Bearer ${token}` },
176
+ });
177
+ const context = new Context(request);
178
+ (context as any).routeHandler = {
179
+ controller: TestController.prototype,
180
+ method: 'admin',
181
+ };
182
+ let nextCalled = false;
183
+
184
+ const next = async () => {
185
+ nextCalled = true;
186
+ return context.createResponse({ ok: true });
187
+ };
188
+
189
+ await filter(context, next);
190
+
191
+ expect(nextCalled).toBe(true);
192
+ });
193
+
194
+ test('should use custom token extractor', async () => {
195
+ filter = createSecurityFilter({
196
+ authenticationManager: authManager,
197
+ excludePaths: [],
198
+ defaultAuthRequired: false,
199
+ extractToken: (ctx) => ctx.getHeader('x-custom-token'),
200
+ });
201
+
202
+ const token = jwtUtil.generateAccessToken({
203
+ sub: 'user-1',
204
+ username: 'testuser',
205
+ roles: ['user'],
206
+ });
207
+
208
+ const request = new Request('http://localhost/api/test', {
209
+ headers: { 'x-custom-token': token },
210
+ });
211
+ const context = new Context(request);
212
+ let nextCalled = false;
213
+
214
+ const next = async () => {
215
+ nextCalled = true;
216
+ return context.createResponse({ ok: true });
217
+ };
218
+
219
+ await filter(context, next);
220
+
221
+ expect(nextCalled).toBe(true);
222
+ expect((context as any).auth.isAuthenticated).toBe(true);
223
+ });
224
+ });
225
+
@@ -0,0 +1,106 @@
1
+ import { describe, expect, test, beforeEach } from 'bun:test';
2
+ import { JwtAuthenticationProvider } from '../../src/security/providers/jwt-provider';
3
+ import { JWTUtil } from '../../src/auth/jwt';
4
+
5
+ describe('JwtAuthenticationProvider', () => {
6
+ let jwtUtil: JWTUtil;
7
+ let provider: JwtAuthenticationProvider;
8
+
9
+ beforeEach(() => {
10
+ jwtUtil = new JWTUtil({
11
+ secret: 'test-secret-key',
12
+ accessTokenExpiresIn: 3600,
13
+ });
14
+ provider = new JwtAuthenticationProvider(jwtUtil);
15
+ });
16
+
17
+ test('should support jwt and bearer types', () => {
18
+ expect(provider.supports('jwt')).toBe(true);
19
+ expect(provider.supports('bearer')).toBe(true);
20
+ expect(provider.supports('JWT')).toBe(true);
21
+ expect(provider.supports('BEARER')).toBe(true);
22
+ expect(provider.supports('oauth2')).toBe(false);
23
+ });
24
+
25
+ test('should authenticate with valid JWT token', async () => {
26
+ const token = jwtUtil.generateAccessToken({
27
+ sub: 'user-1',
28
+ username: 'testuser',
29
+ roles: ['user'],
30
+ });
31
+
32
+ const authentication = await provider.authenticate({
33
+ principal: '',
34
+ credentials: token,
35
+ type: 'jwt',
36
+ });
37
+
38
+ expect(authentication).not.toBeNull();
39
+ expect(authentication?.authenticated).toBe(true);
40
+ expect(authentication?.principal.id).toBe('user-1');
41
+ expect(authentication?.principal.username).toBe('testuser');
42
+ expect(authentication?.principal.roles).toEqual(['user']);
43
+ expect(authentication?.authorities).toEqual(['user']);
44
+ });
45
+
46
+ test('should return null for invalid token', async () => {
47
+ const authentication = await provider.authenticate({
48
+ principal: '',
49
+ credentials: 'invalid-token',
50
+ type: 'jwt',
51
+ });
52
+
53
+ expect(authentication).toBeNull();
54
+ });
55
+
56
+ test('should return null for missing token', async () => {
57
+ const authentication = await provider.authenticate({
58
+ principal: '',
59
+ credentials: '',
60
+ type: 'jwt',
61
+ });
62
+
63
+ expect(authentication).toBeNull();
64
+ });
65
+
66
+ test('should return null for expired token', async () => {
67
+ const expiredJwtUtil = new JWTUtil({
68
+ secret: 'test-secret-key',
69
+ accessTokenExpiresIn: -1, // 已过期
70
+ });
71
+ const expiredProvider = new JwtAuthenticationProvider(expiredJwtUtil);
72
+ const token = expiredJwtUtil.generateAccessToken({
73
+ sub: 'user-1',
74
+ username: 'testuser',
75
+ });
76
+
77
+ // 等待一下确保过期
78
+ await new Promise((resolve) => setTimeout(resolve, 100));
79
+
80
+ const authentication = await expiredProvider.authenticate({
81
+ principal: '',
82
+ credentials: token,
83
+ type: 'jwt',
84
+ });
85
+
86
+ expect(authentication).toBeNull();
87
+ });
88
+
89
+ test('should handle token without roles', async () => {
90
+ const token = jwtUtil.generateAccessToken({
91
+ sub: 'user-1',
92
+ username: 'testuser',
93
+ });
94
+
95
+ const authentication = await provider.authenticate({
96
+ principal: '',
97
+ credentials: token,
98
+ type: 'jwt',
99
+ });
100
+
101
+ expect(authentication).not.toBeNull();
102
+ expect(authentication?.principal.roles).toEqual([]);
103
+ expect(authentication?.authorities).toEqual([]);
104
+ });
105
+ });
106
+