@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,615 @@
1
+ import 'reflect-metadata';
2
+ import { describe, expect, test, afterEach, beforeEach } from 'bun:test';
3
+ import { Application } from '../../src/core/application';
4
+ import { SecurityModule } from '../../src/security/security-module';
5
+ import { Controller } from '../../src/controller';
6
+ import { GET } from '../../src/router/decorators';
7
+ import { Auth } from '../../src/auth/decorators';
8
+ import { RouteRegistry } from '../../src/router/registry';
9
+ import { ControllerRegistry } from '../../src/controller/controller';
10
+ import { ModuleRegistry } from '../../src/di/module-registry';
11
+ import { SecurityContextHolder } from '../../src/security/context';
12
+ import { MODULE_METADATA_KEY } from '../../src/di/module';
13
+ import { getTestPort } from '../utils/test-port';
14
+
15
+ /**
16
+ * 测试用的受保护控制器
17
+ */
18
+ @Controller('/api/protected')
19
+ class ProtectedController {
20
+ @GET('/profile')
21
+ @Auth()
22
+ public getProfile() {
23
+ const context = SecurityContextHolder.getContext();
24
+ const principal = context.getPrincipal();
25
+ return {
26
+ id: principal?.id,
27
+ username: principal?.username,
28
+ roles: principal?.roles,
29
+ };
30
+ }
31
+
32
+ @GET('/admin')
33
+ @Auth({ roles: ['admin'] })
34
+ public getAdmin() {
35
+ return { message: 'Admin access granted' };
36
+ }
37
+ }
38
+
39
+ describe('OAuth2 E2E Integration Tests', () => {
40
+ let app: Application;
41
+ let port: number;
42
+ const baseUrl = () => `http://localhost:${port}`;
43
+
44
+ const oauth2Client = {
45
+ clientId: 'test-client',
46
+ clientSecret: 'test-secret',
47
+ redirectUris: ['http://localhost:3000/callback'],
48
+ grantTypes: ['authorization_code', 'refresh_token'] as ('authorization_code' | 'refresh_token')[],
49
+ };
50
+
51
+ beforeEach(() => {
52
+ port = getTestPort();
53
+ SecurityContextHolder.clearContext();
54
+ // 清除 SecurityModule 元数据,避免测试间污染
55
+ Reflect.deleteMetadata(MODULE_METADATA_KEY, SecurityModule);
56
+ });
57
+
58
+ afterEach(async () => {
59
+ if (app) {
60
+ await app.stop();
61
+ }
62
+ RouteRegistry.getInstance().clear();
63
+ ControllerRegistry.getInstance().clear();
64
+ ModuleRegistry.getInstance().clear();
65
+ SecurityContextHolder.clearContext();
66
+ // 清除 SecurityModule 元数据
67
+ Reflect.deleteMetadata(MODULE_METADATA_KEY, SecurityModule);
68
+ });
69
+
70
+ test('should complete full OAuth2 authorization code flow', async () => {
71
+ // 1. 创建应用并注册 SecurityModule
72
+ app = new Application({ port });
73
+ SecurityModule.forRoot({
74
+ jwt: {
75
+ secret: 'test-secret-key',
76
+ accessTokenExpiresIn: 3600,
77
+ },
78
+ oauth2Clients: [oauth2Client],
79
+ enableOAuth2Endpoints: true,
80
+ });
81
+ app.registerModule(SecurityModule);
82
+ app.registerController(ProtectedController);
83
+ await app.listen();
84
+
85
+ // 2. 请求授权端点
86
+ const authorizeUrl = new URL(`${baseUrl()}/oauth2/authorize`);
87
+ authorizeUrl.searchParams.set('client_id', oauth2Client.clientId);
88
+ authorizeUrl.searchParams.set('redirect_uri', oauth2Client.redirectUris[0]);
89
+ authorizeUrl.searchParams.set('response_type', 'code');
90
+ authorizeUrl.searchParams.set('state', 'test-state');
91
+
92
+ const authorizeResponse = await fetch(authorizeUrl.toString(), {
93
+ redirect: 'manual', // 不自动跟随重定向
94
+ });
95
+
96
+ // 3. 验证授权响应(应该是重定向)
97
+ expect(authorizeResponse.status).toBe(302);
98
+ const location = authorizeResponse.headers.get('location');
99
+ expect(location).toBeDefined();
100
+ expect(location).not.toBeNull();
101
+ expect(location).toContain('code=');
102
+ expect(location).toContain('state=test-state');
103
+
104
+ // 4. 从重定向 URL 中提取授权码
105
+ const redirectUrl = new URL(location!);
106
+ const code = redirectUrl.searchParams.get('code');
107
+ expect(code).toBeDefined();
108
+
109
+ // 5. 使用授权码交换访问令牌
110
+ const tokenResponse = await fetch(`${baseUrl()}/oauth2/token`, {
111
+ method: 'POST',
112
+ headers: {
113
+ 'Content-Type': 'application/json',
114
+ },
115
+ body: JSON.stringify({
116
+ grant_type: 'authorization_code',
117
+ code: code,
118
+ client_id: oauth2Client.clientId,
119
+ client_secret: oauth2Client.clientSecret,
120
+ redirect_uri: oauth2Client.redirectUris[0],
121
+ }),
122
+ });
123
+
124
+ expect(tokenResponse.status).toBe(200);
125
+ const tokenData = (await tokenResponse.json()) as {
126
+ accessToken: string;
127
+ tokenType: string;
128
+ refreshToken?: string;
129
+ expiresIn: number;
130
+ };
131
+ expect(tokenData.accessToken).toBeDefined();
132
+ expect(tokenData.tokenType).toBe('Bearer');
133
+ expect(tokenData.refreshToken).toBeDefined();
134
+ expect(tokenData.expiresIn).toBe(3600);
135
+
136
+ // 6. 使用访问令牌访问受保护资源
137
+ const profileResponse = await fetch(`${baseUrl()}/api/protected/profile`, {
138
+ headers: {
139
+ Authorization: `Bearer ${tokenData.accessToken}`,
140
+ },
141
+ });
142
+
143
+ expect(profileResponse.status).toBe(200);
144
+ const profileData = (await profileResponse.json()) as {
145
+ id: string;
146
+ username?: string;
147
+ roles?: string[];
148
+ };
149
+ expect(profileData.id).toBe('user-1');
150
+ expect(profileData.username).toBeDefined();
151
+ });
152
+
153
+ test('should handle refresh token flow', async () => {
154
+ app = new Application({ port });
155
+ SecurityModule.forRoot({
156
+ jwt: {
157
+ secret: 'test-secret-key',
158
+ accessTokenExpiresIn: 3600,
159
+ refreshTokenExpiresIn: 86400,
160
+ },
161
+ oauth2Clients: [oauth2Client],
162
+ enableOAuth2Endpoints: true,
163
+ });
164
+ app.registerModule(SecurityModule);
165
+ app.registerController(ProtectedController);
166
+ await app.listen();
167
+
168
+ // 1. 获取初始访问令牌和刷新令牌
169
+ const authorizeUrl = new URL(`${baseUrl()}/oauth2/authorize`);
170
+ authorizeUrl.searchParams.set('client_id', oauth2Client.clientId);
171
+ authorizeUrl.searchParams.set('redirect_uri', oauth2Client.redirectUris[0]);
172
+ authorizeUrl.searchParams.set('response_type', 'code');
173
+
174
+ const authorizeResponse = await fetch(authorizeUrl.toString(), {
175
+ redirect: 'manual',
176
+ });
177
+ expect(authorizeResponse.status).toBe(302);
178
+ const location = authorizeResponse.headers.get('location');
179
+ expect(location).not.toBeNull();
180
+ if (!location) {
181
+ throw new Error('Location header is null');
182
+ }
183
+ const code = new URL(location!).searchParams.get('code');
184
+ expect(code).toBeDefined();
185
+
186
+ const tokenResponse = await fetch(`${baseUrl()}/oauth2/token`, {
187
+ method: 'POST',
188
+ headers: { 'Content-Type': 'application/json' },
189
+ body: JSON.stringify({
190
+ grant_type: 'authorization_code',
191
+ code: code,
192
+ client_id: oauth2Client.clientId,
193
+ client_secret: oauth2Client.clientSecret,
194
+ redirect_uri: oauth2Client.redirectUris[0],
195
+ }),
196
+ });
197
+
198
+ const tokenData = (await tokenResponse.json()) as {
199
+ accessToken: string;
200
+ refreshToken?: string;
201
+ };
202
+ const refreshToken = tokenData.refreshToken;
203
+
204
+ // 2. 使用刷新令牌获取新的访问令牌
205
+ const refreshResponse = await fetch(`${baseUrl()}/oauth2/token`, {
206
+ method: 'POST',
207
+ headers: { 'Content-Type': 'application/json' },
208
+ body: JSON.stringify({
209
+ grant_type: 'refresh_token',
210
+ refresh_token: refreshToken,
211
+ client_id: oauth2Client.clientId,
212
+ client_secret: oauth2Client.clientSecret,
213
+ }),
214
+ });
215
+
216
+ expect(refreshResponse.status).toBe(200);
217
+ const newTokenData = (await refreshResponse.json()) as {
218
+ accessToken: string;
219
+ refreshToken?: string;
220
+ };
221
+ expect(newTokenData.accessToken).toBeDefined();
222
+ expect(newTokenData.refreshToken).toBeDefined();
223
+ // 验证新令牌可以用于访问受保护资源
224
+ const profileResponse = await fetch(`${baseUrl()}/api/protected/profile`, {
225
+ headers: {
226
+ Authorization: `Bearer ${newTokenData.accessToken}`,
227
+ },
228
+ });
229
+ expect(profileResponse.status).toBe(200);
230
+ // 刷新令牌应该生成新的刷新令牌(即使访问令牌可能因为时间戳相同而相同)
231
+ expect(newTokenData.refreshToken).toBeDefined();
232
+ });
233
+
234
+ test('should reject invalid authorization code', async () => {
235
+ app = new Application({ port });
236
+ SecurityModule.forRoot({
237
+ jwt: {
238
+ secret: 'test-secret-key',
239
+ accessTokenExpiresIn: 3600,
240
+ },
241
+ oauth2Clients: [oauth2Client],
242
+ enableOAuth2Endpoints: true,
243
+ });
244
+ app.registerModule(SecurityModule);
245
+ await app.listen();
246
+
247
+ const tokenResponse = await fetch(`${baseUrl()}/oauth2/token`, {
248
+ method: 'POST',
249
+ headers: { 'Content-Type': 'application/json' },
250
+ body: JSON.stringify({
251
+ grant_type: 'authorization_code',
252
+ code: 'invalid-code',
253
+ client_id: oauth2Client.clientId,
254
+ client_secret: oauth2Client.clientSecret,
255
+ redirect_uri: oauth2Client.redirectUris[0],
256
+ }),
257
+ });
258
+
259
+ expect(tokenResponse.status).toBe(200);
260
+ const errorData = (await tokenResponse.json()) as {
261
+ error: string;
262
+ error_description?: string;
263
+ };
264
+ expect(errorData.error).toBe('invalid_grant');
265
+ expect(errorData.error_description).toBeDefined();
266
+ });
267
+
268
+ test('should reject invalid client credentials', async () => {
269
+ app = new Application({ port });
270
+ SecurityModule.forRoot({
271
+ jwt: {
272
+ secret: 'test-secret-key',
273
+ accessTokenExpiresIn: 3600,
274
+ },
275
+ oauth2Clients: [oauth2Client],
276
+ enableOAuth2Endpoints: true,
277
+ });
278
+ app.registerModule(SecurityModule);
279
+ await app.listen();
280
+
281
+ // 获取有效的授权码
282
+ const authorizeUrl = new URL(`${baseUrl()}/oauth2/authorize`);
283
+ authorizeUrl.searchParams.set('client_id', oauth2Client.clientId);
284
+ authorizeUrl.searchParams.set('redirect_uri', oauth2Client.redirectUris[0]);
285
+ authorizeUrl.searchParams.set('response_type', 'code');
286
+
287
+ const authorizeResponse = await fetch(authorizeUrl.toString(), {
288
+ redirect: 'manual',
289
+ });
290
+ expect(authorizeResponse.status).toBe(302);
291
+ const location = authorizeResponse.headers.get('location');
292
+ expect(location).not.toBeNull();
293
+ if (!location) {
294
+ throw new Error('Location header is null');
295
+ }
296
+ const code = new URL(location!).searchParams.get('code');
297
+ expect(code).toBeDefined();
298
+
299
+ // 使用错误的客户端密钥
300
+ const tokenResponse = await fetch(`${baseUrl()}/oauth2/token`, {
301
+ method: 'POST',
302
+ headers: { 'Content-Type': 'application/json' },
303
+ body: JSON.stringify({
304
+ grant_type: 'authorization_code',
305
+ code: code,
306
+ client_id: oauth2Client.clientId,
307
+ client_secret: 'wrong-secret',
308
+ redirect_uri: oauth2Client.redirectUris[0],
309
+ }),
310
+ });
311
+
312
+ expect(tokenResponse.status).toBe(200);
313
+ const errorData = (await tokenResponse.json()) as {
314
+ error: string;
315
+ };
316
+ expect(errorData.error).toBe('invalid_grant');
317
+ });
318
+
319
+ test('should reject invalid redirect URI', async () => {
320
+ app = new Application({ port });
321
+ SecurityModule.forRoot({
322
+ jwt: {
323
+ secret: 'test-secret-key',
324
+ accessTokenExpiresIn: 3600,
325
+ },
326
+ oauth2Clients: [oauth2Client],
327
+ enableOAuth2Endpoints: true,
328
+ });
329
+ app.registerModule(SecurityModule);
330
+ await app.listen();
331
+
332
+ // 获取有效的授权码
333
+ const authorizeUrl = new URL(`${baseUrl()}/oauth2/authorize`);
334
+ authorizeUrl.searchParams.set('client_id', oauth2Client.clientId);
335
+ authorizeUrl.searchParams.set('redirect_uri', oauth2Client.redirectUris[0]);
336
+ authorizeUrl.searchParams.set('response_type', 'code');
337
+
338
+ const authorizeResponse = await fetch(authorizeUrl.toString(), {
339
+ redirect: 'manual',
340
+ });
341
+ expect(authorizeResponse.status).toBe(302);
342
+ const location = authorizeResponse.headers.get('location');
343
+ expect(location).not.toBeNull();
344
+ if (!location) {
345
+ throw new Error('Location header is null');
346
+ }
347
+ const code = new URL(location!).searchParams.get('code');
348
+ expect(code).toBeDefined();
349
+
350
+ // 使用错误的 redirect_uri
351
+ const tokenResponse = await fetch(`${baseUrl()}/oauth2/token`, {
352
+ method: 'POST',
353
+ headers: { 'Content-Type': 'application/json' },
354
+ body: JSON.stringify({
355
+ grant_type: 'authorization_code',
356
+ code: code,
357
+ client_id: oauth2Client.clientId,
358
+ client_secret: oauth2Client.clientSecret,
359
+ redirect_uri: 'http://localhost:3000/wrong-callback',
360
+ }),
361
+ });
362
+
363
+ expect(tokenResponse.status).toBe(200);
364
+ const errorData = (await tokenResponse.json()) as {
365
+ error: string;
366
+ };
367
+ expect(errorData.error).toBe('invalid_grant');
368
+ });
369
+
370
+ test('should reject unauthorized access to protected resource', async () => {
371
+ app = new Application({ port });
372
+ SecurityModule.forRoot({
373
+ jwt: {
374
+ secret: 'test-secret-key',
375
+ accessTokenExpiresIn: 3600,
376
+ },
377
+ oauth2Clients: [oauth2Client],
378
+ enableOAuth2Endpoints: true,
379
+ defaultAuthRequired: true,
380
+ });
381
+ app.registerModule(SecurityModule);
382
+ app.registerController(ProtectedController);
383
+ await app.listen();
384
+
385
+ // 尝试访问受保护资源而不提供令牌
386
+ const response = await fetch(`${baseUrl()}/api/protected/profile`);
387
+
388
+ expect(response.status).toBe(401);
389
+ const errorData = (await response.json()) as {
390
+ error?: string;
391
+ };
392
+ expect(errorData.error).toBeDefined();
393
+ });
394
+
395
+ test('should reject access with invalid token', async () => {
396
+ app = new Application({ port });
397
+ SecurityModule.forRoot({
398
+ jwt: {
399
+ secret: 'test-secret-key',
400
+ accessTokenExpiresIn: 3600,
401
+ },
402
+ oauth2Clients: [oauth2Client],
403
+ enableOAuth2Endpoints: true,
404
+ defaultAuthRequired: true,
405
+ });
406
+ app.registerModule(SecurityModule);
407
+ app.registerController(ProtectedController);
408
+ await app.listen();
409
+
410
+ // 使用无效的令牌访问受保护资源
411
+ const response = await fetch(`${baseUrl()}/api/protected/profile`, {
412
+ headers: {
413
+ Authorization: 'Bearer invalid-token',
414
+ },
415
+ });
416
+
417
+ expect(response.status).toBe(401);
418
+ });
419
+
420
+ test('should enforce role-based access control', async () => {
421
+ app = new Application({ port });
422
+ SecurityModule.forRoot({
423
+ jwt: {
424
+ secret: 'test-secret-key',
425
+ accessTokenExpiresIn: 3600,
426
+ },
427
+ oauth2Clients: [oauth2Client],
428
+ enableOAuth2Endpoints: true,
429
+ });
430
+ app.registerModule(SecurityModule);
431
+ app.registerController(ProtectedController);
432
+ await app.listen();
433
+
434
+ // 获取访问令牌(用户默认没有 admin 角色)
435
+ const authorizeUrl = new URL(`${baseUrl()}/oauth2/authorize`);
436
+ authorizeUrl.searchParams.set('client_id', oauth2Client.clientId);
437
+ authorizeUrl.searchParams.set('redirect_uri', oauth2Client.redirectUris[0]);
438
+ authorizeUrl.searchParams.set('response_type', 'code');
439
+
440
+ const authorizeResponse = await fetch(authorizeUrl.toString(), {
441
+ redirect: 'manual',
442
+ });
443
+ expect(authorizeResponse.status).toBe(302);
444
+ const location = authorizeResponse.headers.get('location');
445
+ expect(location).not.toBeNull();
446
+ if (!location) {
447
+ throw new Error('Location header is null');
448
+ }
449
+ const code = new URL(location!).searchParams.get('code');
450
+ expect(code).toBeDefined();
451
+
452
+ const tokenResponse = await fetch(`${baseUrl()}/oauth2/token`, {
453
+ method: 'POST',
454
+ headers: { 'Content-Type': 'application/json' },
455
+ body: JSON.stringify({
456
+ grant_type: 'authorization_code',
457
+ code: code,
458
+ client_id: oauth2Client.clientId,
459
+ client_secret: oauth2Client.clientSecret,
460
+ redirect_uri: oauth2Client.redirectUris[0],
461
+ }),
462
+ });
463
+
464
+ const tokenData = (await tokenResponse.json()) as {
465
+ accessToken: string;
466
+ };
467
+
468
+ // 尝试访问需要 admin 角色的资源
469
+ const adminResponse = await fetch(`${baseUrl()}/api/protected/admin`, {
470
+ headers: {
471
+ Authorization: `Bearer ${tokenData.accessToken}`,
472
+ },
473
+ });
474
+
475
+ expect(adminResponse.status).toBe(403);
476
+ const errorData = (await adminResponse.json()) as {
477
+ error?: string;
478
+ };
479
+ expect(errorData.error).toBeDefined();
480
+ });
481
+
482
+ test('should handle authorization request with invalid client', async () => {
483
+ app = new Application({ port });
484
+ SecurityModule.forRoot({
485
+ jwt: {
486
+ secret: 'test-secret-key',
487
+ accessTokenExpiresIn: 3600,
488
+ },
489
+ oauth2Clients: [oauth2Client],
490
+ enableOAuth2Endpoints: true,
491
+ });
492
+ app.registerModule(SecurityModule);
493
+ await app.listen();
494
+
495
+ const authorizeUrl = new URL(`${baseUrl()}/oauth2/authorize`);
496
+ authorizeUrl.searchParams.set('client_id', 'invalid-client');
497
+ authorizeUrl.searchParams.set('redirect_uri', oauth2Client.redirectUris[0]);
498
+ authorizeUrl.searchParams.set('response_type', 'code');
499
+
500
+ const response = await fetch(authorizeUrl.toString(), {
501
+ redirect: 'manual',
502
+ });
503
+
504
+ // 应该返回错误(可能是 500 或重定向到错误页面)
505
+ expect(response.status).toBeGreaterThanOrEqual(400);
506
+ });
507
+
508
+ test('should handle authorization request with invalid redirect URI', async () => {
509
+ app = new Application({ port });
510
+ SecurityModule.forRoot({
511
+ jwt: {
512
+ secret: 'test-secret-key',
513
+ accessTokenExpiresIn: 3600,
514
+ },
515
+ oauth2Clients: [oauth2Client],
516
+ enableOAuth2Endpoints: true,
517
+ });
518
+ app.registerModule(SecurityModule);
519
+ await app.listen();
520
+
521
+ const authorizeUrl = new URL(`${baseUrl()}/oauth2/authorize`);
522
+ authorizeUrl.searchParams.set('client_id', oauth2Client.clientId);
523
+ authorizeUrl.searchParams.set('redirect_uri', 'http://evil.com/callback');
524
+ authorizeUrl.searchParams.set('response_type', 'code');
525
+
526
+ const response = await fetch(authorizeUrl.toString(), {
527
+ redirect: 'manual',
528
+ });
529
+
530
+ // 应该返回错误
531
+ expect(response.status).toBeGreaterThanOrEqual(400);
532
+ });
533
+
534
+ test('should support user provider for custom user info', async () => {
535
+ app = new Application({ port });
536
+ SecurityModule.forRoot({
537
+ jwt: {
538
+ secret: 'test-secret-key',
539
+ accessTokenExpiresIn: 3600,
540
+ },
541
+ oauth2Clients: [oauth2Client],
542
+ enableOAuth2Endpoints: true,
543
+ userProvider: {
544
+ async findById(userId: string) {
545
+ return {
546
+ id: userId,
547
+ username: 'custom-user',
548
+ roles: ['user', 'admin'],
549
+ };
550
+ },
551
+ },
552
+ });
553
+ app.registerModule(SecurityModule);
554
+ app.registerController(ProtectedController);
555
+ await app.listen();
556
+
557
+ // 获取访问令牌
558
+ const authorizeUrl = new URL(`${baseUrl()}/oauth2/authorize`);
559
+ authorizeUrl.searchParams.set('client_id', oauth2Client.clientId);
560
+ authorizeUrl.searchParams.set('redirect_uri', oauth2Client.redirectUris[0]);
561
+ authorizeUrl.searchParams.set('response_type', 'code');
562
+
563
+ const authorizeResponse = await fetch(authorizeUrl.toString(), {
564
+ redirect: 'manual',
565
+ });
566
+ expect(authorizeResponse.status).toBe(302);
567
+ const location = authorizeResponse.headers.get('location');
568
+ expect(location).not.toBeNull();
569
+ if (!location) {
570
+ throw new Error('Location header is null');
571
+ }
572
+ const code = new URL(location!).searchParams.get('code');
573
+ expect(code).toBeDefined();
574
+
575
+ const tokenResponse = await fetch(`${baseUrl()}/oauth2/token`, {
576
+ method: 'POST',
577
+ headers: { 'Content-Type': 'application/json' },
578
+ body: JSON.stringify({
579
+ grant_type: 'authorization_code',
580
+ code: code,
581
+ client_id: oauth2Client.clientId,
582
+ client_secret: oauth2Client.clientSecret,
583
+ redirect_uri: oauth2Client.redirectUris[0],
584
+ }),
585
+ });
586
+
587
+ const tokenData = (await tokenResponse.json()) as {
588
+ accessToken: string;
589
+ };
590
+
591
+ // 访问受保护资源,应该包含自定义用户信息
592
+ const profileResponse = await fetch(`${baseUrl()}/api/protected/profile`, {
593
+ headers: {
594
+ Authorization: `Bearer ${tokenData.accessToken}`,
595
+ },
596
+ });
597
+
598
+ expect(profileResponse.status).toBe(200);
599
+ const profileData = (await profileResponse.json()) as {
600
+ username?: string;
601
+ roles?: string[];
602
+ };
603
+ expect(profileData.username).toBe('custom-user');
604
+ expect(profileData.roles).toContain('admin');
605
+
606
+ // 应该能够访问 admin 资源
607
+ const adminResponse = await fetch(`${baseUrl()}/api/protected/admin`, {
608
+ headers: {
609
+ Authorization: `Bearer ${tokenData.accessToken}`,
610
+ },
611
+ });
612
+
613
+ expect(adminResponse.status).toBe(200);
614
+ });
615
+ });