@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,212 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
|
|
4
|
+
import { MODULE_METADATA_KEY } from '../../src/di/module';
|
|
5
|
+
import { Container } from '../../src/di/container';
|
|
6
|
+
import { ModuleRegistry } from '../../src/di/module-registry';
|
|
7
|
+
import {
|
|
8
|
+
CacheModule,
|
|
9
|
+
CacheService,
|
|
10
|
+
CACHE_SERVICE_TOKEN,
|
|
11
|
+
MemoryCacheStore,
|
|
12
|
+
type CacheModuleOptions,
|
|
13
|
+
} from '../../src/cache';
|
|
14
|
+
|
|
15
|
+
describe('CacheModule', () => {
|
|
16
|
+
let container: Container;
|
|
17
|
+
let moduleRegistry: ModuleRegistry;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
Reflect.deleteMetadata(MODULE_METADATA_KEY, CacheModule);
|
|
21
|
+
container = new Container();
|
|
22
|
+
moduleRegistry = ModuleRegistry.getInstance();
|
|
23
|
+
moduleRegistry.clear();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should register cache service provider', () => {
|
|
27
|
+
CacheModule.forRoot();
|
|
28
|
+
|
|
29
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, CacheModule);
|
|
30
|
+
expect(metadata).toBeDefined();
|
|
31
|
+
expect(metadata.providers).toBeDefined();
|
|
32
|
+
|
|
33
|
+
const cacheProvider = metadata.providers.find(
|
|
34
|
+
(provider: any) => provider.provide === CACHE_SERVICE_TOKEN,
|
|
35
|
+
);
|
|
36
|
+
expect(cacheProvider).toBeDefined();
|
|
37
|
+
expect(cacheProvider.useValue).toBeInstanceOf(CacheService);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should use custom store when provided', () => {
|
|
41
|
+
const customStore = new MemoryCacheStore();
|
|
42
|
+
CacheModule.forRoot({
|
|
43
|
+
store: customStore,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, CacheModule);
|
|
47
|
+
const cacheProvider = metadata.providers.find(
|
|
48
|
+
(provider: any) => provider.provide === CACHE_SERVICE_TOKEN,
|
|
49
|
+
);
|
|
50
|
+
const service = cacheProvider.useValue as CacheService;
|
|
51
|
+
expect(service).toBeInstanceOf(CacheService);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should use default TTL when provided', () => {
|
|
55
|
+
CacheModule.forRoot({
|
|
56
|
+
defaultTtl: 60000,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, CacheModule);
|
|
60
|
+
const cacheProvider = metadata.providers.find(
|
|
61
|
+
(provider: any) => provider.provide === CACHE_SERVICE_TOKEN,
|
|
62
|
+
);
|
|
63
|
+
expect(cacheProvider).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('should use key prefix when provided', () => {
|
|
67
|
+
CacheModule.forRoot({
|
|
68
|
+
keyPrefix: 'app:',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, CacheModule);
|
|
72
|
+
const cacheProvider = metadata.providers.find(
|
|
73
|
+
(provider: any) => provider.provide === CACHE_SERVICE_TOKEN,
|
|
74
|
+
);
|
|
75
|
+
expect(cacheProvider).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('CacheService', () => {
|
|
80
|
+
let service: CacheService;
|
|
81
|
+
let store: MemoryCacheStore;
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
store = new MemoryCacheStore();
|
|
85
|
+
service = new CacheService({
|
|
86
|
+
store,
|
|
87
|
+
defaultTtl: 3600000,
|
|
88
|
+
keyPrefix: '',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('should get and set cache values', async () => {
|
|
93
|
+
await service.set('key1', 'value1');
|
|
94
|
+
const value = await service.get<string>('key1');
|
|
95
|
+
expect(value).toBe('value1');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('should return undefined for non-existent key', async () => {
|
|
99
|
+
const value = await service.get('non-existent');
|
|
100
|
+
expect(value).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('should delete cache value', async () => {
|
|
104
|
+
await service.set('key1', 'value1');
|
|
105
|
+
await service.delete('key1');
|
|
106
|
+
const value = await service.get('key1');
|
|
107
|
+
expect(value).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('should check if key exists', async () => {
|
|
111
|
+
await service.set('key1', 'value1');
|
|
112
|
+
expect(await service.has('key1')).toBe(true);
|
|
113
|
+
expect(await service.has('non-existent')).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('should clear all cache', async () => {
|
|
117
|
+
await service.set('key1', 'value1');
|
|
118
|
+
await service.set('key2', 'value2');
|
|
119
|
+
await service.clear();
|
|
120
|
+
expect(await service.has('key1')).toBe(false);
|
|
121
|
+
expect(await service.has('key2')).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('should get or set cache value', async () => {
|
|
125
|
+
const value1 = await service.getOrSet('key1', () => 'computed');
|
|
126
|
+
expect(value1).toBe('computed');
|
|
127
|
+
|
|
128
|
+
const value2 = await service.getOrSet('key1', () => 'should-not-run');
|
|
129
|
+
expect(value2).toBe('computed');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('should get many cache values', async () => {
|
|
133
|
+
await service.set('key1', 'value1');
|
|
134
|
+
await service.set('key2', 'value2');
|
|
135
|
+
const values = await service.getMany<string>(['key1', 'key2', 'key3']);
|
|
136
|
+
expect(values.get('key1')).toBe('value1');
|
|
137
|
+
expect(values.get('key2')).toBe('value2');
|
|
138
|
+
expect(values.get('key3')).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('should set many cache values', async () => {
|
|
142
|
+
await service.setMany([
|
|
143
|
+
{ key: 'key1', value: 'value1' },
|
|
144
|
+
{ key: 'key2', value: 'value2' },
|
|
145
|
+
]);
|
|
146
|
+
expect(await service.get('key1')).toBe('value1');
|
|
147
|
+
expect(await service.get('key2')).toBe('value2');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('should delete many cache values', async () => {
|
|
151
|
+
await service.set('key1', 'value1');
|
|
152
|
+
await service.set('key2', 'value2');
|
|
153
|
+
await service.set('key3', 'value3');
|
|
154
|
+
const deleted = await service.deleteMany(['key1', 'key2']);
|
|
155
|
+
expect(deleted.length).toBe(2);
|
|
156
|
+
expect(await service.has('key1')).toBe(false);
|
|
157
|
+
expect(await service.has('key2')).toBe(false);
|
|
158
|
+
expect(await service.has('key3')).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('should respect TTL', async () => {
|
|
162
|
+
await service.set('key1', 'value1', 100);
|
|
163
|
+
expect(await service.get('key1')).toBe('value1');
|
|
164
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
165
|
+
const value = await service.get('key1');
|
|
166
|
+
expect(value).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('should use key prefix', async () => {
|
|
170
|
+
const prefixedService = new CacheService({
|
|
171
|
+
store,
|
|
172
|
+
defaultTtl: 3600000,
|
|
173
|
+
keyPrefix: 'app:',
|
|
174
|
+
});
|
|
175
|
+
await prefixedService.set('key1', 'value1');
|
|
176
|
+
const value = await prefixedService.get('key1');
|
|
177
|
+
expect(value).toBe('value1');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('MemoryCacheStore', () => {
|
|
182
|
+
let store: MemoryCacheStore;
|
|
183
|
+
|
|
184
|
+
beforeEach(() => {
|
|
185
|
+
store = new MemoryCacheStore();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('should store and retrieve values', async () => {
|
|
189
|
+
await store.set('key1', 'value1');
|
|
190
|
+
const value = await store.get('key1');
|
|
191
|
+
expect(value).toBe('value1');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('should expire values after TTL', async () => {
|
|
195
|
+
await store.set('key1', 'value1', 100);
|
|
196
|
+
expect(await store.get('key1')).toBe('value1');
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
198
|
+
const value = await store.get('key1');
|
|
199
|
+
expect(value).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('should handle getMany and setMany', async () => {
|
|
203
|
+
await store.setMany([
|
|
204
|
+
{ key: 'key1', value: 'value1' },
|
|
205
|
+
{ key: 'key2', value: 'value2' },
|
|
206
|
+
]);
|
|
207
|
+
const values = await store.getMany(['key1', 'key2', 'key3']);
|
|
208
|
+
expect(values.get('key1')).toBe('value1');
|
|
209
|
+
expect(values.get('key2')).toBe('value2');
|
|
210
|
+
expect(values.get('key3')).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
|
|
4
|
+
import { MODULE_METADATA_KEY } from '../../src/di/module';
|
|
5
|
+
import { Container } from '../../src/di/container';
|
|
6
|
+
import {
|
|
7
|
+
ConfigModule,
|
|
8
|
+
ConfigService,
|
|
9
|
+
CONFIG_SERVICE_TOKEN,
|
|
10
|
+
type ConfigModuleOptions,
|
|
11
|
+
} from '../../src/config';
|
|
12
|
+
|
|
13
|
+
describe('ConfigService', () => {
|
|
14
|
+
test('should get and set values via path', () => {
|
|
15
|
+
const service = new ConfigService({
|
|
16
|
+
db: {
|
|
17
|
+
host: 'localhost',
|
|
18
|
+
port: 5432,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(service.get<string>('db.host')).toBe('localhost');
|
|
23
|
+
expect(service.get<number>('db.port')).toBe(5432);
|
|
24
|
+
expect(service.get<string>('db.user', 'root')).toBe('root');
|
|
25
|
+
expect(service.get<string>('db.password')).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('should throw when required value is missing', () => {
|
|
29
|
+
const service = new ConfigService({});
|
|
30
|
+
|
|
31
|
+
expect(() => service.getRequired('missing')).toThrow(
|
|
32
|
+
'Config value required for key: missing',
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should support namespace', () => {
|
|
37
|
+
const service = new ConfigService({
|
|
38
|
+
logger: {
|
|
39
|
+
level: 'debug',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const loggerConfig = service.withNamespace('logger');
|
|
44
|
+
expect(loggerConfig.get<string>('level')).toBe('debug');
|
|
45
|
+
expect(loggerConfig.get<string>('logger.level')).toBe('debug');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('ConfigModule', () => {
|
|
50
|
+
const ORIGINAL_ENV = process.env;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
process.env = { ...ORIGINAL_ENV };
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should register config service provider', () => {
|
|
57
|
+
const options: ConfigModuleOptions = {
|
|
58
|
+
defaultConfig: {
|
|
59
|
+
appName: 'test-app',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
ConfigModule.forRoot(options);
|
|
64
|
+
|
|
65
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, ConfigModule);
|
|
66
|
+
expect(metadata).toBeDefined();
|
|
67
|
+
expect(metadata.providers).toBeDefined();
|
|
68
|
+
|
|
69
|
+
const configProvider = metadata.providers.find(
|
|
70
|
+
(provider: any) => provider.provide === CONFIG_SERVICE_TOKEN,
|
|
71
|
+
);
|
|
72
|
+
expect(configProvider).toBeDefined();
|
|
73
|
+
expect(configProvider.useValue).toBeInstanceOf(ConfigService);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should merge defaultConfig and env-loaded config', () => {
|
|
77
|
+
const options: ConfigModuleOptions = {
|
|
78
|
+
defaultConfig: {
|
|
79
|
+
appName: 'default-app',
|
|
80
|
+
port: 3000,
|
|
81
|
+
},
|
|
82
|
+
load() {
|
|
83
|
+
return {
|
|
84
|
+
appName: 'env-app',
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
ConfigModule.forRoot(options);
|
|
90
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, ConfigModule);
|
|
91
|
+
const providers = metadata.providers.filter(
|
|
92
|
+
(p: any) => p.provide === CONFIG_SERVICE_TOKEN,
|
|
93
|
+
);
|
|
94
|
+
const service = providers[providers.length - 1].useValue as ConfigService;
|
|
95
|
+
|
|
96
|
+
expect(service.get<string>('appName')).toBe('env-app');
|
|
97
|
+
expect(service.get<number>('port')).toBe(3000);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('should validate config', () => {
|
|
101
|
+
const options: ConfigModuleOptions = {
|
|
102
|
+
defaultConfig: {
|
|
103
|
+
appName: 'test-app',
|
|
104
|
+
},
|
|
105
|
+
validate(config) {
|
|
106
|
+
if (!config.appName) {
|
|
107
|
+
throw new Error('appName is required');
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
expect(() => ConfigModule.forRoot(options)).not.toThrow();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should throw when validation fails', () => {
|
|
116
|
+
const options: ConfigModuleOptions = {
|
|
117
|
+
defaultConfig: {},
|
|
118
|
+
validate(config) {
|
|
119
|
+
if (!config.appName) {
|
|
120
|
+
throw new Error('appName is required');
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
expect(() => ConfigModule.forRoot(options)).toThrow(
|
|
126
|
+
'appName is required',
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('should export config service to parent container', () => {
|
|
131
|
+
const options: ConfigModuleOptions = {
|
|
132
|
+
defaultConfig: {
|
|
133
|
+
appName: 'test-app',
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
ConfigModule.forRoot(options);
|
|
138
|
+
|
|
139
|
+
const rootContainer = new Container();
|
|
140
|
+
const { ModuleRegistry } = require('../../src/di/module-registry') as typeof import('../../src/di/module-registry');
|
|
141
|
+
const registry = ModuleRegistry.getInstance();
|
|
142
|
+
registry.clear();
|
|
143
|
+
registry.register(ConfigModule, rootContainer);
|
|
144
|
+
|
|
145
|
+
const configService = rootContainer.resolve<ConfigService>(CONFIG_SERVICE_TOKEN);
|
|
146
|
+
expect(configService).toBeInstanceOf(ConfigService);
|
|
147
|
+
expect(configService.get<string>('appName')).toBe('test-app');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
3
|
+
import { Application } from '../../src/core/application';
|
|
4
|
+
import { Controller, ControllerRegistry } from '../../src/controller/controller';
|
|
5
|
+
import { Injectable, Inject } from '../../src/di/decorators';
|
|
6
|
+
import { GET, POST } from '../../src/router/decorators';
|
|
7
|
+
import { Body, Param, Query } from '../../src/controller/decorators';
|
|
8
|
+
import { RouteRegistry } from '../../src/router/registry';
|
|
9
|
+
import { getTestPort } from '../utils/test-port';
|
|
10
|
+
|
|
11
|
+
describe('Controller Integration', () => {
|
|
12
|
+
let app: Application;
|
|
13
|
+
let port: number;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
port = getTestPort();
|
|
17
|
+
app = new Application({ port });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
if (app) {
|
|
22
|
+
await app.stop();
|
|
23
|
+
}
|
|
24
|
+
RouteRegistry.getInstance().clear();
|
|
25
|
+
ControllerRegistry.getInstance().clear();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('should register and handle controller with GET route', async () => {
|
|
29
|
+
@Controller('/api/users')
|
|
30
|
+
class UserController {
|
|
31
|
+
@GET('/')
|
|
32
|
+
public getUsers() {
|
|
33
|
+
return { users: ['user1', 'user2'] };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
app.registerController(UserController);
|
|
38
|
+
await app.listen();
|
|
39
|
+
|
|
40
|
+
const response = await fetch(`http://localhost:${port}/api/users`);
|
|
41
|
+
expect(response.status).toBe(200);
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
expect(data.users).toEqual(['user1', 'user2']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should handle controller with path parameter', async () => {
|
|
47
|
+
@Controller('/api/users')
|
|
48
|
+
class UserController {
|
|
49
|
+
@GET('/:id')
|
|
50
|
+
public getUser(@Param('id') id: string) {
|
|
51
|
+
return { id, name: `User ${id}` };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
app.registerController(UserController);
|
|
56
|
+
await app.listen();
|
|
57
|
+
|
|
58
|
+
const response = await fetch(`http://localhost:${port}/api/users/123`);
|
|
59
|
+
expect(response.status).toBe(200);
|
|
60
|
+
const data = await response.json();
|
|
61
|
+
expect(data.id).toBe('123');
|
|
62
|
+
expect(data.name).toBe('User 123');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('should handle controller with query parameter', async () => {
|
|
66
|
+
@Controller('/api/users')
|
|
67
|
+
class UserController {
|
|
68
|
+
@GET('/search')
|
|
69
|
+
public searchUsers(@Query('q') query: string | null) {
|
|
70
|
+
return { query, results: [`result for ${query}`] };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
app.registerController(UserController);
|
|
75
|
+
await app.listen();
|
|
76
|
+
|
|
77
|
+
const response = await fetch(`http://localhost:${port}/api/users/search?q=test`);
|
|
78
|
+
expect(response.status).toBe(200);
|
|
79
|
+
const data = await response.json();
|
|
80
|
+
expect(data.query).toBe('test');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should handle controller with POST and body', async () => {
|
|
84
|
+
@Controller('/api/users')
|
|
85
|
+
class UserController {
|
|
86
|
+
@POST('/')
|
|
87
|
+
public createUser(@Body() user: { name: string; email: string }) {
|
|
88
|
+
return { id: '1', ...user };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
app.registerController(UserController);
|
|
93
|
+
await app.listen();
|
|
94
|
+
|
|
95
|
+
const response = await fetch(`http://localhost:${port}/api/users`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/json' },
|
|
98
|
+
body: JSON.stringify({ name: 'John', email: 'john@example.com' }),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(response.status).toBe(200);
|
|
102
|
+
const data = await response.json();
|
|
103
|
+
expect(data.id).toBe('1');
|
|
104
|
+
expect(data.name).toBe('John');
|
|
105
|
+
expect(data.email).toBe('john@example.com');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('should handle controller with dependency injection', async () => {
|
|
109
|
+
@Injectable()
|
|
110
|
+
class UserService {
|
|
111
|
+
public findUser(id: string) {
|
|
112
|
+
return { id, name: 'John Doe' };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 注册服务到容器(在控制器定义之前)
|
|
117
|
+
const container = app.getContainer();
|
|
118
|
+
container.register(UserService);
|
|
119
|
+
|
|
120
|
+
@Controller('/api/users-di')
|
|
121
|
+
class UserController {
|
|
122
|
+
public constructor(@Inject(UserService) private userService: UserService) {}
|
|
123
|
+
|
|
124
|
+
@GET('/:id')
|
|
125
|
+
public getUser(@Param('id') id: string) {
|
|
126
|
+
return this.userService.findUser(id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
app.registerController(UserController);
|
|
131
|
+
await app.listen();
|
|
132
|
+
|
|
133
|
+
const response = await fetch(`http://localhost:${port}/api/users-di/123`);
|
|
134
|
+
if (response.status !== 200) {
|
|
135
|
+
const errorText = await response.text();
|
|
136
|
+
console.error('Error response:', errorText);
|
|
137
|
+
}
|
|
138
|
+
expect(response.status).toBe(200);
|
|
139
|
+
const data = await response.json();
|
|
140
|
+
expect(data.id).toBe('123');
|
|
141
|
+
expect(data.name).toBe('John Doe');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('should handle multiple routes in same controller', async () => {
|
|
145
|
+
@Controller('/api/users')
|
|
146
|
+
class UserController {
|
|
147
|
+
@GET('/')
|
|
148
|
+
public getUsers() {
|
|
149
|
+
return { users: ['user1', 'user2'] };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@GET('/:id')
|
|
153
|
+
public getUser(@Param('id') id: string) {
|
|
154
|
+
return { id, name: `User ${id}` };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@POST('/')
|
|
158
|
+
public createUser(@Body() user: { name: string }) {
|
|
159
|
+
return { id: '1', ...user };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
app.registerController(UserController);
|
|
164
|
+
await app.listen();
|
|
165
|
+
|
|
166
|
+
// 测试 GET /
|
|
167
|
+
const listResponse = await fetch(`http://localhost:${port}/api/users`);
|
|
168
|
+
expect(listResponse.status).toBe(200);
|
|
169
|
+
const listData = await listResponse.json();
|
|
170
|
+
expect(listData.users).toEqual(['user1', 'user2']);
|
|
171
|
+
|
|
172
|
+
// 测试 GET /:id
|
|
173
|
+
const getResponse = await fetch(`http://localhost:${port}/api/users/123`);
|
|
174
|
+
expect(getResponse.status).toBe(200);
|
|
175
|
+
const getData = await getResponse.json();
|
|
176
|
+
expect(getData.id).toBe('123');
|
|
177
|
+
|
|
178
|
+
// 测试 POST /
|
|
179
|
+
const postResponse = await fetch(`http://localhost:${port}/api/users`, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify({ name: 'New User' }),
|
|
183
|
+
});
|
|
184
|
+
expect(postResponse.status).toBe(200);
|
|
185
|
+
const postData = await postResponse.json();
|
|
186
|
+
expect(postData.name).toBe('New User');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, test, afterEach } from 'bun:test';
|
|
2
|
+
import { Application } from '../../src/core/application';
|
|
3
|
+
import { getTestPort } from '../utils/test-port';
|
|
4
|
+
|
|
5
|
+
describe('Application', () => {
|
|
6
|
+
let app: Application;
|
|
7
|
+
|
|
8
|
+
afterEach(async () => {
|
|
9
|
+
if (app) {
|
|
10
|
+
await app.stop();
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('should create application instance', () => {
|
|
15
|
+
app = new Application();
|
|
16
|
+
expect(app).toBeInstanceOf(Application);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should create application with options', () => {
|
|
20
|
+
const port = getTestPort();
|
|
21
|
+
app = new Application({ port });
|
|
22
|
+
expect(app).toBeInstanceOf(Application);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should start server', async () => {
|
|
26
|
+
const port = getTestPort();
|
|
27
|
+
app = new Application({ port });
|
|
28
|
+
await app.listen();
|
|
29
|
+
|
|
30
|
+
const server = app.getServer();
|
|
31
|
+
expect(server).toBeDefined();
|
|
32
|
+
expect(server?.isRunning()).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should stop server', async () => {
|
|
36
|
+
const port = getTestPort();
|
|
37
|
+
app = new Application({ port });
|
|
38
|
+
await app.listen();
|
|
39
|
+
await app.stop();
|
|
40
|
+
|
|
41
|
+
const server = app.getServer();
|
|
42
|
+
expect(server?.isRunning()).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should handle request with 404 by default', async () => {
|
|
46
|
+
const port = getTestPort();
|
|
47
|
+
app = new Application({ port });
|
|
48
|
+
await app.listen();
|
|
49
|
+
|
|
50
|
+
const response = await fetch(`http://localhost:${port}/api/users`);
|
|
51
|
+
expect(response.status).toBe(404);
|
|
52
|
+
|
|
53
|
+
const data = await response.json();
|
|
54
|
+
expect(data.error).toBe('Not Found');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { Context } from '../../src/core/context';
|
|
3
|
+
|
|
4
|
+
describe('Context Body Parsing', () => {
|
|
5
|
+
test('should parse JSON body', async () => {
|
|
6
|
+
const request = new Request('http://localhost:3000/api/users', {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
body: JSON.stringify({ name: 'John', age: 30 }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const context = new Context(request);
|
|
13
|
+
const body = await context.getBody();
|
|
14
|
+
|
|
15
|
+
expect(body).toEqual({ name: 'John', age: 30 });
|
|
16
|
+
expect(context.body).toEqual({ name: 'John', age: 30 });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should parse body only once', async () => {
|
|
20
|
+
const request = new Request('http://localhost:3000/api/users', {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({ name: 'John' }),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const context = new Context(request);
|
|
27
|
+
const body1 = await context.getBody();
|
|
28
|
+
const body2 = await context.getBody();
|
|
29
|
+
|
|
30
|
+
expect(body1).toEqual(body2);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should return undefined for GET request', async () => {
|
|
34
|
+
const request = new Request('http://localhost:3000/api/users', {
|
|
35
|
+
method: 'GET',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const context = new Context(request);
|
|
39
|
+
const body = await context.getBody();
|
|
40
|
+
|
|
41
|
+
expect(body).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|