@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,207 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
2
+ import 'reflect-metadata';
3
+ import {
4
+ Application,
5
+ SessionModule,
6
+ SessionService,
7
+ SESSION_SERVICE_TOKEN,
8
+ ConfigModule,
9
+ Controller,
10
+ GET,
11
+ POST,
12
+ Inject,
13
+ Injectable,
14
+ Module,
15
+ Param,
16
+ Body,
17
+ Session,
18
+ createSessionMiddleware,
19
+ } from '../../src';
20
+ import { getTestPort } from '../utils/test-port';
21
+
22
+ @Injectable()
23
+ class AuthService {
24
+ public constructor(
25
+ @Inject(SESSION_SERVICE_TOKEN) private readonly sessionService: SessionService,
26
+ ) {}
27
+
28
+ public async login(username: string, sessionId?: string): Promise<{ sessionId: string }> {
29
+ // 如果提供了 sessionId,使用现有 Session,否则创建新 Session
30
+ let session;
31
+ if (sessionId) {
32
+ session = await this.sessionService.get(sessionId);
33
+ if (session) {
34
+ // 更新现有 Session 的数据
35
+ await this.sessionService.set(sessionId, {
36
+ username,
37
+ loginTime: Date.now(),
38
+ });
39
+ session = await this.sessionService.get(sessionId);
40
+ }
41
+ }
42
+
43
+ if (!session) {
44
+ session = await this.sessionService.create({
45
+ username,
46
+ loginTime: Date.now(),
47
+ });
48
+ }
49
+
50
+ return { sessionId: session.id };
51
+ }
52
+ }
53
+
54
+ @Controller('/api/auth')
55
+ class AuthController {
56
+ public constructor(
57
+ @Inject(AuthService) private readonly authService: AuthService,
58
+ ) {}
59
+
60
+ @POST('/login')
61
+ public async login(
62
+ @Body() body: { username: string },
63
+ @Session() session: any,
64
+ ) {
65
+ // 使用中间件创建的 Session(如果存在)
66
+ const sessionId = session?.id;
67
+ return await this.authService.login(body.username, sessionId);
68
+ }
69
+ }
70
+
71
+ @Controller('/api/session')
72
+ class SessionController {
73
+ public constructor(
74
+ @Inject(SESSION_SERVICE_TOKEN) private readonly sessionService: SessionService,
75
+ ) {}
76
+
77
+ @GET('/info')
78
+ public async getInfo(@Session() session: any) {
79
+ if (!session) {
80
+ return { error: 'No session' };
81
+ }
82
+ return {
83
+ sessionId: session.id,
84
+ username: session.data.username,
85
+ };
86
+ }
87
+
88
+ @POST('/set')
89
+ public async setValue(
90
+ @Session() session: any,
91
+ @Body() body: { key: string; value: unknown },
92
+ ) {
93
+ if (!session) {
94
+ return { error: 'No session' };
95
+ }
96
+ await this.sessionService.setValue(session.id, body.key, body.value);
97
+ return { success: true };
98
+ }
99
+ }
100
+
101
+ @Module({
102
+ controllers: [AuthController, SessionController],
103
+ providers: [AuthService],
104
+ })
105
+ class AppModule {}
106
+
107
+ describe('Session E2E', () => {
108
+ let app: Application;
109
+ let port: number;
110
+
111
+ beforeEach(async () => {
112
+ port = getTestPort();
113
+ ConfigModule.forRoot({
114
+ defaultConfig: { app: { name: 'Session E2E Test', port } },
115
+ });
116
+ SessionModule.forRoot({
117
+ name: 'sessionId',
118
+ maxAge: 86400000,
119
+ rolling: true,
120
+ });
121
+ app = new Application({ port });
122
+ app.registerModule(SessionModule);
123
+ const container = app.getContainer();
124
+ app.use(createSessionMiddleware(container));
125
+ app.registerModule(AppModule);
126
+ await app.listen();
127
+ });
128
+
129
+ afterEach(async () => {
130
+ await app.stop();
131
+ });
132
+
133
+ test('should create session on login', async () => {
134
+ const response = await fetch(`http://localhost:${port}/api/auth/login`, {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({ username: 'testuser' }),
138
+ });
139
+
140
+ expect(response.status).toBe(200);
141
+ const data = await response.json();
142
+ expect(data.sessionId).toBeDefined();
143
+
144
+ // 检查 Cookie
145
+ const setCookie = response.headers.get('Set-Cookie');
146
+ expect(setCookie).toBeTruthy();
147
+ // Set-Cookie 可能返回字符串或数组
148
+ const cookieStr = Array.isArray(setCookie) ? setCookie[0] : setCookie;
149
+ if (cookieStr && typeof cookieStr === 'string') {
150
+ expect(cookieStr.includes('sessionId=')).toBe(true);
151
+ }
152
+ });
153
+
154
+ test('should get session info', async () => {
155
+ // 先登录获取 session
156
+ const loginResponse = await fetch(`http://localhost:${port}/api/auth/login`, {
157
+ method: 'POST',
158
+ headers: { 'Content-Type': 'application/json' },
159
+ body: JSON.stringify({ username: 'testuser' }),
160
+ });
161
+ const loginData = await loginResponse.json();
162
+ const setCookie = loginResponse.headers.get('Set-Cookie');
163
+
164
+ // Set-Cookie 可能返回字符串或数组
165
+ const cookieStr = Array.isArray(setCookie) ? setCookie[0] : setCookie;
166
+ if (!cookieStr || typeof cookieStr !== 'string') {
167
+ throw new Error('Set-Cookie header not found');
168
+ }
169
+
170
+ // 提取 sessionId=value 部分(第一个分号之前的内容)
171
+ const cookieValue = cookieStr.split(';')[0].trim();
172
+
173
+ // 使用 Cookie 访问受保护端点
174
+ const infoResponse = await fetch(`http://localhost:${port}/api/session/info`, {
175
+ headers: {
176
+ Cookie: cookieValue,
177
+ },
178
+ });
179
+
180
+ expect(infoResponse.status).toBe(200);
181
+ const infoData = await infoResponse.json();
182
+ expect(infoData.sessionId).toBe(loginData.sessionId);
183
+ expect(infoData.username).toBe('testuser');
184
+ });
185
+
186
+ test('should set session value', async () => {
187
+ const loginResponse = await fetch(`http://localhost:${port}/api/auth/login`, {
188
+ method: 'POST',
189
+ headers: { 'Content-Type': 'application/json' },
190
+ body: JSON.stringify({ username: 'testuser' }),
191
+ });
192
+ const cookies = loginResponse.headers.get('Set-Cookie') || '';
193
+
194
+ const setResponse = await fetch(`http://localhost:${port}/api/session/set`, {
195
+ method: 'POST',
196
+ headers: {
197
+ 'Content-Type': 'application/json',
198
+ Cookie: cookies.split(';')[0],
199
+ },
200
+ body: JSON.stringify({ key: 'cart', value: ['item1', 'item2'] }),
201
+ });
202
+
203
+ expect(setResponse.status).toBe(200);
204
+ const data = await setResponse.json();
205
+ expect(data.success).toBe(true);
206
+ });
207
+ });
@@ -0,0 +1,178 @@
1
+ import { describe, expect, test, beforeEach } from 'bun:test';
2
+ import 'reflect-metadata';
3
+
4
+ import { Application } from '../../src/core/application';
5
+ import { MetricsModule, MetricsCollector, createHttpMetricsMiddleware, type CustomMetric } from '../../src/metrics';
6
+ import { Controller } from '../../src/controller';
7
+ import { METRICS_SERVICE_TOKEN } from '../../src/metrics';
8
+ import { Inject } from '../../src/di/decorators';
9
+ import { MODULE_METADATA_KEY } from '../../src/di/module';
10
+
11
+ describe('MetricsModule', () => {
12
+ beforeEach(() => {
13
+ // 清除模块元数据
14
+ Reflect.deleteMetadata(MODULE_METADATA_KEY, MetricsModule);
15
+ });
16
+
17
+ test('should register metrics service provider', () => {
18
+ MetricsModule.forRoot();
19
+
20
+ const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, MetricsModule);
21
+ expect(metadata).toBeDefined();
22
+ expect(metadata.providers).toBeDefined();
23
+
24
+ const metricsProvider = metadata.providers.find(
25
+ (provider: any) => provider.provide === METRICS_SERVICE_TOKEN,
26
+ );
27
+ expect(metricsProvider).toBeDefined();
28
+ expect(metricsProvider.useValue).toBeInstanceOf(MetricsCollector);
29
+ });
30
+
31
+ test('should register custom metrics', () => {
32
+ const customMetric: CustomMetric = {
33
+ name: 'custom_metric',
34
+ type: 'gauge',
35
+ help: 'A custom metric',
36
+ getValue: () => 42,
37
+ };
38
+
39
+ MetricsModule.forRoot({
40
+ customMetrics: [customMetric],
41
+ });
42
+
43
+ const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, MetricsModule);
44
+ const metricsProvider = metadata.providers.find(
45
+ (provider: any) => provider.provide === METRICS_SERVICE_TOKEN,
46
+ );
47
+ const collector = metricsProvider.useValue as MetricsCollector;
48
+
49
+ // 验证自定义指标已注册(通过检查是否能获取到)
50
+ expect(collector).toBeInstanceOf(MetricsCollector);
51
+ });
52
+ });
53
+
54
+ describe('MetricsCollector', () => {
55
+ let collector: MetricsCollector;
56
+
57
+ beforeEach(() => {
58
+ collector = new MetricsCollector();
59
+ });
60
+
61
+ test('should increment counter', async () => {
62
+ collector.incrementCounter('test_counter');
63
+ collector.incrementCounter('test_counter', { label: 'value' }, 2);
64
+
65
+ const dataPoints = await collector.getAllDataPoints();
66
+ const counterPoints = dataPoints.filter((p) => p.name === 'test_counter');
67
+ expect(counterPoints.length).toBeGreaterThan(0);
68
+ });
69
+
70
+ test('should set gauge', async () => {
71
+ collector.setGauge('test_gauge', undefined, 100);
72
+ collector.setGauge('test_gauge', { label: 'value' }, 200);
73
+
74
+ const dataPoints = await collector.getAllDataPoints();
75
+ const gaugePoints = dataPoints.filter((p) => p.name === 'test_gauge');
76
+ expect(gaugePoints.length).toBeGreaterThan(0);
77
+ });
78
+
79
+ test('should observe histogram', async () => {
80
+ collector.observeHistogram('test_histogram', undefined, 0.5);
81
+ collector.observeHistogram('test_histogram', { label: 'value' }, 1.0);
82
+
83
+ const dataPoints = await collector.getAllDataPoints();
84
+ const histogramPoints = dataPoints.filter((p) => p.name.startsWith('test_histogram'));
85
+ expect(histogramPoints.length).toBeGreaterThan(0);
86
+ });
87
+
88
+ test('should collect custom metrics', async () => {
89
+ const customMetric: CustomMetric = {
90
+ name: 'custom_gauge',
91
+ type: 'gauge',
92
+ help: 'Custom gauge metric',
93
+ getValue: () => 123,
94
+ };
95
+
96
+ collector.registerCustomMetric(customMetric);
97
+
98
+ const dataPoints = await collector.getAllDataPoints();
99
+ const customPoint = dataPoints.find((p) => p.name === 'custom_gauge');
100
+ expect(customPoint).toBeDefined();
101
+ expect(customPoint?.value).toBe(123);
102
+ expect(customPoint?.help).toBe('Custom gauge metric');
103
+ });
104
+
105
+ test('should reset all metrics', async () => {
106
+ collector.incrementCounter('test_counter');
107
+ collector.setGauge('test_gauge', undefined, 100);
108
+
109
+ collector.reset();
110
+
111
+ const dataPoints = await collector.getAllDataPoints();
112
+ // 自定义指标可能仍然存在
113
+ const nonCustomPoints = dataPoints.filter((p) => !p.name.startsWith('custom_'));
114
+ expect(nonCustomPoints.length).toBe(0);
115
+ });
116
+ });
117
+
118
+ describe('PrometheusFormatter', () => {
119
+ test('should format metrics in Prometheus format', async () => {
120
+ const collector = new MetricsCollector();
121
+ collector.incrementCounter('http_requests_total', { method: 'GET', status: '200' });
122
+ collector.setGauge('active_connections', undefined, 10);
123
+
124
+ const { PrometheusFormatter } = await import('../../src/metrics/prometheus');
125
+ const formatter = new PrometheusFormatter();
126
+ const dataPoints = await collector.getAllDataPoints();
127
+ const formatted = formatter.format(dataPoints);
128
+
129
+ expect(formatted).toContain('http_requests_total');
130
+ expect(formatted).toContain('active_connections');
131
+ expect(formatted).toContain('method="GET"');
132
+ expect(formatted).toContain('status="200"');
133
+ });
134
+ });
135
+
136
+ describe('MetricsController', () => {
137
+ test('should create controller instance', () => {
138
+ const collector = new MetricsCollector();
139
+ const { MetricsController } = require('../../src/metrics/controller');
140
+ const controller = new MetricsController(collector, {});
141
+ expect(controller).toBeDefined();
142
+ });
143
+
144
+ test('should format metrics response', async () => {
145
+ const collector = new MetricsCollector();
146
+ collector.incrementCounter('test_counter', { label: 'value' });
147
+
148
+ const { MetricsController } = await import('../../src/metrics/controller');
149
+ const controller = new MetricsController(collector, {});
150
+
151
+ const response = await controller.metrics();
152
+ expect(response.status).toBe(200);
153
+ expect(response.headers.get('Content-Type')).toContain('text/plain');
154
+
155
+ const text = await response.text();
156
+ expect(text).toContain('test_counter');
157
+ });
158
+ });
159
+
160
+ describe('createHttpMetricsMiddleware', () => {
161
+ test('should collect HTTP request metrics', async () => {
162
+ const collector = new MetricsCollector();
163
+ const middleware = createHttpMetricsMiddleware(collector);
164
+
165
+ const { Context } = await import('../../src/core/context');
166
+ const context = new Context(new Request('http://localhost:3000/api/test', { method: 'GET' }));
167
+
168
+ await middleware(context, async () => {
169
+ return new Response('OK', { status: 200 });
170
+ });
171
+
172
+ const dataPoints = await collector.getAllDataPoints();
173
+ const requestTotal = dataPoints.find((p) => p.name === 'http_requests_total');
174
+ expect(requestTotal).toBeDefined();
175
+ expect(requestTotal?.labels?.method).toBe('GET');
176
+ expect(requestTotal?.labels?.status).toBe('200');
177
+ });
178
+ });
@@ -0,0 +1,206 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { Context } from '../../src/core/context';
4
+ import {
5
+ createLoggerMiddleware,
6
+ createRequestLoggingMiddleware,
7
+ createErrorHandlingMiddleware,
8
+ createCorsMiddleware,
9
+ runMiddlewares,
10
+ } from '../../src/middleware';
11
+ import { ValidationError } from '../../src/validation/errors';
12
+ import { HttpException } from '../../src/error/http-exception';
13
+
14
+ function createContext(url = 'http://localhost:3000/api/test', method: string = 'GET'): Context {
15
+ return new Context(
16
+ new Request(url, {
17
+ method,
18
+ headers: {
19
+ Origin: 'http://localhost:4000',
20
+ },
21
+ }),
22
+ );
23
+ }
24
+
25
+ describe('Builtin Middlewares', () => {
26
+ test('logger middleware should invoke custom logger', async () => {
27
+ const logs: string[] = [];
28
+ const logger = createLoggerMiddleware({
29
+ logger: (message) => logs.push(message),
30
+ prefix: '[TestLogger]',
31
+ });
32
+
33
+ const ctx = createContext();
34
+ const response = await runMiddlewares(
35
+ [logger],
36
+ ctx,
37
+ async () => ctx.createResponse({ message: 'ok' }),
38
+ );
39
+
40
+ expect(await response.json()).toEqual({ message: 'ok' });
41
+ expect(logs).toContain('[TestLogger] GET /api/test 200');
42
+ });
43
+
44
+ test('logger middleware should log downstream status code', async () => {
45
+ const logs: string[] = [];
46
+ const logger = createLoggerMiddleware({
47
+ logger: (message) => logs.push(message),
48
+ prefix: '[TestLogger]',
49
+ });
50
+
51
+ const ctx = createContext();
52
+ const response = await runMiddlewares(
53
+ [logger],
54
+ ctx,
55
+ async () => {
56
+ ctx.setStatus(404);
57
+ return ctx.createResponse({ error: 'not found' });
58
+ },
59
+ );
60
+
61
+ expect(response.status).toBe(404);
62
+ expect(logs).toContain('[TestLogger] GET /api/test 404');
63
+ });
64
+
65
+ test('request logging middleware should set duration header', async () => {
66
+ const ctx = createContext();
67
+ const middleware = createRequestLoggingMiddleware({ setHeader: true });
68
+
69
+ const response = await runMiddlewares(
70
+ [middleware],
71
+ ctx,
72
+ async () => ctx.createResponse({ ok: true }),
73
+ );
74
+
75
+ expect(response.headers.get('x-request-duration')).toBeDefined();
76
+ });
77
+
78
+ test('request logging middleware should capture error details', async () => {
79
+ const logs: Array<{ message: string; details?: Record<string, unknown> }> = [];
80
+ const middleware = createRequestLoggingMiddleware({
81
+ logger: (message, details) => logs.push({ message, details }),
82
+ setHeader: false,
83
+ prefix: '[ReqTest]',
84
+ });
85
+
86
+ const ctx = createContext();
87
+ await expect(
88
+ runMiddlewares([middleware], ctx, async () => {
89
+ throw new Error('request failed');
90
+ }),
91
+ ).rejects.toThrow('request failed');
92
+
93
+ expect(logs[0]?.message).toContain('[ReqTest] GET /api/test error');
94
+ expect(logs[0]?.details?.error).toBe('request failed');
95
+ });
96
+
97
+ test('error handling middleware should capture errors', async () => {
98
+ const errors: string[] = [];
99
+ const middleware = createErrorHandlingMiddleware({
100
+ exposeError: true,
101
+ logger: (error) => {
102
+ errors.push(error instanceof Error ? error.message : String(error));
103
+ },
104
+ });
105
+
106
+ const ctx = createContext();
107
+ const response = await runMiddlewares([middleware], ctx, async () => {
108
+ throw new Error('boom');
109
+ });
110
+
111
+ expect(errors).toContain('boom');
112
+ expect(response.status).toBe(500);
113
+ expect(await response.json()).toEqual({ error: 'boom' });
114
+ });
115
+
116
+ test('error handling middleware should format validation errors', async () => {
117
+ const middleware = createErrorHandlingMiddleware();
118
+ const ctx = createContext();
119
+ const response = await runMiddlewares([middleware], ctx, async () => {
120
+ throw new ValidationError('invalid', [{ index: 0, rule: 'IsString', message: 'bad' }]);
121
+ });
122
+
123
+ expect(response.status).toBe(400);
124
+ expect(await response.json()).toEqual({
125
+ error: 'invalid',
126
+ issues: [{ index: 0, rule: 'IsString', message: 'bad' }],
127
+ });
128
+ });
129
+
130
+ test('error handling middleware should return response when downstream throws Response', async () => {
131
+ const middleware = createErrorHandlingMiddleware();
132
+ const ctx = createContext();
133
+ const downstream = new Response('prebuilt', { status: 418 });
134
+ const response = await runMiddlewares([middleware], ctx, async () => {
135
+ throw downstream;
136
+ });
137
+
138
+ expect(response.status).toBe(418);
139
+ expect(await response.text()).toBe('prebuilt');
140
+ });
141
+
142
+ test('error handling middleware should respect HttpException exposure flag', async () => {
143
+ const middleware = createErrorHandlingMiddleware();
144
+ const ctx = createContext();
145
+ const response = await runMiddlewares([middleware], ctx, async () => {
146
+ throw new HttpException(503, 'Service down', { retry: true });
147
+ });
148
+
149
+ expect(response.status).toBe(503);
150
+ expect(await response.json()).toEqual({
151
+ error: 'Service down',
152
+ details: { retry: true },
153
+ });
154
+ });
155
+
156
+ test('error handling middleware should expose HttpException when configured', async () => {
157
+ const middleware = createErrorHandlingMiddleware({ exposeError: true });
158
+ const ctx = createContext();
159
+ const response = await runMiddlewares([middleware], ctx, async () => {
160
+ throw new HttpException(429, 'Too Many Requests');
161
+ });
162
+
163
+ expect(response.status).toBe(429);
164
+ expect(await response.json()).toEqual({ error: 'Too Many Requests' });
165
+ });
166
+
167
+ test('cors middleware should handle options request', async () => {
168
+ const cors = createCorsMiddleware({
169
+ origin: ['http://localhost:4000'],
170
+ allowedHeaders: ['Content-Type'],
171
+ });
172
+
173
+ const ctx = createContext('http://localhost:3000/api/cors', 'OPTIONS');
174
+ const response = await runMiddlewares([cors], ctx, async () =>
175
+ ctx.createResponse({ ok: true }),
176
+ );
177
+
178
+ expect(response.status).toBe(204);
179
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('http://localhost:4000');
180
+ expect(response.headers.get('Access-Control-Allow-Headers')).toContain('Content-Type');
181
+ });
182
+
183
+ test('cors middleware should allow fine grained configuration', async () => {
184
+ const cors = createCorsMiddleware({
185
+ origin: ['http://foo.com', 'http://bar.com'],
186
+ methods: ['GET'],
187
+ allowedHeaders: ['X-Test'],
188
+ exposedHeaders: ['X-Expose'],
189
+ credentials: false,
190
+ maxAge: 10,
191
+ });
192
+
193
+ const ctx = createContext('http://localhost:3000/api/cors', 'GET');
194
+ ctx.headers.set('Origin', 'http://bar.com');
195
+ const response = await runMiddlewares([cors], ctx, async () => ctx.createResponse({}));
196
+
197
+ expect(response).toBeDefined();
198
+ expect(ctx.responseHeaders.get('Access-Control-Allow-Origin')).toBe('http://bar.com');
199
+ expect(ctx.responseHeaders.get('Access-Control-Allow-Credentials')).toBeNull();
200
+ expect(ctx.responseHeaders.get('Access-Control-Allow-Methods')).toBe('GET');
201
+ expect(ctx.responseHeaders.get('Access-Control-Expose-Headers')).toBe('X-Expose');
202
+ expect(ctx.responseHeaders.get('Access-Control-Max-Age')).toBe('10');
203
+ });
204
+ });
205
+
206
+
@@ -0,0 +1,41 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { Context } from '../../src/core/context';
4
+ import { createFileUploadMiddleware } from '../../src/middleware/builtin/file-upload';
5
+
6
+ function createMultipartRequest(boundary: string, body: string): Request {
7
+ return new Request('http://localhost/upload', {
8
+ method: 'POST',
9
+ headers: {
10
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
11
+ },
12
+ body,
13
+ });
14
+ }
15
+
16
+ describe('FileUploadMiddleware', () => {
17
+ test('should parse multipart form data and attach files', async () => {
18
+ const boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW';
19
+ const body =
20
+ `--${boundary}\r\n` +
21
+ `Content-Disposition: form-data; name="text"\r\n\r\n` +
22
+ `hello\r\n` +
23
+ `--${boundary}\r\n` +
24
+ `Content-Disposition: form-data; name="file"; filename="test.txt"\r\n` +
25
+ `Content-Type: text/plain\r\n\r\n` +
26
+ `file content\r\n` +
27
+ `--${boundary}--`;
28
+
29
+ const ctx = new Context(createMultipartRequest(boundary, body));
30
+ const middleware = createFileUploadMiddleware();
31
+
32
+ const response = await middleware(ctx, async () => ctx.createResponse({ ok: true }));
33
+ expect(response.status).toBe(200);
34
+
35
+ const bodyData = ctx.body as { fields: Record<string, unknown>; files: Record<string, unknown> };
36
+ expect(bodyData.fields.text).toBe('hello');
37
+ expect(bodyData.files.file[0].name).toBe('test.txt');
38
+ });
39
+ });
40
+
41
+