@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.
- package/package.json +4 -2
- package/readme.md +163 -2
- package/src/auth/controller.ts +148 -0
- package/src/auth/decorators.ts +81 -0
- package/src/auth/index.ts +12 -0
- package/src/auth/jwt.ts +169 -0
- package/src/auth/oauth2.ts +244 -0
- package/src/auth/types.ts +248 -0
- package/src/cache/cache-module.ts +67 -0
- package/src/cache/decorators.ts +202 -0
- package/src/cache/index.ts +27 -0
- package/src/cache/service.ts +151 -0
- package/src/cache/types.ts +420 -0
- package/src/config/config-module.ts +76 -0
- package/src/config/index.ts +8 -0
- package/src/config/service.ts +93 -0
- package/src/config/types.ts +27 -0
- package/src/controller/controller.ts +251 -0
- package/src/controller/decorators.ts +84 -0
- package/src/controller/index.ts +7 -0
- package/src/controller/metadata.ts +27 -0
- package/src/controller/param-binder.ts +157 -0
- package/src/core/application.ts +233 -0
- package/src/core/context.ts +228 -0
- package/src/core/index.ts +4 -0
- package/src/core/server.ts +128 -0
- package/src/core/types.ts +2 -0
- package/src/database/connection-manager.ts +239 -0
- package/src/database/connection-pool.ts +322 -0
- package/src/database/database-extension.ts +62 -0
- package/src/database/database-module.ts +115 -0
- package/src/database/health-indicator.ts +51 -0
- package/src/database/index.ts +47 -0
- package/src/database/orm/decorators.ts +155 -0
- package/src/database/orm/drizzle-repository.ts +39 -0
- package/src/database/orm/index.ts +23 -0
- package/src/database/orm/repository-decorator.ts +39 -0
- package/src/database/orm/repository.ts +103 -0
- package/src/database/orm/service.ts +49 -0
- package/src/database/orm/transaction-decorator.ts +45 -0
- package/src/database/orm/transaction-interceptor.ts +243 -0
- package/src/database/orm/transaction-manager.ts +276 -0
- package/src/database/orm/transaction-types.ts +140 -0
- package/src/database/orm/types.ts +99 -0
- package/src/database/service.ts +221 -0
- package/src/database/types.ts +171 -0
- package/src/di/container.ts +398 -0
- package/src/di/decorators.ts +228 -0
- package/src/di/index.ts +4 -0
- package/src/di/module-registry.ts +188 -0
- package/src/di/module.ts +65 -0
- package/src/di/types.ts +67 -0
- package/src/error/error-codes.ts +222 -0
- package/src/error/filter.ts +43 -0
- package/src/error/handler.ts +66 -0
- package/src/error/http-exception.ts +115 -0
- package/src/error/i18n.ts +217 -0
- package/src/error/index.ts +16 -0
- package/src/extensions/index.ts +5 -0
- package/src/extensions/logger-extension.ts +31 -0
- package/src/extensions/logger-module.ts +69 -0
- package/src/extensions/types.ts +14 -0
- package/src/files/index.ts +5 -0
- package/src/files/static-middleware.ts +53 -0
- package/src/files/storage.ts +67 -0
- package/src/files/types.ts +33 -0
- package/src/files/upload-middleware.ts +45 -0
- package/src/health/controller.ts +76 -0
- package/src/health/health-module.ts +51 -0
- package/src/health/index.ts +12 -0
- package/src/health/types.ts +28 -0
- package/src/index.ts +270 -0
- package/src/metrics/collector.ts +209 -0
- package/src/metrics/controller.ts +40 -0
- package/src/metrics/index.ts +15 -0
- package/src/metrics/metrics-module.ts +58 -0
- package/src/metrics/middleware.ts +46 -0
- package/src/metrics/prometheus.ts +79 -0
- package/src/metrics/types.ts +103 -0
- package/src/middleware/builtin/cors.ts +60 -0
- package/src/middleware/builtin/error-handler.ts +90 -0
- package/src/middleware/builtin/file-upload.ts +42 -0
- package/src/middleware/builtin/index.ts +14 -0
- package/src/middleware/builtin/logger.ts +91 -0
- package/src/middleware/builtin/rate-limit.ts +252 -0
- package/src/middleware/builtin/static-file.ts +88 -0
- package/src/middleware/decorators.ts +91 -0
- package/src/middleware/index.ts +11 -0
- package/src/middleware/middleware.ts +13 -0
- package/src/middleware/pipeline.ts +93 -0
- package/src/queue/decorators.ts +110 -0
- package/src/queue/index.ts +26 -0
- package/src/queue/queue-module.ts +64 -0
- package/src/queue/service.ts +302 -0
- package/src/queue/types.ts +341 -0
- package/src/request/body-parser.ts +133 -0
- package/src/request/file-handler.ts +46 -0
- package/src/request/index.ts +5 -0
- package/src/request/request.ts +107 -0
- package/src/request/response.ts +150 -0
- package/src/router/decorators.ts +122 -0
- package/src/router/index.ts +6 -0
- package/src/router/registry.ts +98 -0
- package/src/router/route.ts +140 -0
- package/src/router/router.ts +241 -0
- package/src/router/types.ts +27 -0
- package/src/security/access-decision-manager.ts +34 -0
- package/src/security/authentication-manager.ts +47 -0
- package/src/security/context.ts +92 -0
- package/src/security/filter.ts +162 -0
- package/src/security/index.ts +8 -0
- package/src/security/providers/index.ts +3 -0
- package/src/security/providers/jwt-provider.ts +60 -0
- package/src/security/providers/oauth2-provider.ts +70 -0
- package/src/security/security-module.ts +145 -0
- package/src/security/types.ts +165 -0
- package/src/session/decorators.ts +45 -0
- package/src/session/index.ts +19 -0
- package/src/session/middleware.ts +143 -0
- package/src/session/service.ts +218 -0
- package/src/session/session-module.ts +69 -0
- package/src/session/types.ts +373 -0
- package/src/swagger/decorators.ts +133 -0
- package/src/swagger/generator.ts +234 -0
- package/src/swagger/index.ts +7 -0
- package/src/swagger/swagger-extension.ts +41 -0
- package/src/swagger/swagger-module.ts +83 -0
- package/src/swagger/types.ts +188 -0
- package/src/swagger/ui.ts +98 -0
- package/src/testing/harness.ts +96 -0
- package/src/validation/decorators.ts +95 -0
- package/src/validation/errors.ts +28 -0
- package/src/validation/index.ts +14 -0
- package/src/validation/types.ts +35 -0
- package/src/validation/validator.ts +63 -0
- package/src/websocket/decorators.ts +51 -0
- package/src/websocket/index.ts +12 -0
- package/src/websocket/registry.ts +133 -0
- package/tests/cache/cache-module.test.ts +212 -0
- package/tests/config/config-module.test.ts +151 -0
- package/tests/controller/controller.test.ts +189 -0
- package/tests/core/application.test.ts +57 -0
- package/tests/core/context-body.test.ts +44 -0
- package/tests/core/context.test.ts +86 -0
- package/tests/core/edge-cases.test.ts +432 -0
- package/tests/database/database-module.test.ts +385 -0
- package/tests/database/orm.test.ts +164 -0
- package/tests/database/postgres-mysql-integration.test.ts +395 -0
- package/tests/database/transaction.test.ts +238 -0
- package/tests/di/container.test.ts +264 -0
- package/tests/di/module.test.ts +128 -0
- package/tests/error/error-codes.test.ts +121 -0
- package/tests/error/error-handler.test.ts +68 -0
- package/tests/error/error-handling.test.ts +254 -0
- package/tests/error/http-exception.test.ts +37 -0
- package/tests/error/i18n-integration.test.ts +175 -0
- package/tests/extensions/logger-extension.test.ts +40 -0
- package/tests/files/static-middleware.test.ts +67 -0
- package/tests/files/upload-middleware.test.ts +43 -0
- package/tests/health/health-module.test.ts +116 -0
- package/tests/integration/application-router.test.ts +85 -0
- package/tests/integration/body-parsing.test.ts +88 -0
- package/tests/integration/cache-e2e.test.ts +114 -0
- package/tests/integration/oauth2-e2e.test.ts +615 -0
- package/tests/integration/session-e2e.test.ts +207 -0
- package/tests/metrics/metrics-module.test.ts +178 -0
- package/tests/middleware/builtin.test.ts +206 -0
- package/tests/middleware/file-upload.test.ts +41 -0
- package/tests/middleware/middleware.test.ts +120 -0
- package/tests/middleware/pipeline.test.ts +72 -0
- package/tests/middleware/rate-limit.test.ts +314 -0
- package/tests/middleware/static-file.test.ts +62 -0
- package/tests/perf/harness.test.ts +48 -0
- package/tests/perf/optimization.test.ts +183 -0
- package/tests/perf/regression.test.ts +120 -0
- package/tests/queue/queue-module.test.ts +217 -0
- package/tests/request/body-parser.test.ts +96 -0
- package/tests/request/response.test.ts +99 -0
- package/tests/router/decorators.test.ts +48 -0
- package/tests/router/registry.test.ts +51 -0
- package/tests/router/route.test.ts +71 -0
- package/tests/router/router-normalization.test.ts +106 -0
- package/tests/router/router.test.ts +133 -0
- package/tests/security/access-decision-manager.test.ts +84 -0
- package/tests/security/authentication-manager.test.ts +81 -0
- package/tests/security/context.test.ts +302 -0
- package/tests/security/filter.test.ts +225 -0
- package/tests/security/jwt-provider.test.ts +106 -0
- package/tests/security/oauth2-provider.test.ts +269 -0
- package/tests/security/security-module.test.ts +143 -0
- package/tests/session/session-module.test.ts +307 -0
- package/tests/stress/di-stress.test.ts +30 -0
- package/tests/swagger/decorators.test.ts +153 -0
- package/tests/swagger/generator.test.ts +202 -0
- package/tests/swagger/swagger-extension.test.ts +72 -0
- package/tests/swagger/swagger-module.test.ts +79 -0
- package/tests/utils/test-port.ts +10 -0
- package/tests/validation/controller-validation.test.ts +64 -0
- package/tests/validation/validation.test.ts +42 -0
- package/tests/websocket/gateway.test.ts +68 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
HttpException,
|
|
4
|
+
ErrorCode,
|
|
5
|
+
ErrorMessageI18n,
|
|
6
|
+
ExceptionFilterRegistry,
|
|
7
|
+
ExceptionFilter,
|
|
8
|
+
BadRequestException,
|
|
9
|
+
NotFoundException,
|
|
10
|
+
type Context,
|
|
11
|
+
type MessageParams,
|
|
12
|
+
} from '../../src/error';
|
|
13
|
+
|
|
14
|
+
describe('Error Handling Enhancement', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// 重置语言为默认值
|
|
17
|
+
ErrorMessageI18n.setLanguage('en');
|
|
18
|
+
// 清除异常过滤器注册表
|
|
19
|
+
ExceptionFilterRegistry.getInstance().clear();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('ErrorCode System', () => {
|
|
23
|
+
test('should have all error codes defined', () => {
|
|
24
|
+
// 检查新增的错误码是否存在
|
|
25
|
+
expect(ErrorCode.DATABASE_CONNECTION_FAILED).toBe('DATABASE_CONNECTION_FAILED');
|
|
26
|
+
expect(ErrorCode.FILE_NOT_FOUND).toBe('FILE_NOT_FOUND');
|
|
27
|
+
expect(ErrorCode.RATE_LIMIT_EXCEEDED).toBe('RATE_LIMIT_EXCEEDED');
|
|
28
|
+
expect(ErrorCode.CONFIG_INVALID).toBe('CONFIG_INVALID');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should map error codes to HTTP status codes', () => {
|
|
32
|
+
const { ERROR_CODE_TO_STATUS } = require('../../src/error/error-codes');
|
|
33
|
+
|
|
34
|
+
expect(ERROR_CODE_TO_STATUS[ErrorCode.RATE_LIMIT_EXCEEDED]).toBe(429);
|
|
35
|
+
expect(ERROR_CODE_TO_STATUS[ErrorCode.DATABASE_CONNECTION_FAILED]).toBe(503);
|
|
36
|
+
expect(ERROR_CODE_TO_STATUS[ErrorCode.FILE_SIZE_EXCEEDED]).toBe(413);
|
|
37
|
+
expect(ERROR_CODE_TO_STATUS[ErrorCode.CONFIG_INVALID]).toBe(500);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('Error Message Internationalization', () => {
|
|
42
|
+
test('should get English message by default', () => {
|
|
43
|
+
const message = ErrorMessageI18n.getMessage(ErrorCode.RESOURCE_NOT_FOUND);
|
|
44
|
+
expect(message).toBe('Resource Not Found');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('should get Chinese message when language is zh-CN', () => {
|
|
48
|
+
const message = ErrorMessageI18n.getMessage(ErrorCode.RESOURCE_NOT_FOUND, 'zh-CN');
|
|
49
|
+
expect(message).toBe('资源未找到');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('should parse language from Accept-Language header', () => {
|
|
53
|
+
expect(ErrorMessageI18n.parseLanguageFromHeader('zh-CN,zh;q=0.9')).toBe('zh-CN');
|
|
54
|
+
expect(ErrorMessageI18n.parseLanguageFromHeader('ja,en;q=0.8')).toBe('ja');
|
|
55
|
+
expect(ErrorMessageI18n.parseLanguageFromHeader('ko,en;q=0.8')).toBe('ko');
|
|
56
|
+
expect(ErrorMessageI18n.parseLanguageFromHeader('en-US,en;q=0.9')).toBe('en');
|
|
57
|
+
expect(ErrorMessageI18n.parseLanguageFromHeader(null)).toBe('en');
|
|
58
|
+
expect(ErrorMessageI18n.parseLanguageFromHeader(undefined)).toBe('en');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should support message template with parameters', () => {
|
|
62
|
+
// 测试消息模板替换功能
|
|
63
|
+
const template = 'Resource {resource} not found';
|
|
64
|
+
const params: MessageParams = { resource: 'User' };
|
|
65
|
+
|
|
66
|
+
// 由于当前消息模板可能不包含占位符,我们测试替换功能
|
|
67
|
+
const result = template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
68
|
+
const value = params[key];
|
|
69
|
+
return value !== undefined ? String(value) : match;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result).toBe('Resource User not found');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('should set and get current language', () => {
|
|
76
|
+
ErrorMessageI18n.setLanguage('zh-CN');
|
|
77
|
+
expect(ErrorMessageI18n.getLanguage()).toBe('zh-CN');
|
|
78
|
+
|
|
79
|
+
ErrorMessageI18n.setLanguage('en');
|
|
80
|
+
expect(ErrorMessageI18n.getLanguage()).toBe('en');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('HttpException with Message Params', () => {
|
|
85
|
+
test('should create exception with message params', () => {
|
|
86
|
+
const exception = HttpException.withCode(
|
|
87
|
+
ErrorCode.RESOURCE_NOT_FOUND,
|
|
88
|
+
undefined,
|
|
89
|
+
undefined,
|
|
90
|
+
{ resource: 'User' },
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(exception.code).toBe(ErrorCode.RESOURCE_NOT_FOUND);
|
|
94
|
+
expect(exception.messageParams).toEqual({ resource: 'User' });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('should create exception with code and details', () => {
|
|
98
|
+
const exception = HttpException.withCode(
|
|
99
|
+
ErrorCode.VALIDATION_FAILED,
|
|
100
|
+
'Custom message',
|
|
101
|
+
{ field: 'email', reason: 'Invalid format' },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(exception.code).toBe(ErrorCode.VALIDATION_FAILED);
|
|
105
|
+
expect(exception.message).toBe('Custom message');
|
|
106
|
+
expect(exception.details).toEqual({ field: 'email', reason: 'Invalid format' });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('should create BadRequestException with message params', () => {
|
|
110
|
+
const exception = new BadRequestException(
|
|
111
|
+
'Bad Request',
|
|
112
|
+
undefined,
|
|
113
|
+
ErrorCode.INVALID_REQUEST,
|
|
114
|
+
{ field: 'id' },
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(exception.status).toBe(400);
|
|
118
|
+
expect(exception.messageParams).toEqual({ field: 'id' });
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Exception Filter', () => {
|
|
123
|
+
test('should register and execute exception filter', async () => {
|
|
124
|
+
const registry = ExceptionFilterRegistry.getInstance();
|
|
125
|
+
|
|
126
|
+
let filterExecuted = false;
|
|
127
|
+
|
|
128
|
+
const filter: ExceptionFilter = {
|
|
129
|
+
catch: (error: unknown, context: Context) => {
|
|
130
|
+
filterExecuted = true;
|
|
131
|
+
if (error instanceof HttpException && error.code === ErrorCode.RESOURCE_NOT_FOUND) {
|
|
132
|
+
return context.createResponse({
|
|
133
|
+
error: 'Custom not found message',
|
|
134
|
+
code: error.code,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
registry.register(filter);
|
|
142
|
+
|
|
143
|
+
const exception = HttpException.withCode(ErrorCode.RESOURCE_NOT_FOUND);
|
|
144
|
+
|
|
145
|
+
// 创建模拟的 Context
|
|
146
|
+
const mockContext = {
|
|
147
|
+
createResponse: (body: unknown) => new Response(JSON.stringify(body)),
|
|
148
|
+
getHeader: () => null,
|
|
149
|
+
setStatus: () => {},
|
|
150
|
+
getPath: () => '/test',
|
|
151
|
+
getMethod: () => 'GET',
|
|
152
|
+
} as unknown as Context;
|
|
153
|
+
|
|
154
|
+
const result = await registry.execute(exception, mockContext);
|
|
155
|
+
|
|
156
|
+
expect(filterExecuted).toBe(true);
|
|
157
|
+
expect(result).toBeDefined();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('should execute filters in registration order', async () => {
|
|
161
|
+
const registry = ExceptionFilterRegistry.getInstance();
|
|
162
|
+
const executionOrder: number[] = [];
|
|
163
|
+
|
|
164
|
+
const filter1: ExceptionFilter = {
|
|
165
|
+
catch: () => {
|
|
166
|
+
executionOrder.push(1);
|
|
167
|
+
return undefined;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const filter2: ExceptionFilter = {
|
|
172
|
+
catch: () => {
|
|
173
|
+
executionOrder.push(2);
|
|
174
|
+
return undefined;
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
registry.register(filter1);
|
|
179
|
+
registry.register(filter2);
|
|
180
|
+
|
|
181
|
+
const exception = HttpException.withCode(ErrorCode.INTERNAL_ERROR);
|
|
182
|
+
const mockContext = {
|
|
183
|
+
createResponse: (body: unknown) => new Response(JSON.stringify(body)),
|
|
184
|
+
getHeader: () => null,
|
|
185
|
+
setStatus: () => {},
|
|
186
|
+
getPath: () => '/test',
|
|
187
|
+
getMethod: () => 'GET',
|
|
188
|
+
} as unknown as Context;
|
|
189
|
+
|
|
190
|
+
await registry.execute(exception, mockContext);
|
|
191
|
+
|
|
192
|
+
expect(executionOrder).toEqual([1, 2]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('should stop execution when filter returns response', async () => {
|
|
196
|
+
const registry = ExceptionFilterRegistry.getInstance();
|
|
197
|
+
let secondFilterExecuted = false;
|
|
198
|
+
|
|
199
|
+
const filter1: ExceptionFilter = {
|
|
200
|
+
catch: (error: unknown, context: Context) => {
|
|
201
|
+
return context.createResponse({ error: 'Handled by filter 1' });
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const filter2: ExceptionFilter = {
|
|
206
|
+
catch: () => {
|
|
207
|
+
secondFilterExecuted = true;
|
|
208
|
+
return undefined;
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
registry.register(filter1);
|
|
213
|
+
registry.register(filter2);
|
|
214
|
+
|
|
215
|
+
const exception = HttpException.withCode(ErrorCode.INTERNAL_ERROR);
|
|
216
|
+
const mockContext = {
|
|
217
|
+
createResponse: (body: unknown) => new Response(JSON.stringify(body)),
|
|
218
|
+
getHeader: () => null,
|
|
219
|
+
setStatus: () => {},
|
|
220
|
+
getPath: () => '/test',
|
|
221
|
+
getMethod: () => 'GET',
|
|
222
|
+
} as unknown as Context;
|
|
223
|
+
|
|
224
|
+
await registry.execute(exception, mockContext);
|
|
225
|
+
|
|
226
|
+
expect(secondFilterExecuted).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('Error Code Coverage', () => {
|
|
231
|
+
test('should have messages for all error codes', () => {
|
|
232
|
+
const { ERROR_CODE_MESSAGES } = require('../../src/error/error-codes');
|
|
233
|
+
|
|
234
|
+
// 检查所有错误码都有对应的消息
|
|
235
|
+
Object.values(ErrorCode).forEach((code) => {
|
|
236
|
+
expect(ERROR_CODE_MESSAGES[code]).toBeDefined();
|
|
237
|
+
expect(typeof ERROR_CODE_MESSAGES[code]).toBe('string');
|
|
238
|
+
expect(ERROR_CODE_MESSAGES[code].length).toBeGreaterThan(0);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('should have status codes for all error codes', () => {
|
|
243
|
+
const { ERROR_CODE_TO_STATUS } = require('../../src/error/error-codes');
|
|
244
|
+
|
|
245
|
+
// 检查所有错误码都有对应的 HTTP 状态码
|
|
246
|
+
Object.values(ErrorCode).forEach((code) => {
|
|
247
|
+
expect(ERROR_CODE_TO_STATUS[code]).toBeDefined();
|
|
248
|
+
expect(typeof ERROR_CODE_TO_STATUS[code]).toBe('number');
|
|
249
|
+
expect(ERROR_CODE_TO_STATUS[code]).toBeGreaterThanOrEqual(400);
|
|
250
|
+
expect(ERROR_CODE_TO_STATUS[code]).toBeLessThan(600);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
HttpException,
|
|
5
|
+
BadRequestException,
|
|
6
|
+
NotFoundException,
|
|
7
|
+
UnauthorizedException,
|
|
8
|
+
ForbiddenException,
|
|
9
|
+
InternalServerErrorException,
|
|
10
|
+
} from '../../src/error/http-exception';
|
|
11
|
+
|
|
12
|
+
describe('HttpException', () => {
|
|
13
|
+
test('should preserve status and message', () => {
|
|
14
|
+
const error = new HttpException(418, 'I am a teapot', { foo: 'bar' });
|
|
15
|
+
expect(error.status).toBe(418);
|
|
16
|
+
expect(error.message).toBe('I am a teapot');
|
|
17
|
+
expect(error.details).toEqual({ foo: 'bar' });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should provide convenient subclasses', () => {
|
|
21
|
+
const badRequest = new BadRequestException('Invalid body');
|
|
22
|
+
const notFound = new NotFoundException();
|
|
23
|
+
const unauthorized = new UnauthorizedException();
|
|
24
|
+
const forbidden = new ForbiddenException('Stop');
|
|
25
|
+
const internal = new InternalServerErrorException(undefined, { traceId: 'abc' });
|
|
26
|
+
|
|
27
|
+
expect(badRequest.status).toBe(400);
|
|
28
|
+
expect(badRequest.message).toBe('Invalid body');
|
|
29
|
+
expect(notFound.status).toBe(404);
|
|
30
|
+
expect(notFound.message).toBe('Not Found');
|
|
31
|
+
expect(unauthorized.status).toBe(401);
|
|
32
|
+
expect(forbidden.message).toBe('Stop');
|
|
33
|
+
expect(internal.details).toEqual({ traceId: 'abc' });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { Application } from '../../src/core/application';
|
|
3
|
+
import { Controller } from '../../src/controller';
|
|
4
|
+
import { GET } from '../../src/router/decorators';
|
|
5
|
+
import { Auth } from '../../src/auth/decorators';
|
|
6
|
+
import {
|
|
7
|
+
UnauthorizedException,
|
|
8
|
+
ForbiddenException,
|
|
9
|
+
NotFoundException,
|
|
10
|
+
} from '../../src/error/http-exception';
|
|
11
|
+
import { ErrorCode } from '../../src/error/error-codes';
|
|
12
|
+
import { SecurityModule } from '../../src/security/security-module';
|
|
13
|
+
import { RouteRegistry } from '../../src/router/registry';
|
|
14
|
+
import { ControllerRegistry } from '../../src/controller/controller';
|
|
15
|
+
import { ModuleRegistry } from '../../src/di/module-registry';
|
|
16
|
+
import { ExceptionFilterRegistry } from '../../src/error/filter';
|
|
17
|
+
import { getTestPort } from '../utils/test-port';
|
|
18
|
+
import type { Context } from '../../src/core/context';
|
|
19
|
+
|
|
20
|
+
let port: number;
|
|
21
|
+
let app: Application;
|
|
22
|
+
|
|
23
|
+
@Controller('/api/test')
|
|
24
|
+
class TestController {
|
|
25
|
+
@GET('/unauthorized')
|
|
26
|
+
@Auth()
|
|
27
|
+
public unauthorized() {
|
|
28
|
+
throw new UnauthorizedException('Test unauthorized');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@GET('/forbidden')
|
|
32
|
+
@Auth({ roles: ['admin'] })
|
|
33
|
+
public forbidden(ctx: Context) {
|
|
34
|
+
// 这个端点需要 admin 角色,但用户只有 user 角色
|
|
35
|
+
return ctx.createResponse({ message: 'ok' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@GET('/not-found')
|
|
39
|
+
public notFound() {
|
|
40
|
+
throw new NotFoundException('Resource not found', undefined, ErrorCode.RESOURCE_NOT_FOUND);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('Error I18n Integration', () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
port = getTestPort();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
if (app) {
|
|
51
|
+
await app.stop();
|
|
52
|
+
}
|
|
53
|
+
RouteRegistry.getInstance().clear();
|
|
54
|
+
ControllerRegistry.getInstance().clear();
|
|
55
|
+
ModuleRegistry.getInstance().clear();
|
|
56
|
+
ExceptionFilterRegistry.getInstance().clear();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should return error code in response', async () => {
|
|
60
|
+
app = new Application({ port });
|
|
61
|
+
SecurityModule.forRoot({
|
|
62
|
+
jwt: {
|
|
63
|
+
secret: 'test-secret',
|
|
64
|
+
accessTokenExpiresIn: 3600,
|
|
65
|
+
},
|
|
66
|
+
defaultAuthRequired: false,
|
|
67
|
+
});
|
|
68
|
+
app.registerModule(SecurityModule);
|
|
69
|
+
app.registerController(TestController);
|
|
70
|
+
await app.listen();
|
|
71
|
+
|
|
72
|
+
const response = await fetch(`http://localhost:${port}/api/test/not-found`);
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
|
|
75
|
+
expect(response.status).toBe(404);
|
|
76
|
+
// 调试:打印实际响应
|
|
77
|
+
if (!data.code) {
|
|
78
|
+
console.log('Actual response:', JSON.stringify(data, null, 2));
|
|
79
|
+
}
|
|
80
|
+
expect(data.code).toBe(ErrorCode.RESOURCE_NOT_FOUND);
|
|
81
|
+
expect(data.error).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('should return English error message by default', async () => {
|
|
85
|
+
app = new Application({ port });
|
|
86
|
+
SecurityModule.forRoot({
|
|
87
|
+
jwt: {
|
|
88
|
+
secret: 'test-secret',
|
|
89
|
+
accessTokenExpiresIn: 3600,
|
|
90
|
+
},
|
|
91
|
+
defaultAuthRequired: false,
|
|
92
|
+
});
|
|
93
|
+
app.registerModule(SecurityModule);
|
|
94
|
+
app.registerController(TestController);
|
|
95
|
+
await app.listen();
|
|
96
|
+
|
|
97
|
+
const response = await fetch(`http://localhost:${port}/api/test/not-found`);
|
|
98
|
+
const data = await response.json();
|
|
99
|
+
|
|
100
|
+
expect(response.status).toBe(404);
|
|
101
|
+
expect(data.error).toBe('Resource Not Found');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('should return Chinese error message when Accept-Language is zh-CN', async () => {
|
|
105
|
+
app = new Application({ port });
|
|
106
|
+
SecurityModule.forRoot({
|
|
107
|
+
jwt: {
|
|
108
|
+
secret: 'test-secret',
|
|
109
|
+
accessTokenExpiresIn: 3600,
|
|
110
|
+
},
|
|
111
|
+
defaultAuthRequired: false,
|
|
112
|
+
});
|
|
113
|
+
app.registerModule(SecurityModule);
|
|
114
|
+
app.registerController(TestController);
|
|
115
|
+
await app.listen();
|
|
116
|
+
|
|
117
|
+
const response = await fetch(`http://localhost:${port}/api/test/not-found`, {
|
|
118
|
+
headers: {
|
|
119
|
+
'Accept-Language': 'zh-CN,zh;q=0.9',
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
const data = await response.json();
|
|
123
|
+
|
|
124
|
+
expect(response.status).toBe(404);
|
|
125
|
+
expect(data.code).toBe(ErrorCode.RESOURCE_NOT_FOUND);
|
|
126
|
+
expect(data.error).toBe('资源未找到');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('should return error code for authentication errors', async () => {
|
|
130
|
+
app = new Application({ port });
|
|
131
|
+
SecurityModule.forRoot({
|
|
132
|
+
jwt: {
|
|
133
|
+
secret: 'test-secret',
|
|
134
|
+
accessTokenExpiresIn: 3600,
|
|
135
|
+
},
|
|
136
|
+
defaultAuthRequired: false,
|
|
137
|
+
});
|
|
138
|
+
app.registerModule(SecurityModule);
|
|
139
|
+
app.registerController(TestController);
|
|
140
|
+
await app.listen();
|
|
141
|
+
|
|
142
|
+
const response = await fetch(`http://localhost:${port}/api/test/unauthorized`);
|
|
143
|
+
const data = await response.json();
|
|
144
|
+
|
|
145
|
+
expect(response.status).toBe(401);
|
|
146
|
+
expect(data.code).toBe(ErrorCode.AUTH_REQUIRED);
|
|
147
|
+
expect(data.error).toBeDefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('should return Chinese error message for authentication errors', async () => {
|
|
151
|
+
app = new Application({ port });
|
|
152
|
+
SecurityModule.forRoot({
|
|
153
|
+
jwt: {
|
|
154
|
+
secret: 'test-secret',
|
|
155
|
+
accessTokenExpiresIn: 3600,
|
|
156
|
+
},
|
|
157
|
+
defaultAuthRequired: false,
|
|
158
|
+
});
|
|
159
|
+
app.registerModule(SecurityModule);
|
|
160
|
+
app.registerController(TestController);
|
|
161
|
+
await app.listen();
|
|
162
|
+
|
|
163
|
+
const response = await fetch(`http://localhost:${port}/api/test/unauthorized`, {
|
|
164
|
+
headers: {
|
|
165
|
+
'Accept-Language': 'zh-CN',
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
const data = await response.json();
|
|
169
|
+
|
|
170
|
+
expect(response.status).toBe(401);
|
|
171
|
+
expect(data.code).toBe(ErrorCode.AUTH_REQUIRED);
|
|
172
|
+
expect(data.error).toBe('需要认证');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { Container } from '../../src/di/container';
|
|
4
|
+
import { LoggerExtension, LOGGER_TOKEN, LogLevel, type LogEntry } from '../../src/extensions';
|
|
5
|
+
import type { Logger } from '../../src/extensions';
|
|
6
|
+
|
|
7
|
+
describe('LoggerExtension', () => {
|
|
8
|
+
test('should register logger instance into container', () => {
|
|
9
|
+
const container = new Container();
|
|
10
|
+
const extension = new LoggerExtension({ prefix: 'Test', level: LogLevel.DEBUG });
|
|
11
|
+
|
|
12
|
+
extension.register(container);
|
|
13
|
+
|
|
14
|
+
const logger = container.resolve(LOGGER_TOKEN) as Logger;
|
|
15
|
+
expect(logger).toBeDefined();
|
|
16
|
+
expect(() => logger.info('hello')).not.toThrow();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should use custom sink function', () => {
|
|
20
|
+
const container = new Container();
|
|
21
|
+
const entries: LogEntry[] = [];
|
|
22
|
+
|
|
23
|
+
const extension = new LoggerExtension({
|
|
24
|
+
sink(entry) {
|
|
25
|
+
entries.push(entry);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
extension.register(container);
|
|
30
|
+
|
|
31
|
+
const logger = container.resolve(LOGGER_TOKEN) as Logger;
|
|
32
|
+
logger.warn('warn message');
|
|
33
|
+
|
|
34
|
+
expect(entries.length).toBe(1);
|
|
35
|
+
expect(entries[0].message).toBe('warn message');
|
|
36
|
+
expect(entries[0].level).toBe(LogLevel.WARN);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
|
|
6
|
+
import { createStaticFileMiddleware } from '../../src/files/static-middleware';
|
|
7
|
+
import { Context } from '../../src/core/context';
|
|
8
|
+
|
|
9
|
+
describe('Static File Middleware', () => {
|
|
10
|
+
let root: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
root = await mkdtemp(join(tmpdir(), 'bun-static-'));
|
|
14
|
+
await Bun.write(join(root, 'hello.txt'), 'hello world');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
await rm(root, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should serve static files by prefix', async () => {
|
|
22
|
+
const middleware = createStaticFileMiddleware({ root, prefix: '/static' });
|
|
23
|
+
const request = new Request('http://localhost/static/hello.txt');
|
|
24
|
+
const context = new Context(request);
|
|
25
|
+
|
|
26
|
+
const response = await middleware(context, async () => context.createResponse({ error: '404' }));
|
|
27
|
+
expect(response.status).toBe(200);
|
|
28
|
+
expect(await response.text()).toBe('hello world');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should fall through when file missing', async () => {
|
|
32
|
+
const middleware = createStaticFileMiddleware({ root, prefix: '/static' });
|
|
33
|
+
const request = new Request('http://localhost/static/missing.txt');
|
|
34
|
+
const context = new Context(request);
|
|
35
|
+
|
|
36
|
+
const response = await middleware(context, async () =>
|
|
37
|
+
context.createResponse({ fallback: true }, { status: 404 }),
|
|
38
|
+
);
|
|
39
|
+
expect(response.status).toBe(404);
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
expect(data.fallback).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should return 404 when fallthrough disabled', async () => {
|
|
45
|
+
const middleware = createStaticFileMiddleware({ root, prefix: '/files', fallthrough: false });
|
|
46
|
+
const request = new Request('http://localhost/files/absent.txt');
|
|
47
|
+
const context = new Context(request);
|
|
48
|
+
|
|
49
|
+
const response = await middleware(context, async () =>
|
|
50
|
+
context.createResponse({ fallback: false }, { status: 200 }),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(response.status).toBe(404);
|
|
54
|
+
expect(await response.json()).toEqual({ error: 'File Not Found' });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should skip non GET/HEAD methods', async () => {
|
|
58
|
+
const middleware = createStaticFileMiddleware({ root, prefix: '/static' });
|
|
59
|
+
const request = new Request('http://localhost/static/hello.txt', { method: 'POST' });
|
|
60
|
+
const context = new Context(request);
|
|
61
|
+
|
|
62
|
+
const response = await middleware(context, async () => context.createResponse({ passthrough: true }));
|
|
63
|
+
expect(await response.json()).toEqual({ passthrough: true });
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
|
|
6
|
+
import { createFileUploadMiddleware } from '../../src/files/upload-middleware';
|
|
7
|
+
import { Context } from '../../src/core/context';
|
|
8
|
+
|
|
9
|
+
describe('File Upload Middleware', () => {
|
|
10
|
+
let dest: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
dest = await mkdtemp(join(tmpdir(), 'bun-upload-'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await rm(dest, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should store uploaded files and populate context.files', async () => {
|
|
21
|
+
const middleware = createFileUploadMiddleware({ dest });
|
|
22
|
+
const form = new FormData();
|
|
23
|
+
form.append('avatar', new File([new Blob(['hello'])], 'avatar.txt', { type: 'text/plain' }));
|
|
24
|
+
|
|
25
|
+
const request = new Request('http://localhost/upload', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
body: form,
|
|
28
|
+
});
|
|
29
|
+
const context = new Context(request);
|
|
30
|
+
|
|
31
|
+
await middleware(context, async () => context.createResponse({ ok: true }));
|
|
32
|
+
|
|
33
|
+
expect(context.files.length).toBe(1);
|
|
34
|
+
const [file] = context.files;
|
|
35
|
+
expect(file.fieldName).toBe('avatar');
|
|
36
|
+
expect(file.mimeType).toBe('text/plain;charset=utf-8');
|
|
37
|
+
const saved = Bun.file(join(dest, file.filename));
|
|
38
|
+
expect(await saved.exists()).toBe(true);
|
|
39
|
+
expect(await saved.text()).toBe('hello');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
|