@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,99 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { ResponseBuilder } from '../../src/request/response';
6
+
7
+ describe('ResponseBuilder', () => {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tmpDir = await mkdtemp(join(tmpdir(), 'response-builder-'));
12
+ });
13
+
14
+ afterEach(async () => {
15
+ if (tmpDir) {
16
+ await rm(tmpDir, { recursive: true, force: true });
17
+ }
18
+ });
19
+
20
+ test('should create JSON response', async () => {
21
+ const response = ResponseBuilder.json({ message: 'Hello' });
22
+ expect(response.status).toBe(200);
23
+ expect(response.headers.get('Content-Type')).toBe('application/json');
24
+
25
+ const data = await response.json();
26
+ expect(data.message).toBe('Hello');
27
+ });
28
+
29
+ test('should create JSON response with custom status', async () => {
30
+ const response = ResponseBuilder.json({ error: 'Not Found' }, 404);
31
+ expect(response.status).toBe(404);
32
+ });
33
+
34
+ test('should create text response', async () => {
35
+ const response = ResponseBuilder.text('Hello World');
36
+ expect(response.status).toBe(200);
37
+ expect(response.headers.get('Content-Type')).toBe('text/plain');
38
+
39
+ const text = await response.text();
40
+ expect(text).toBe('Hello World');
41
+ });
42
+
43
+ test('should create HTML response', async () => {
44
+ const response = ResponseBuilder.html('<h1>Hello</h1>');
45
+ expect(response.status).toBe(200);
46
+ expect(response.headers.get('Content-Type')).toBe('text/html');
47
+
48
+ const html = await response.text();
49
+ expect(html).toBe('<h1>Hello</h1>');
50
+ });
51
+
52
+ test('should create empty response', () => {
53
+ const response = ResponseBuilder.empty(204);
54
+ expect(response.status).toBe(204);
55
+ expect(response.body).toBeNull();
56
+ });
57
+
58
+ test('should create redirect response', () => {
59
+ const response = ResponseBuilder.redirect('http://example.com');
60
+ expect(response.status).toBe(302);
61
+ expect(response.headers.get('Location')).toBe('http://example.com');
62
+ });
63
+
64
+ test('should create error response', async () => {
65
+ const response = ResponseBuilder.error('Internal Server Error', 500);
66
+ expect(response.status).toBe(500);
67
+
68
+ const data = await response.json();
69
+ expect(data.error).toBe('Internal Server Error');
70
+ });
71
+
72
+ test('should create file response from path', async () => {
73
+ const filePath = join(tmpDir, 'hello.txt');
74
+ await Bun.write(filePath, 'file-content');
75
+
76
+ const response = ResponseBuilder.file(filePath, {
77
+ fileName: 'download.txt',
78
+ contentType: 'text/plain',
79
+ });
80
+
81
+ expect(response.status).toBe(200);
82
+ expect(response.headers.get('Content-Disposition')).toBe('attachment; filename="download.txt"');
83
+ expect(response.headers.get('Content-Type')).toBe('text/plain');
84
+ expect(await response.text()).toBe('file-content');
85
+ });
86
+
87
+ test('should create file response from binary source', async () => {
88
+ const buffer = new TextEncoder().encode('binary-content');
89
+ const response = ResponseBuilder.file(buffer, {
90
+ status: 206,
91
+ headers: { ETag: '123' },
92
+ });
93
+
94
+ expect(response.status).toBe(206);
95
+ expect(response.headers.get('ETag')).toBe('123');
96
+ expect(await response.text()).toBe('binary-content');
97
+ });
98
+ });
99
+
@@ -0,0 +1,48 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+
3
+ import { Controller } from '../../src/controller/controller';
4
+ import { GET, POST } from '../../src/router/decorators';
5
+ import { getRouteMetadata } from '../../src/controller/metadata';
6
+ import { RouteRegistry } from '../../src/router/registry';
7
+ import { Context } from '../../src/core/context';
8
+
9
+ describe('Router Decorators', () => {
10
+ afterEach(() => {
11
+ RouteRegistry.getInstance().clear();
12
+ });
13
+
14
+ test('should record metadata for controller methods', () => {
15
+ @Controller('/decorator')
16
+ class DecoratedController {
17
+ @GET('/list')
18
+ public list() {
19
+ return 'ok';
20
+ }
21
+ }
22
+
23
+ const metadata = getRouteMetadata(DecoratedController.prototype);
24
+ expect(metadata.length).toBe(1);
25
+ expect(metadata[0]?.path).toBe('/list');
26
+ expect(metadata[0]?.method).toBe('GET');
27
+ expect(typeof metadata[0]?.handler).toBe('function');
28
+ });
29
+
30
+ test('should register handler immediately when not within controller', async () => {
31
+ class PlainHandlers {
32
+ @POST('/plain')
33
+ public handler(context: Context) {
34
+ return context.createResponse({ ok: true });
35
+ }
36
+ }
37
+
38
+ const registry = RouteRegistry.getInstance();
39
+ const router = registry.getRouter();
40
+ const request = new Request('http://localhost/plain', { method: 'POST' });
41
+ const context = new Context(request);
42
+ const response = await router.handle(context);
43
+
44
+ expect(response).toBeDefined();
45
+ expect(await response?.json()).toEqual({ ok: true });
46
+ });
47
+ });
48
+
@@ -0,0 +1,51 @@
1
+ import { describe, expect, test, beforeEach } from 'bun:test';
2
+ import { RouteRegistry } from '../../src/router/registry';
3
+ import { Context } from '../../src/core/context';
4
+
5
+ describe('RouteRegistry', () => {
6
+ beforeEach(() => {
7
+ // 重置单例(在实际使用中,单例会保持状态)
8
+ // 这里我们只是测试基本功能
9
+ });
10
+
11
+ test('should get singleton instance', () => {
12
+ const instance1 = RouteRegistry.getInstance();
13
+ const instance2 = RouteRegistry.getInstance();
14
+
15
+ expect(instance1).toBe(instance2);
16
+ });
17
+
18
+ test('should register route', () => {
19
+ const registry = RouteRegistry.getInstance();
20
+ const handler = (ctx: Context) => ctx.createResponse({});
21
+
22
+ registry.register('GET', '/api/users', handler);
23
+
24
+ const router = registry.getRouter();
25
+ const route = router.findRoute('GET', '/api/users');
26
+ expect(route).toBeDefined();
27
+ });
28
+
29
+ test('should register GET route', () => {
30
+ const registry = RouteRegistry.getInstance();
31
+ const handler = (ctx: Context) => ctx.createResponse({});
32
+
33
+ registry.get('/api/users', handler);
34
+
35
+ const router = registry.getRouter();
36
+ const route = router.findRoute('GET', '/api/users');
37
+ expect(route).toBeDefined();
38
+ });
39
+
40
+ test('should register POST route', () => {
41
+ const registry = RouteRegistry.getInstance();
42
+ const handler = (ctx: Context) => ctx.createResponse({});
43
+
44
+ registry.post('/api/users', handler);
45
+
46
+ const router = registry.getRouter();
47
+ const route = router.findRoute('POST', '/api/users');
48
+ expect(route).toBeDefined();
49
+ });
50
+ });
51
+
@@ -0,0 +1,71 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { Route } from '../../src/router/route';
3
+ import { Context } from '../../src/core/context';
4
+
5
+ describe('Route', () => {
6
+ test('should create route', () => {
7
+ const handler = (ctx: Context) => ctx.createResponse({ message: 'Hello' });
8
+ const route = new Route('GET', '/api/users', handler);
9
+
10
+ expect(route.method).toBe('GET');
11
+ expect(route.path).toBe('/api/users');
12
+ });
13
+
14
+ test('should match exact path', () => {
15
+ const handler = (ctx: Context) => ctx.createResponse({});
16
+ const route = new Route('GET', '/api/users', handler);
17
+
18
+ const match = route.match('GET', '/api/users');
19
+ expect(match.matched).toBe(true);
20
+ expect(match.params).toEqual({});
21
+ });
22
+
23
+ test('should not match different method', () => {
24
+ const handler = (ctx: Context) => ctx.createResponse({});
25
+ const route = new Route('GET', '/api/users', handler);
26
+
27
+ const match = route.match('POST', '/api/users');
28
+ expect(match.matched).toBe(false);
29
+ });
30
+
31
+ test('should not match different path', () => {
32
+ const handler = (ctx: Context) => ctx.createResponse({});
33
+ const route = new Route('GET', '/api/users', handler);
34
+
35
+ const match = route.match('GET', '/api/posts');
36
+ expect(match.matched).toBe(false);
37
+ });
38
+
39
+ test('should match path with parameter', () => {
40
+ const handler = (ctx: Context) => ctx.createResponse({});
41
+ const route = new Route('GET', '/api/users/:id', handler);
42
+
43
+ const match = route.match('GET', '/api/users/123');
44
+ expect(match.matched).toBe(true);
45
+ expect(match.params).toEqual({ id: '123' });
46
+ });
47
+
48
+ test('should match path with multiple parameters', () => {
49
+ const handler = (ctx: Context) => ctx.createResponse({});
50
+ const route = new Route('GET', '/api/users/:userId/posts/:postId', handler);
51
+
52
+ const match = route.match('GET', '/api/users/123/posts/456');
53
+ expect(match.matched).toBe(true);
54
+ expect(match.params).toEqual({ userId: '123', postId: '456' });
55
+ });
56
+
57
+ test('should execute handler', async () => {
58
+ const handler = (ctx: Context) => ctx.createResponse({ message: 'Hello' });
59
+ const route = new Route('GET', '/api/users', handler);
60
+
61
+ const request = new Request('http://localhost:3000/api/users');
62
+ const context = new Context(request);
63
+
64
+ const response = await route.execute(context);
65
+ expect(response.status).toBe(200);
66
+
67
+ const data = await response.json();
68
+ expect(data.message).toBe('Hello');
69
+ });
70
+ });
71
+
@@ -0,0 +1,106 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { Router } from '../../src/router/router';
3
+ import { Context } from '../../src/core/context';
4
+
5
+ /**
6
+ * 测试路由路径规范化行为
7
+ * 确保带尾部斜杠的路径能正确匹配
8
+ */
9
+ describe('Router Path Normalization', () => {
10
+ test('findRoute should normalize path with trailing slash', () => {
11
+ const router = new Router();
12
+ const handler = (ctx: Context) => ctx.createResponse({ ok: true });
13
+
14
+ // 注册路由(无尾部斜杠)
15
+ router.get('/api/users', handler);
16
+
17
+ // 使用带尾部斜杠的路径查找应该能匹配
18
+ const route1 = router.findRoute('GET', '/api/users/');
19
+ expect(route1).toBeDefined();
20
+ expect(route1?.path).toBe('/api/users');
21
+
22
+ // 使用无尾部斜杠的路径查找应该能匹配
23
+ const route2 = router.findRoute('GET', '/api/users');
24
+ expect(route2).toBeDefined();
25
+ expect(route2?.path).toBe('/api/users');
26
+
27
+ // 两者应该返回相同的路由
28
+ expect(route1).toBe(route2);
29
+ });
30
+
31
+ test('findRoute should normalize root path correctly', () => {
32
+ const router = new Router();
33
+ const handler = (ctx: Context) => ctx.createResponse({ ok: true });
34
+
35
+ // 注册根路径
36
+ router.get('/', handler);
37
+
38
+ // 根路径应该始终匹配(无论是否有尾部斜杠)
39
+ const route1 = router.findRoute('GET', '/');
40
+ expect(route1).toBeDefined();
41
+ expect(route1?.path).toBe('/');
42
+ });
43
+
44
+ test('findRoute should use cache for normalized paths', () => {
45
+ const router = new Router();
46
+ const handler = (ctx: Context) => ctx.createResponse({ ok: true });
47
+
48
+ router.get('/api/test', handler);
49
+
50
+ // 第一次查找(带尾部斜杠)
51
+ const route1 = router.findRoute('GET', '/api/test/');
52
+ expect(route1).toBeDefined();
53
+
54
+ // 第二次查找(无尾部斜杠)- 应该使用缓存
55
+ const route2 = router.findRoute('GET', '/api/test');
56
+ expect(route2).toBeDefined();
57
+ expect(route2).toBe(route1);
58
+ });
59
+
60
+ test('findRouteWithMatch should normalize path before caching', () => {
61
+ const router = new Router();
62
+ const handler = (ctx: Context) => ctx.createResponse({ ok: true });
63
+
64
+ router.get('/api/users', handler);
65
+
66
+ // 使用带尾部斜杠的路径
67
+ const result1 = router.findRouteWithMatch('GET', '/api/users/');
68
+ expect(result1).toBeDefined();
69
+ expect(result1?.route.path).toBe('/api/users');
70
+
71
+ // 使用无尾部斜杠的路径 - 应该从缓存获取
72
+ const result2 = router.findRouteWithMatch('GET', '/api/users');
73
+ expect(result2).toBeDefined();
74
+ expect(result2?.route.path).toBe('/api/users');
75
+
76
+ // 两者应该返回相同的结果(缓存命中)
77
+ expect(result1).toBe(result2);
78
+ });
79
+
80
+ test('findRoute should handle dynamic routes with trailing slash', async () => {
81
+ const router = new Router();
82
+ const handler = (ctx: Context) => ctx.createResponse({ id: ctx.getParam('id') });
83
+
84
+ router.get('/api/users/:id', handler);
85
+
86
+ // 使用带尾部斜杠的路径查找应该能匹配
87
+ const route1 = router.findRoute('GET', '/api/users/123/');
88
+ expect(route1).toBeDefined();
89
+
90
+ // 使用无尾部斜杠的路径查找应该能匹配
91
+ const route2 = router.findRoute('GET', '/api/users/123');
92
+ expect(route2).toBeDefined();
93
+
94
+ // 两者应该返回相同的路由
95
+ expect(route1).toBe(route2);
96
+
97
+ // 验证参数提取
98
+ const context = new Context(new Request('http://localhost:3000/api/users/123/'));
99
+ const response = await router.handle(context);
100
+ expect(response).toBeDefined();
101
+ if (response) {
102
+ const data = await response.json();
103
+ expect(data.id).toBe('123');
104
+ }
105
+ });
106
+ });
@@ -0,0 +1,133 @@
1
+ import { describe, expect, test, beforeEach } from 'bun:test';
2
+ import { Router } from '../../src/router/router';
3
+ import { Context } from '../../src/core/context';
4
+
5
+ describe('Router', () => {
6
+ let router: Router;
7
+
8
+ beforeEach(() => {
9
+ router = new Router();
10
+ });
11
+
12
+ test('should register route', () => {
13
+ const handler = (ctx: Context) => ctx.createResponse({});
14
+ router.register('GET', '/api/users', handler);
15
+
16
+ const routes = router.getRoutes();
17
+ expect(routes.length).toBe(1);
18
+ });
19
+
20
+ test('should register GET route', () => {
21
+ const handler = (ctx: Context) => ctx.createResponse({});
22
+ router.get('/api/users', handler);
23
+
24
+ const route = router.findRoute('GET', '/api/users');
25
+ expect(route).toBeDefined();
26
+ });
27
+
28
+ test('should register POST route', () => {
29
+ const handler = (ctx: Context) => ctx.createResponse({});
30
+ router.post('/api/users', handler);
31
+
32
+ const route = router.findRoute('POST', '/api/users');
33
+ expect(route).toBeDefined();
34
+ });
35
+
36
+ test('should find route by method and path', () => {
37
+ const handler = (ctx: Context) => ctx.createResponse({});
38
+ router.get('/api/users', handler);
39
+
40
+ const route = router.findRoute('GET', '/api/users');
41
+ expect(route).toBeDefined();
42
+ expect(route?.method).toBe('GET');
43
+ expect(route?.path).toBe('/api/users');
44
+ });
45
+
46
+ test('should not find non-existent route', () => {
47
+ const route = router.findRoute('GET', '/api/users');
48
+ expect(route).toBeUndefined();
49
+ });
50
+
51
+ test('should handle request', async () => {
52
+ const handler = (ctx: Context) => ctx.createResponse({ message: 'Hello' });
53
+ router.get('/api/users', handler);
54
+
55
+ const request = new Request('http://localhost:3000/api/users');
56
+ const context = new Context(request);
57
+
58
+ const response = await router.handle(context);
59
+ expect(response).toBeDefined();
60
+
61
+ if (response) {
62
+ const data = await response.json();
63
+ expect(data.message).toBe('Hello');
64
+ }
65
+ });
66
+
67
+ test('should return undefined for non-existent route', async () => {
68
+ const request = new Request('http://localhost:3000/api/users');
69
+ const context = new Context(request);
70
+
71
+ const response = await router.handle(context);
72
+ expect(response).toBeUndefined();
73
+ });
74
+
75
+ test('should extract path parameters', async () => {
76
+ const handler = (ctx: Context) => {
77
+ return ctx.createResponse({ id: ctx.getParam('id') });
78
+ };
79
+ router.get('/api/users/:id', handler);
80
+
81
+ const request = new Request('http://localhost:3000/api/users/123');
82
+ const context = new Context(request);
83
+
84
+ const response = await router.handle(context);
85
+ expect(response).toBeDefined();
86
+
87
+ if (response) {
88
+ const data = await response.json();
89
+ expect(data.id).toBe('123');
90
+ }
91
+ });
92
+
93
+ test('should handle multiple routes', () => {
94
+ const handler1 = (ctx: Context) => ctx.createResponse({});
95
+ const handler2 = (ctx: Context) => ctx.createResponse({});
96
+
97
+ router.get('/api/users', handler1);
98
+ router.post('/api/users', handler2);
99
+
100
+ const getRoute = router.findRoute('GET', '/api/users');
101
+ const postRoute = router.findRoute('POST', '/api/users');
102
+
103
+ expect(getRoute).toBeDefined();
104
+ expect(postRoute).toBeDefined();
105
+ expect(getRoute?.method).toBe('GET');
106
+ expect(postRoute?.method).toBe('POST');
107
+ });
108
+
109
+ test('should cache static routes for faster lookup', () => {
110
+ const handler = (ctx: Context) => ctx.createResponse({});
111
+ router.get('/static/path', handler);
112
+
113
+ const internalMap = (router as unknown as { staticRoutes: Map<string, unknown> }).staticRoutes;
114
+ expect(internalMap.size).toBe(1);
115
+ expect(router.findRoute('GET', '/static/path')).toBeDefined();
116
+
117
+ // dynamic route should still work via fallback iteration
118
+ router.get('/api/items/:id', handler);
119
+ expect(router.findRoute('GET', '/api/items/1')).toBeDefined();
120
+ });
121
+
122
+ test('should clear registered routes and caches', () => {
123
+ router.get('/cache/test', (ctx: Context) => ctx.createResponse({}));
124
+ expect(router.getRoutes().length).toBe(1);
125
+
126
+ router.clear();
127
+ expect(router.getRoutes().length).toBe(0);
128
+ const internalMap = (router as unknown as { staticRoutes: Map<string, unknown> }).staticRoutes;
129
+ expect(internalMap.size).toBe(0);
130
+ expect(router.findRoute('GET', '/cache/test')).toBeUndefined();
131
+ });
132
+ });
133
+
@@ -0,0 +1,84 @@
1
+ import { describe, expect, test, beforeEach } from 'bun:test';
2
+ import { RoleBasedAccessDecisionManager } from '../../src/security/access-decision-manager';
3
+ import type { Authentication } from '../../src/security/types';
4
+
5
+ describe('RoleBasedAccessDecisionManager', () => {
6
+ let manager: RoleBasedAccessDecisionManager;
7
+
8
+ beforeEach(() => {
9
+ manager = new RoleBasedAccessDecisionManager();
10
+ });
11
+
12
+ test('should allow access when no roles required', () => {
13
+ const authentication: Authentication = {
14
+ authenticated: true,
15
+ principal: { id: 'user-1', username: 'test', roles: [] },
16
+ credentials: { type: 'jwt', data: 'token' },
17
+ authorities: [],
18
+ };
19
+
20
+ const result = manager.decide(authentication, []);
21
+ expect(result).toBe(true);
22
+ });
23
+
24
+ test('should deny access when not authenticated', () => {
25
+ const authentication: Authentication = {
26
+ authenticated: false,
27
+ principal: { id: 'user-1', username: 'test', roles: [] },
28
+ credentials: { type: 'jwt', data: 'token' },
29
+ authorities: [],
30
+ };
31
+
32
+ const result = manager.decide(authentication, ['admin']);
33
+ expect(result).toBe(false);
34
+ });
35
+
36
+ test('should allow access when user has required role', () => {
37
+ const authentication: Authentication = {
38
+ authenticated: true,
39
+ principal: { id: 'user-1', username: 'test', roles: ['admin'] },
40
+ credentials: { type: 'jwt', data: 'token' },
41
+ authorities: ['admin'],
42
+ };
43
+
44
+ const result = manager.decide(authentication, ['admin']);
45
+ expect(result).toBe(true);
46
+ });
47
+
48
+ test('should allow access when user has one of required roles', () => {
49
+ const authentication: Authentication = {
50
+ authenticated: true,
51
+ principal: { id: 'user-1', username: 'test', roles: ['user'] },
52
+ credentials: { type: 'jwt', data: 'token' },
53
+ authorities: ['user'],
54
+ };
55
+
56
+ const result = manager.decide(authentication, ['admin', 'user']);
57
+ expect(result).toBe(true);
58
+ });
59
+
60
+ test('should deny access when user does not have required role', () => {
61
+ const authentication: Authentication = {
62
+ authenticated: true,
63
+ principal: { id: 'user-1', username: 'test', roles: ['user'] },
64
+ credentials: { type: 'jwt', data: 'token' },
65
+ authorities: ['user'],
66
+ };
67
+
68
+ const result = manager.decide(authentication, ['admin']);
69
+ expect(result).toBe(false);
70
+ });
71
+
72
+ test('should handle empty authorities', () => {
73
+ const authentication: Authentication = {
74
+ authenticated: true,
75
+ principal: { id: 'user-1', username: 'test', roles: [] },
76
+ credentials: { type: 'jwt', data: 'token' },
77
+ authorities: [],
78
+ };
79
+
80
+ const result = manager.decide(authentication, ['admin']);
81
+ expect(result).toBe(false);
82
+ });
83
+ });
84
+
@@ -0,0 +1,81 @@
1
+ import { describe, expect, test, beforeEach } from 'bun:test';
2
+ import { AuthenticationManager } from '../../src/security/authentication-manager';
3
+ import { JwtAuthenticationProvider } from '../../src/security/providers/jwt-provider';
4
+ import { JWTUtil } from '../../src/auth/jwt';
5
+
6
+ describe('AuthenticationManager', () => {
7
+ let manager: AuthenticationManager;
8
+ let jwtUtil: JWTUtil;
9
+ let jwtProvider: JwtAuthenticationProvider;
10
+
11
+ beforeEach((done) => {
12
+ manager = new AuthenticationManager();
13
+ jwtUtil = new JWTUtil({
14
+ secret: 'test-secret-key',
15
+ accessTokenExpiresIn: 3600,
16
+ });
17
+ jwtProvider = new JwtAuthenticationProvider(jwtUtil);
18
+ done();
19
+ });
20
+
21
+ test('should register provider', (done) => {
22
+ manager.registerProvider(jwtProvider);
23
+ const providers = manager.getProviders();
24
+ expect(providers.length).toBe(1);
25
+ expect(providers[0]).toBe(jwtProvider);
26
+ done();
27
+ });
28
+
29
+ test('should register multiple providers', (done) => {
30
+ const provider2 = new JwtAuthenticationProvider(jwtUtil);
31
+ manager.registerProvider(jwtProvider);
32
+ manager.registerProvider(provider2);
33
+ expect(manager.getProviders().length).toBe(2);
34
+ done();
35
+ });
36
+
37
+ test('should authenticate with registered provider', async (done) => {
38
+ manager.registerProvider(jwtProvider);
39
+ const token = jwtUtil.generateAccessToken({
40
+ sub: 'user-1',
41
+ username: 'testuser',
42
+ roles: ['user'],
43
+ });
44
+
45
+ const authentication = await manager.authenticate({
46
+ principal: '',
47
+ credentials: token,
48
+ type: 'jwt',
49
+ });
50
+
51
+ expect(authentication).not.toBeNull();
52
+ expect(authentication?.authenticated).toBe(true);
53
+ done();
54
+ });
55
+
56
+ test('should throw error for unsupported type', async (done) => {
57
+ manager.registerProvider(jwtProvider);
58
+
59
+ await expect(
60
+ manager.authenticate({
61
+ principal: '',
62
+ credentials: 'token',
63
+ type: 'unsupported',
64
+ }),
65
+ ).rejects.toThrow('No authentication provider found for type: unsupported');
66
+ done();
67
+ });
68
+
69
+ test('should use default type when not specified', async (done) => {
70
+ manager.registerProvider(jwtProvider);
71
+
72
+ await expect(
73
+ manager.authenticate({
74
+ principal: '',
75
+ credentials: 'token',
76
+ }),
77
+ ).rejects.toThrow('No authentication provider found for type: default');
78
+ done();
79
+ });
80
+ });
81
+