@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,264 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
3
|
+
import { Container } from '../../src/di/container';
|
|
4
|
+
import { Lifecycle } from '../../src/di/types';
|
|
5
|
+
import { Injectable, Inject } from '../../src/di/decorators';
|
|
6
|
+
import { Controller, ControllerRegistry } from '../../src/controller/controller';
|
|
7
|
+
import { GET } from '../../src/router/decorators';
|
|
8
|
+
import { RouteRegistry } from '../../src/router/registry';
|
|
9
|
+
import { Context } from '../../src/core/context';
|
|
10
|
+
|
|
11
|
+
describe('Container', () => {
|
|
12
|
+
let container: Container;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
container = new Container();
|
|
16
|
+
// 清理全局注册表,避免测试间污染
|
|
17
|
+
RouteRegistry.getInstance().clear();
|
|
18
|
+
ControllerRegistry.getInstance().clear();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should create container instance', () => {
|
|
22
|
+
expect(container).toBeInstanceOf(Container);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should register and resolve singleton', () => {
|
|
26
|
+
class TestService {
|
|
27
|
+
public value = 'test';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
container.register(TestService, { lifecycle: Lifecycle.Singleton });
|
|
31
|
+
|
|
32
|
+
const instance1 = container.resolve(TestService);
|
|
33
|
+
const instance2 = container.resolve(TestService);
|
|
34
|
+
|
|
35
|
+
expect(instance1).toBe(instance2);
|
|
36
|
+
expect(instance1.value).toBe('test');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('should register and resolve transient', () => {
|
|
40
|
+
class TestService {
|
|
41
|
+
public value = 'test';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
container.register(TestService, { lifecycle: Lifecycle.Transient });
|
|
45
|
+
|
|
46
|
+
const instance1 = container.resolve(TestService);
|
|
47
|
+
const instance2 = container.resolve(TestService);
|
|
48
|
+
|
|
49
|
+
expect(instance1).not.toBe(instance2);
|
|
50
|
+
expect(instance1.value).toBe('test');
|
|
51
|
+
expect(instance2.value).toBe('test');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should resolve with constructor injection', () => {
|
|
55
|
+
@Injectable()
|
|
56
|
+
class Dependency {
|
|
57
|
+
public name = 'dependency';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Injectable()
|
|
61
|
+
class Service {
|
|
62
|
+
public dependency: Dependency;
|
|
63
|
+
|
|
64
|
+
public constructor(@Inject(Dependency) dependency: Dependency) {
|
|
65
|
+
this.dependency = dependency;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
container.register(Dependency);
|
|
70
|
+
container.register(Service);
|
|
71
|
+
|
|
72
|
+
const service = container.resolve(Service);
|
|
73
|
+
|
|
74
|
+
expect(service).toBeInstanceOf(Service);
|
|
75
|
+
expect(service.dependency).toBeInstanceOf(Dependency);
|
|
76
|
+
expect(service.dependency.name).toBe('dependency');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('should resolve nested dependencies', () => {
|
|
80
|
+
@Injectable()
|
|
81
|
+
class Level1 {
|
|
82
|
+
public name = 'level1';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Injectable()
|
|
86
|
+
class Level2 {
|
|
87
|
+
public level1: Level1;
|
|
88
|
+
|
|
89
|
+
public constructor(@Inject(Level1) level1: Level1) {
|
|
90
|
+
this.level1 = level1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Injectable()
|
|
95
|
+
class Level3 {
|
|
96
|
+
public level2: Level2;
|
|
97
|
+
|
|
98
|
+
public constructor(@Inject(Level2) level2: Level2) {
|
|
99
|
+
this.level2 = level2;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
container.register(Level1);
|
|
104
|
+
container.register(Level2);
|
|
105
|
+
container.register(Level3);
|
|
106
|
+
|
|
107
|
+
const level3 = container.resolve(Level3);
|
|
108
|
+
|
|
109
|
+
expect(level3).toBeInstanceOf(Level3);
|
|
110
|
+
expect(level3.level2).toBeInstanceOf(Level2);
|
|
111
|
+
expect(level3.level2.level1).toBeInstanceOf(Level1);
|
|
112
|
+
expect(level3.level2.level1.name).toBe('level1');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should resolve dependency without explicit registration when constructor has no deps', () => {
|
|
116
|
+
@Injectable()
|
|
117
|
+
class PlainService {
|
|
118
|
+
public id = 'plain';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Injectable()
|
|
122
|
+
class Consumer {
|
|
123
|
+
public constructor(public readonly service: PlainService) {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
container.register(Consumer);
|
|
127
|
+
|
|
128
|
+
const consumer = container.resolve(Consumer);
|
|
129
|
+
expect(consumer.service).toBeInstanceOf(PlainService);
|
|
130
|
+
expect(consumer.service.id).toBe('plain');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('should use factory function', () => {
|
|
134
|
+
class TestService {
|
|
135
|
+
public value: string;
|
|
136
|
+
|
|
137
|
+
public constructor(value: string) {
|
|
138
|
+
this.value = value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
container.register(TestService, {
|
|
143
|
+
factory: () => new TestService('factory-created'),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const instance = container.resolve(TestService);
|
|
147
|
+
|
|
148
|
+
expect(instance).toBeInstanceOf(TestService);
|
|
149
|
+
expect(instance.value).toBe('factory-created');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('should register and resolve instance', () => {
|
|
153
|
+
class TestService {
|
|
154
|
+
public value = 'pre-created';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const instance = new TestService();
|
|
158
|
+
container.registerInstance(TestService, instance);
|
|
159
|
+
|
|
160
|
+
const resolved = container.resolve(TestService);
|
|
161
|
+
|
|
162
|
+
expect(resolved).toBe(instance);
|
|
163
|
+
expect(resolved.value).toBe('pre-created');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('should resolve dependencies registered in container via controller registry', () => {
|
|
167
|
+
@Injectable()
|
|
168
|
+
class Service {
|
|
169
|
+
public id = 'service';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@Injectable()
|
|
173
|
+
class Controller {
|
|
174
|
+
public constructor(@Inject(Service) public readonly service: Service) {}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
container.register(Service);
|
|
178
|
+
container.register(Controller);
|
|
179
|
+
|
|
180
|
+
const instance = container.resolve(Controller);
|
|
181
|
+
expect(instance.service).toBeInstanceOf(Service);
|
|
182
|
+
expect(instance.service.id).toBe('service');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('should resolve service when controller registered through registry', async () => {
|
|
186
|
+
@Injectable()
|
|
187
|
+
class Service {
|
|
188
|
+
public name = 'svc';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@Controller('/test-di')
|
|
192
|
+
class TestController {
|
|
193
|
+
public constructor(@Inject(Service) public readonly svc: Service) {}
|
|
194
|
+
|
|
195
|
+
@GET('/')
|
|
196
|
+
public get() {
|
|
197
|
+
return this.svc.name;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const controllerRegistry = ControllerRegistry.getInstance();
|
|
202
|
+
const routeRegistry = RouteRegistry.getInstance();
|
|
203
|
+
controllerRegistry.clear();
|
|
204
|
+
routeRegistry.clear();
|
|
205
|
+
controllerRegistry.getContainer().register(Service);
|
|
206
|
+
controllerRegistry.register(TestController);
|
|
207
|
+
|
|
208
|
+
const router = routeRegistry.getRouter();
|
|
209
|
+
const request = new Request('http://localhost/test-di');
|
|
210
|
+
const context = new Context(request);
|
|
211
|
+
|
|
212
|
+
const response = await router.handle(context);
|
|
213
|
+
expect(response?.status).toBe(200);
|
|
214
|
+
expect(await response?.text()).toBe('svc');
|
|
215
|
+
controllerRegistry.clear();
|
|
216
|
+
routeRegistry.clear();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('should check if registered', () => {
|
|
220
|
+
class TestService {}
|
|
221
|
+
|
|
222
|
+
expect(container.isRegistered(TestService)).toBe(false);
|
|
223
|
+
|
|
224
|
+
container.register(TestService);
|
|
225
|
+
|
|
226
|
+
expect(container.isRegistered(TestService)).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('should clear all registrations', () => {
|
|
230
|
+
class TestService {}
|
|
231
|
+
|
|
232
|
+
container.register(TestService);
|
|
233
|
+
expect(container.isRegistered(TestService)).toBe(true);
|
|
234
|
+
|
|
235
|
+
container.clear();
|
|
236
|
+
expect(container.isRegistered(TestService)).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('should throw error for unregistered provider with dependencies', () => {
|
|
240
|
+
@Injectable()
|
|
241
|
+
class Dependency {}
|
|
242
|
+
|
|
243
|
+
@Injectable()
|
|
244
|
+
class UnregisteredService {
|
|
245
|
+
public constructor(@Inject() dependency: Dependency) {}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 只注册 Dependency,不注册 UnregisteredService
|
|
249
|
+
container.register(Dependency);
|
|
250
|
+
|
|
251
|
+
// 尝试解析未注册的服务,应该抛出错误
|
|
252
|
+
expect(() => {
|
|
253
|
+
container.resolve(UnregisteredService);
|
|
254
|
+
}).toThrow('Provider not found');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// 注意:循环依赖测试暂时跳过
|
|
258
|
+
// 因为类定义时的循环引用会导致编译错误
|
|
259
|
+
// 循环依赖检测可以在后续版本中实现
|
|
260
|
+
// test('should handle circular dependencies', () => {
|
|
261
|
+
// // 循环依赖会导致无限递归,实际使用中应该避免
|
|
262
|
+
// });
|
|
263
|
+
});
|
|
264
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { beforeEach, afterEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { Application } from '../../src/core/application';
|
|
4
|
+
import { Controller, ControllerRegistry } from '../../src/controller/controller';
|
|
5
|
+
import { GET } from '../../src/router/decorators';
|
|
6
|
+
import { Module } from '../../src/di/module';
|
|
7
|
+
import { ModuleRegistry } from '../../src/di/module-registry';
|
|
8
|
+
import { RouteRegistry } from '../../src/router/registry';
|
|
9
|
+
import { Injectable, Inject } from '../../src/di/decorators';
|
|
10
|
+
import { Context } from '../../src/core/context';
|
|
11
|
+
|
|
12
|
+
describe('ModuleRegistry', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// 清理全局注册表,避免测试间污染
|
|
15
|
+
RouteRegistry.getInstance().clear();
|
|
16
|
+
ControllerRegistry.getInstance().clear();
|
|
17
|
+
ModuleRegistry.getInstance().clear();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
// 确保测试后清理
|
|
22
|
+
RouteRegistry.getInstance().clear();
|
|
23
|
+
ControllerRegistry.getInstance().clear();
|
|
24
|
+
ModuleRegistry.getInstance().clear();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should register module providers and controllers', async () => {
|
|
28
|
+
@Injectable()
|
|
29
|
+
class UserService {
|
|
30
|
+
public list(): string {
|
|
31
|
+
return 'users';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Controller('/module-users')
|
|
36
|
+
class UserController {
|
|
37
|
+
public constructor(private readonly service: UserService) {}
|
|
38
|
+
|
|
39
|
+
@GET('/')
|
|
40
|
+
public getUsers(): string {
|
|
41
|
+
return this.service.list();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Module({
|
|
46
|
+
controllers: [UserController],
|
|
47
|
+
providers: [UserService],
|
|
48
|
+
exports: [UserService],
|
|
49
|
+
})
|
|
50
|
+
class UserModule {}
|
|
51
|
+
|
|
52
|
+
const app = new Application();
|
|
53
|
+
app.registerModule(UserModule);
|
|
54
|
+
|
|
55
|
+
const moduleRef = ModuleRegistry.getInstance().getModuleRef(UserModule);
|
|
56
|
+
expect(moduleRef).toBeDefined();
|
|
57
|
+
expect(moduleRef?.container.resolve(UserService).list()).toBe('users');
|
|
58
|
+
|
|
59
|
+
const router = RouteRegistry.getInstance().getRouter();
|
|
60
|
+
const context = new Context(new Request('http://localhost/module-users'));
|
|
61
|
+
const response = await router.handle(context);
|
|
62
|
+
expect(await response?.text()).toBe('users');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('should share exported providers between imported modules', () => {
|
|
66
|
+
@Injectable()
|
|
67
|
+
class SharedService {
|
|
68
|
+
public readonly id = Math.random();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Module({
|
|
72
|
+
providers: [SharedService],
|
|
73
|
+
exports: [SharedService],
|
|
74
|
+
})
|
|
75
|
+
class SharedModule {}
|
|
76
|
+
|
|
77
|
+
@Injectable()
|
|
78
|
+
class FeatureService {
|
|
79
|
+
public constructor(@Inject(SharedService) public readonly shared: SharedService) {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Module({
|
|
83
|
+
imports: [SharedModule],
|
|
84
|
+
providers: [FeatureService],
|
|
85
|
+
exports: [FeatureService],
|
|
86
|
+
})
|
|
87
|
+
class FeatureModule {}
|
|
88
|
+
|
|
89
|
+
const app = new Application();
|
|
90
|
+
app.registerModule(FeatureModule);
|
|
91
|
+
|
|
92
|
+
const registry = ModuleRegistry.getInstance();
|
|
93
|
+
const sharedRef = registry.getModuleRef(SharedModule);
|
|
94
|
+
const featureRef = registry.getModuleRef(FeatureModule);
|
|
95
|
+
expect(sharedRef).toBeDefined();
|
|
96
|
+
expect(featureRef).toBeDefined();
|
|
97
|
+
|
|
98
|
+
const sharedFromFeature = featureRef!.container.resolve(SharedService);
|
|
99
|
+
const sharedFromModule = sharedRef!.container.resolve(SharedService);
|
|
100
|
+
expect(sharedFromFeature).toBe(sharedFromModule);
|
|
101
|
+
|
|
102
|
+
const featureService = featureRef!.container.resolve(FeatureService);
|
|
103
|
+
expect(featureService.shared).toBe(sharedFromModule);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('should throw error for circular module dependencies', () => {
|
|
107
|
+
@Module({
|
|
108
|
+
imports: [],
|
|
109
|
+
})
|
|
110
|
+
class ModuleA {}
|
|
111
|
+
|
|
112
|
+
@Module({
|
|
113
|
+
imports: [ModuleA],
|
|
114
|
+
})
|
|
115
|
+
class ModuleB {}
|
|
116
|
+
|
|
117
|
+
// 重新装饰 ModuleA,使其导入 ModuleB,形成环
|
|
118
|
+
Module({
|
|
119
|
+
imports: [ModuleB],
|
|
120
|
+
})(ModuleA);
|
|
121
|
+
|
|
122
|
+
const app = new Application();
|
|
123
|
+
expect(() => app.registerModule(ModuleA)).toThrowError(
|
|
124
|
+
/Circular module dependency detected/,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
HttpException,
|
|
4
|
+
BadRequestException,
|
|
5
|
+
UnauthorizedException,
|
|
6
|
+
ForbiddenException,
|
|
7
|
+
NotFoundException,
|
|
8
|
+
InternalServerErrorException,
|
|
9
|
+
} from '../../src/error/http-exception';
|
|
10
|
+
import { ErrorCode, ERROR_CODE_MESSAGES, ERROR_CODE_TO_STATUS } from '../../src/error/error-codes';
|
|
11
|
+
import { ErrorMessageI18n } from '../../src/error/i18n';
|
|
12
|
+
|
|
13
|
+
describe('ErrorCode', () => {
|
|
14
|
+
test('should have all error codes defined', () => {
|
|
15
|
+
expect(ErrorCode.INTERNAL_ERROR).toBe('INTERNAL_ERROR');
|
|
16
|
+
expect(ErrorCode.AUTH_REQUIRED).toBe('AUTH_REQUIRED');
|
|
17
|
+
expect(ErrorCode.VALIDATION_FAILED).toBe('VALIDATION_FAILED');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should have status code mapping for all error codes', () => {
|
|
21
|
+
const codes = Object.values(ErrorCode);
|
|
22
|
+
for (const code of codes) {
|
|
23
|
+
expect(ERROR_CODE_TO_STATUS[code]).toBeDefined();
|
|
24
|
+
expect(typeof ERROR_CODE_TO_STATUS[code]).toBe('number');
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('should have default messages for all error codes', () => {
|
|
29
|
+
const codes = Object.values(ErrorCode);
|
|
30
|
+
for (const code of codes) {
|
|
31
|
+
expect(ERROR_CODE_MESSAGES[code]).toBeDefined();
|
|
32
|
+
expect(typeof ERROR_CODE_MESSAGES[code]).toBe('string');
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('HttpException with ErrorCode', () => {
|
|
38
|
+
test('should create exception with error code', () => {
|
|
39
|
+
const exception = HttpException.withCode(
|
|
40
|
+
ErrorCode.AUTH_REQUIRED,
|
|
41
|
+
'Custom message',
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(exception.code).toBe(ErrorCode.AUTH_REQUIRED);
|
|
45
|
+
expect(exception.status).toBe(401);
|
|
46
|
+
expect(exception.message).toBe('Custom message');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should use default message when not provided', () => {
|
|
50
|
+
const exception = HttpException.withCode(ErrorCode.AUTH_REQUIRED);
|
|
51
|
+
|
|
52
|
+
expect(exception.code).toBe(ErrorCode.AUTH_REQUIRED);
|
|
53
|
+
expect(exception.status).toBe(401);
|
|
54
|
+
expect(exception.message).toBe(ERROR_CODE_MESSAGES[ErrorCode.AUTH_REQUIRED]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should support details with error code', () => {
|
|
58
|
+
const details = { userId: '123', reason: 'Token expired' };
|
|
59
|
+
const exception = HttpException.withCode(
|
|
60
|
+
ErrorCode.AUTH_TOKEN_EXPIRED,
|
|
61
|
+
undefined,
|
|
62
|
+
details,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(exception.code).toBe(ErrorCode.AUTH_TOKEN_EXPIRED);
|
|
66
|
+
expect(exception.details).toEqual(details);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should allow custom exception classes with error code', () => {
|
|
70
|
+
const exception = new UnauthorizedException(
|
|
71
|
+
'Custom message',
|
|
72
|
+
{ token: 'invalid' },
|
|
73
|
+
ErrorCode.AUTH_INVALID_TOKEN,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(exception.code).toBe(ErrorCode.AUTH_INVALID_TOKEN);
|
|
77
|
+
expect(exception.status).toBe(401);
|
|
78
|
+
expect(exception.message).toBe('Custom message');
|
|
79
|
+
expect(exception.details).toEqual({ token: 'invalid' });
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('ErrorMessageI18n', () => {
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
ErrorMessageI18n.setLanguage('en');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('should get English message by default', () => {
|
|
89
|
+
const message = ErrorMessageI18n.getMessage(ErrorCode.AUTH_REQUIRED);
|
|
90
|
+
expect(message).toBe('Authentication Required');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('should get Chinese message when language is zh-CN', () => {
|
|
94
|
+
ErrorMessageI18n.setLanguage('zh-CN');
|
|
95
|
+
const message = ErrorMessageI18n.getMessage(ErrorCode.AUTH_REQUIRED);
|
|
96
|
+
expect(message).toBe('需要认证');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('should parse language from Accept-Language header', () => {
|
|
100
|
+
expect(ErrorMessageI18n.parseLanguageFromHeader('zh-CN,zh;q=0.9')).toBe('zh-CN');
|
|
101
|
+
expect(ErrorMessageI18n.parseLanguageFromHeader('zh,en;q=0.9')).toBe('zh-CN');
|
|
102
|
+
expect(ErrorMessageI18n.parseLanguageFromHeader('en-US,en;q=0.9')).toBe('en');
|
|
103
|
+
expect(ErrorMessageI18n.parseLanguageFromHeader(null)).toBe('en');
|
|
104
|
+
expect(ErrorMessageI18n.parseLanguageFromHeader(undefined)).toBe('en');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should get message for specific language', () => {
|
|
108
|
+
const enMessage = ErrorMessageI18n.getMessage(ErrorCode.AUTH_REQUIRED, 'en');
|
|
109
|
+
const zhMessage = ErrorMessageI18n.getMessage(ErrorCode.AUTH_REQUIRED, 'zh-CN');
|
|
110
|
+
|
|
111
|
+
expect(enMessage).toBe('Authentication Required');
|
|
112
|
+
expect(zhMessage).toBe('需要认证');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should fallback to English if message not found', () => {
|
|
116
|
+
// 测试未定义的错误码应该回退到英文
|
|
117
|
+
const message = ErrorMessageI18n.getMessage(ErrorCode.INTERNAL_ERROR, 'zh-CN');
|
|
118
|
+
expect(message).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { Context } from '../../src/core/context';
|
|
4
|
+
import { handleError } from '../../src/error/handler';
|
|
5
|
+
import { HttpException, BadRequestException } from '../../src/error/http-exception';
|
|
6
|
+
import { ValidationError } from '../../src/validation';
|
|
7
|
+
import { ExceptionFilterRegistry, type ExceptionFilter } from '../../src/error/filter';
|
|
8
|
+
|
|
9
|
+
function createContext(url: string = 'http://localhost/api/error'): Context {
|
|
10
|
+
return new Context(new Request(url));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('Error Handler', () => {
|
|
14
|
+
test('should handle HttpException', async () => {
|
|
15
|
+
const ctx = createContext();
|
|
16
|
+
const error = new BadRequestException('Invalid payload');
|
|
17
|
+
const response = await handleError(error, ctx);
|
|
18
|
+
expect(response.status).toBe(400);
|
|
19
|
+
const data = await response.json();
|
|
20
|
+
expect(data.error).toBe('Invalid payload');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('should handle ValidationError', async () => {
|
|
24
|
+
const ctx = createContext();
|
|
25
|
+
const validationError = new ValidationError('Validation failed', [
|
|
26
|
+
{ index: 0, rule: 'isString', message: 'Must be string' },
|
|
27
|
+
]);
|
|
28
|
+
const response = await handleError(validationError, ctx);
|
|
29
|
+
expect(response.status).toBe(400);
|
|
30
|
+
const data = await response.json();
|
|
31
|
+
expect(data.issues.length).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('should allow custom exception filter', async () => {
|
|
35
|
+
const registry = ExceptionFilterRegistry.getInstance();
|
|
36
|
+
registry.clear();
|
|
37
|
+
|
|
38
|
+
const filter: ExceptionFilter = {
|
|
39
|
+
catch(error, context) {
|
|
40
|
+
if (error instanceof Error && error.message === 'custom') {
|
|
41
|
+
context.setStatus(418);
|
|
42
|
+
return context.createResponse({ error: 'filtered' });
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
registry.register(filter);
|
|
49
|
+
|
|
50
|
+
const ctx = createContext();
|
|
51
|
+
const response = await handleError(new Error('custom'), ctx);
|
|
52
|
+
expect(response.status).toBe(418);
|
|
53
|
+
const data = await response.json();
|
|
54
|
+
expect(data.error).toBe('filtered');
|
|
55
|
+
|
|
56
|
+
registry.clear();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should fallback to 500 for unknown errors', async () => {
|
|
60
|
+
const ctx = createContext();
|
|
61
|
+
const response = await handleError(new Error('unknown'), ctx);
|
|
62
|
+
expect(response.status).toBe(500);
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
expect(data.error).toBe('Internal Server Error');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
|