@dangao/bun-server 1.0.1 → 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,307 @@
|
|
|
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
|
+
SessionModule,
|
|
9
|
+
SessionService,
|
|
10
|
+
SESSION_SERVICE_TOKEN,
|
|
11
|
+
MemorySessionStore,
|
|
12
|
+
type SessionModuleOptions,
|
|
13
|
+
} from '../../src/session';
|
|
14
|
+
|
|
15
|
+
describe('SessionModule', () => {
|
|
16
|
+
let container: Container;
|
|
17
|
+
let moduleRegistry: ModuleRegistry;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
Reflect.deleteMetadata(MODULE_METADATA_KEY, SessionModule);
|
|
21
|
+
container = new Container();
|
|
22
|
+
moduleRegistry = ModuleRegistry.getInstance();
|
|
23
|
+
moduleRegistry.clear();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should register session service provider', () => {
|
|
27
|
+
SessionModule.forRoot();
|
|
28
|
+
|
|
29
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, SessionModule);
|
|
30
|
+
expect(metadata).toBeDefined();
|
|
31
|
+
expect(metadata.providers).toBeDefined();
|
|
32
|
+
|
|
33
|
+
const sessionProvider = metadata.providers.find(
|
|
34
|
+
(provider: any) => provider.provide === SESSION_SERVICE_TOKEN,
|
|
35
|
+
);
|
|
36
|
+
expect(sessionProvider).toBeDefined();
|
|
37
|
+
expect(sessionProvider.useValue).toBeInstanceOf(SessionService);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should use custom store when provided', () => {
|
|
41
|
+
const customStore = new MemorySessionStore();
|
|
42
|
+
SessionModule.forRoot({
|
|
43
|
+
store: customStore,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, SessionModule);
|
|
47
|
+
const sessionProvider = metadata.providers.find(
|
|
48
|
+
(provider: any) => provider.provide === SESSION_SERVICE_TOKEN,
|
|
49
|
+
);
|
|
50
|
+
expect(sessionProvider).toBeDefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should configure session options', () => {
|
|
54
|
+
SessionModule.forRoot({
|
|
55
|
+
name: 'custom-session',
|
|
56
|
+
maxAge: 7200000,
|
|
57
|
+
rolling: false,
|
|
58
|
+
cookie: {
|
|
59
|
+
secure: true,
|
|
60
|
+
httpOnly: true,
|
|
61
|
+
path: '/api',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, SessionModule);
|
|
66
|
+
expect(metadata).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('SessionService', () => {
|
|
71
|
+
let service: SessionService;
|
|
72
|
+
let store: MemorySessionStore;
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
store = new MemorySessionStore();
|
|
76
|
+
service = new SessionService({
|
|
77
|
+
store,
|
|
78
|
+
name: 'sessionId',
|
|
79
|
+
maxAge: 86400000,
|
|
80
|
+
rolling: true,
|
|
81
|
+
cookie: {
|
|
82
|
+
secure: false,
|
|
83
|
+
httpOnly: true,
|
|
84
|
+
path: '/',
|
|
85
|
+
sameSite: 'lax',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('should create new session', async () => {
|
|
91
|
+
const session = await service.create({ userId: '123' });
|
|
92
|
+
expect(session).toBeDefined();
|
|
93
|
+
expect(session.id).toBeDefined();
|
|
94
|
+
expect(session.data.userId).toBe('123');
|
|
95
|
+
expect(session.createdAt).toBeGreaterThan(0);
|
|
96
|
+
expect(session.lastAccessedAt).toBeGreaterThan(0);
|
|
97
|
+
expect(session.expiresAt).toBeGreaterThan(Date.now());
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('should get session by id', async () => {
|
|
101
|
+
const session = await service.create({ userId: '123' });
|
|
102
|
+
const retrieved = await service.get(session.id);
|
|
103
|
+
expect(retrieved).toBeDefined();
|
|
104
|
+
expect(retrieved?.id).toBe(session.id);
|
|
105
|
+
expect(retrieved?.data.userId).toBe('123');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('should return undefined for non-existent session', async () => {
|
|
109
|
+
const session = await service.get('non-existent-id');
|
|
110
|
+
expect(session).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('should update session data', async () => {
|
|
114
|
+
const session = await service.create({ userId: '123' });
|
|
115
|
+
const updated = await service.set(session.id, { userId: '456', role: 'admin' });
|
|
116
|
+
expect(updated).toBe(true);
|
|
117
|
+
|
|
118
|
+
const retrieved = await service.get(session.id);
|
|
119
|
+
expect(retrieved?.data.userId).toBe('456');
|
|
120
|
+
expect(retrieved?.data.role).toBe('admin');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('should get session value', async () => {
|
|
124
|
+
const session = await service.create({ userId: '123', role: 'admin' });
|
|
125
|
+
const userId = await service.getValue<string>(session.id, 'userId');
|
|
126
|
+
expect(userId).toBe('123');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('should set session value', async () => {
|
|
130
|
+
const session = await service.create({ userId: '123' });
|
|
131
|
+
const set = await service.setValue(session.id, 'role', 'admin');
|
|
132
|
+
expect(set).toBe(true);
|
|
133
|
+
|
|
134
|
+
const role = await service.getValue<string>(session.id, 'role');
|
|
135
|
+
expect(role).toBe('admin');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should delete session value', async () => {
|
|
139
|
+
const session = await service.create({ userId: '123', role: 'admin' });
|
|
140
|
+
const deleted = await service.deleteValue(session.id, 'role');
|
|
141
|
+
expect(deleted).toBe(true);
|
|
142
|
+
|
|
143
|
+
const role = await service.getValue(session.id, 'role');
|
|
144
|
+
expect(role).toBeUndefined();
|
|
145
|
+
const userId = await service.getValue(session.id, 'userId');
|
|
146
|
+
expect(userId).toBe('123');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('should delete session', async () => {
|
|
150
|
+
const session = await service.create({ userId: '123' });
|
|
151
|
+
const deleted = await service.delete(session.id);
|
|
152
|
+
expect(deleted).toBe(true);
|
|
153
|
+
|
|
154
|
+
const retrieved = await service.get(session.id);
|
|
155
|
+
expect(retrieved).toBeUndefined();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('should touch session to update lastAccessedAt', async () => {
|
|
159
|
+
const session = await service.create({ userId: '123' });
|
|
160
|
+
const originalTime = session.lastAccessedAt;
|
|
161
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
162
|
+
const touched = await service.touch(session.id);
|
|
163
|
+
expect(touched).toBe(true);
|
|
164
|
+
|
|
165
|
+
const retrieved = await service.get(session.id);
|
|
166
|
+
expect(retrieved?.lastAccessedAt).toBeGreaterThan(originalTime);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('should get cookie name and options', () => {
|
|
170
|
+
const cookieName = service.getCookieName();
|
|
171
|
+
expect(cookieName).toBe('sessionId');
|
|
172
|
+
|
|
173
|
+
const cookieOptions = service.getCookieOptions();
|
|
174
|
+
expect(cookieOptions).toBeDefined();
|
|
175
|
+
expect(cookieOptions.httpOnly).toBe(true);
|
|
176
|
+
expect(cookieOptions.path).toBe('/');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('should respect maxAge', async () => {
|
|
180
|
+
const shortLivedService = new SessionService({
|
|
181
|
+
store,
|
|
182
|
+
name: 'sessionId',
|
|
183
|
+
maxAge: 100,
|
|
184
|
+
rolling: false,
|
|
185
|
+
cookie: {},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const session = await shortLivedService.create({ userId: '123' });
|
|
189
|
+
expect(session.expiresAt).toBeLessThanOrEqual(Date.now() + 100);
|
|
190
|
+
|
|
191
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
192
|
+
const retrieved = await shortLivedService.get(session.id);
|
|
193
|
+
expect(retrieved).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('MemorySessionStore', () => {
|
|
198
|
+
let store: MemorySessionStore;
|
|
199
|
+
|
|
200
|
+
beforeEach(() => {
|
|
201
|
+
store = new MemorySessionStore();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('should store and retrieve session', async () => {
|
|
205
|
+
const session = {
|
|
206
|
+
id: 'session-1',
|
|
207
|
+
data: { userId: '123' },
|
|
208
|
+
createdAt: Date.now(),
|
|
209
|
+
lastAccessedAt: Date.now(),
|
|
210
|
+
expiresAt: Date.now() + 86400000,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
await store.set(session, 86400000);
|
|
214
|
+
const retrieved = await store.get('session-1');
|
|
215
|
+
expect(retrieved).toBeDefined();
|
|
216
|
+
expect(retrieved?.id).toBe('session-1');
|
|
217
|
+
expect(retrieved?.data.userId).toBe('123');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('should expire sessions after maxAge', async () => {
|
|
221
|
+
const session = {
|
|
222
|
+
id: 'session-1',
|
|
223
|
+
data: { userId: '123' },
|
|
224
|
+
createdAt: Date.now(),
|
|
225
|
+
lastAccessedAt: Date.now(),
|
|
226
|
+
expiresAt: Date.now() + 100,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
await store.set(session, 100);
|
|
230
|
+
expect(await store.get('session-1')).toBeDefined();
|
|
231
|
+
|
|
232
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
233
|
+
const retrieved = await store.get('session-1');
|
|
234
|
+
expect(retrieved).toBeUndefined();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('should check if session exists', async () => {
|
|
238
|
+
const session = {
|
|
239
|
+
id: 'session-1',
|
|
240
|
+
data: { userId: '123' },
|
|
241
|
+
createdAt: Date.now(),
|
|
242
|
+
lastAccessedAt: Date.now(),
|
|
243
|
+
expiresAt: Date.now() + 86400000,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
await store.set(session, 86400000);
|
|
247
|
+
expect(await store.has('session-1')).toBe(true);
|
|
248
|
+
expect(await store.has('non-existent')).toBe(false);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('should touch session', async () => {
|
|
252
|
+
const session = {
|
|
253
|
+
id: 'session-1',
|
|
254
|
+
data: { userId: '123' },
|
|
255
|
+
createdAt: Date.now(),
|
|
256
|
+
lastAccessedAt: Date.now(),
|
|
257
|
+
expiresAt: Date.now() + 86400000,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
await store.set(session, 86400000);
|
|
261
|
+
const originalTime = session.lastAccessedAt;
|
|
262
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
263
|
+
const touched = await store.touch('session-1');
|
|
264
|
+
expect(touched).toBe(true);
|
|
265
|
+
|
|
266
|
+
const retrieved = await store.get('session-1');
|
|
267
|
+
expect(retrieved?.lastAccessedAt).toBeGreaterThan(originalTime);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('should delete session', async () => {
|
|
271
|
+
const session = {
|
|
272
|
+
id: 'session-1',
|
|
273
|
+
data: { userId: '123' },
|
|
274
|
+
createdAt: Date.now(),
|
|
275
|
+
lastAccessedAt: Date.now(),
|
|
276
|
+
expiresAt: Date.now() + 86400000,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
await store.set(session, 86400000);
|
|
280
|
+
const deleted = await store.delete('session-1');
|
|
281
|
+
expect(deleted).toBe(true);
|
|
282
|
+
expect(await store.has('session-1')).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('should clear all sessions', async () => {
|
|
286
|
+
const session1 = {
|
|
287
|
+
id: 'session-1',
|
|
288
|
+
data: { userId: '123' },
|
|
289
|
+
createdAt: Date.now(),
|
|
290
|
+
lastAccessedAt: Date.now(),
|
|
291
|
+
expiresAt: Date.now() + 86400000,
|
|
292
|
+
};
|
|
293
|
+
const session2 = {
|
|
294
|
+
id: 'session-2',
|
|
295
|
+
data: { userId: '456' },
|
|
296
|
+
createdAt: Date.now(),
|
|
297
|
+
lastAccessedAt: Date.now(),
|
|
298
|
+
expiresAt: Date.now() + 86400000,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
await store.set(session1, 86400000);
|
|
302
|
+
await store.set(session2, 86400000);
|
|
303
|
+
await store.clear();
|
|
304
|
+
expect(await store.has('session-1')).toBe(false);
|
|
305
|
+
expect(await store.has('session-2')).toBe(false);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { StressTester } from '../../src/testing/harness';
|
|
4
|
+
import { Container } from '../../src/di/container';
|
|
5
|
+
import { Injectable } from '../../src/di/decorators';
|
|
6
|
+
|
|
7
|
+
describe('DI Stress', () => {
|
|
8
|
+
test('should resolve services under concurrent stress', async () => {
|
|
9
|
+
const container = new Container();
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
class HeavyService {
|
|
13
|
+
public readonly createdAt = Date.now();
|
|
14
|
+
public getValue() {
|
|
15
|
+
return 'heavy';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
container.register(HeavyService);
|
|
20
|
+
|
|
21
|
+
const result = await StressTester.run('di:resolve', 60, 6, async () => {
|
|
22
|
+
const service = container.resolve(HeavyService);
|
|
23
|
+
expect(service.getValue()).toBe('heavy');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(result.errors).toBe(0);
|
|
27
|
+
expect(result.iterations).toBe(60);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
import {
|
|
4
|
+
ApiTags,
|
|
5
|
+
ApiOperation,
|
|
6
|
+
ApiParam,
|
|
7
|
+
ApiBody,
|
|
8
|
+
ApiResponse,
|
|
9
|
+
getApiTags,
|
|
10
|
+
getApiOperation,
|
|
11
|
+
getApiParams,
|
|
12
|
+
getApiBody,
|
|
13
|
+
getApiResponses,
|
|
14
|
+
} from '../../src/swagger/decorators';
|
|
15
|
+
|
|
16
|
+
describe('Swagger Decorators', () => {
|
|
17
|
+
describe('ApiTags', () => {
|
|
18
|
+
test('should set tags on class', () => {
|
|
19
|
+
@ApiTags('Users', 'Admin')
|
|
20
|
+
class UserController {}
|
|
21
|
+
|
|
22
|
+
const tags = getApiTags(UserController);
|
|
23
|
+
expect(tags).toEqual(['Users', 'Admin']);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should set tags on method', () => {
|
|
27
|
+
class UserController {
|
|
28
|
+
@ApiTags('Public')
|
|
29
|
+
public getUsers() {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const tags = getApiTags(UserController.prototype, 'getUsers');
|
|
33
|
+
expect(tags).toEqual(['Public']);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should merge tags on method', () => {
|
|
37
|
+
class UserController {
|
|
38
|
+
@ApiTags('Users')
|
|
39
|
+
@ApiTags('Admin')
|
|
40
|
+
public getUsers() {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const tags = getApiTags(UserController.prototype, 'getUsers');
|
|
44
|
+
// 顺序可能不同,但应该包含两个标签
|
|
45
|
+
expect(tags).toContain('Users');
|
|
46
|
+
expect(tags).toContain('Admin');
|
|
47
|
+
expect(tags.length).toBe(2);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('ApiOperation', () => {
|
|
52
|
+
test('should set operation metadata', () => {
|
|
53
|
+
class UserController {
|
|
54
|
+
@ApiOperation({ summary: 'Get users', description: 'Get all users' })
|
|
55
|
+
public getUsers() {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const operation = getApiOperation(UserController.prototype, 'getUsers');
|
|
59
|
+
expect(operation).toBeDefined();
|
|
60
|
+
expect(operation?.summary).toBe('Get users');
|
|
61
|
+
expect(operation?.description).toBe('Get all users');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('ApiParam', () => {
|
|
66
|
+
test('should set param metadata', () => {
|
|
67
|
+
class UserController {
|
|
68
|
+
public getUser(
|
|
69
|
+
@ApiParam({ name: 'id', description: 'User ID', required: true })
|
|
70
|
+
id: string,
|
|
71
|
+
) {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const params = getApiParams(UserController.prototype, 'getUser');
|
|
75
|
+
expect(params).toBeDefined();
|
|
76
|
+
expect(params.length).toBe(1);
|
|
77
|
+
expect(params[0].metadata.name).toBe('id');
|
|
78
|
+
expect(params[0].metadata.description).toBe('User ID');
|
|
79
|
+
expect(params[0].metadata.required).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('should handle multiple params', () => {
|
|
83
|
+
class UserController {
|
|
84
|
+
public getUserPost(
|
|
85
|
+
@ApiParam({ name: 'id', description: 'User ID' })
|
|
86
|
+
id: string,
|
|
87
|
+
@ApiParam({ name: 'postId', description: 'Post ID' })
|
|
88
|
+
postId: string,
|
|
89
|
+
) {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const params = getApiParams(UserController.prototype, 'getUserPost');
|
|
93
|
+
expect(params.length).toBe(2);
|
|
94
|
+
// 参数顺序可能不同,但应该包含两个参数
|
|
95
|
+
const paramNames = params.map((p) => p.metadata.name);
|
|
96
|
+
expect(paramNames).toContain('id');
|
|
97
|
+
expect(paramNames).toContain('postId');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('ApiBody', () => {
|
|
102
|
+
test('should set body metadata', () => {
|
|
103
|
+
class UserController {
|
|
104
|
+
@ApiBody({
|
|
105
|
+
description: 'User data',
|
|
106
|
+
schema: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
name: { type: 'string' },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
public createUser() {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const body = getApiBody(UserController.prototype, 'createUser');
|
|
117
|
+
expect(body).toBeDefined();
|
|
118
|
+
expect(body?.description).toBe('User data');
|
|
119
|
+
expect(body?.schema).toBeDefined();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('ApiResponse', () => {
|
|
124
|
+
test('should set response metadata', () => {
|
|
125
|
+
class UserController {
|
|
126
|
+
@ApiResponse({ status: 200, description: 'Success' })
|
|
127
|
+
public getUsers() {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const responses = getApiResponses(UserController.prototype, 'getUsers');
|
|
131
|
+
expect(responses).toBeDefined();
|
|
132
|
+
expect(responses.length).toBe(1);
|
|
133
|
+
expect(responses[0].status).toBe(200);
|
|
134
|
+
expect(responses[0].description).toBe('Success');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should handle multiple responses', () => {
|
|
138
|
+
class UserController {
|
|
139
|
+
@ApiResponse({ status: 200, description: 'Success' })
|
|
140
|
+
@ApiResponse({ status: 404, description: 'Not found' })
|
|
141
|
+
public getUser() {}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const responses = getApiResponses(UserController.prototype, 'getUser');
|
|
145
|
+
expect(responses.length).toBe(2);
|
|
146
|
+
// 响应顺序可能不同,但应该包含两个响应
|
|
147
|
+
const statuses = responses.map((r) => r.status);
|
|
148
|
+
expect(statuses).toContain(200);
|
|
149
|
+
expect(statuses).toContain(404);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { SwaggerGenerator } from '../../src/swagger/generator';
|
|
3
|
+
import { Controller } from '../../src/controller';
|
|
4
|
+
import { GET, POST } from '../../src/router/decorators';
|
|
5
|
+
import { Body, Param, Query } from '../../src/controller/decorators';
|
|
6
|
+
import {
|
|
7
|
+
ApiTags,
|
|
8
|
+
ApiOperation,
|
|
9
|
+
ApiParam,
|
|
10
|
+
ApiBody,
|
|
11
|
+
ApiResponse,
|
|
12
|
+
} from '../../src/swagger/decorators';
|
|
13
|
+
import { ControllerRegistry } from '../../src/controller/controller';
|
|
14
|
+
import { RouteRegistry } from '../../src/router/registry';
|
|
15
|
+
|
|
16
|
+
describe('SwaggerGenerator', () => {
|
|
17
|
+
let generator: SwaggerGenerator;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
generator = new SwaggerGenerator({
|
|
21
|
+
info: {
|
|
22
|
+
title: 'Test API',
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
ControllerRegistry.getInstance().clear();
|
|
27
|
+
RouteRegistry.getInstance().clear();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
ControllerRegistry.getInstance().clear();
|
|
32
|
+
RouteRegistry.getInstance().clear();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should generate basic swagger document', () => {
|
|
36
|
+
const document = generator.generate();
|
|
37
|
+
|
|
38
|
+
expect(document.openapi).toBe('3.0.0');
|
|
39
|
+
expect(document.info.title).toBe('Test API');
|
|
40
|
+
expect(document.info.version).toBe('1.0.0');
|
|
41
|
+
expect(document.paths).toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should generate paths from controllers', () => {
|
|
45
|
+
@Controller('/api/users')
|
|
46
|
+
@ApiTags('Users')
|
|
47
|
+
class UserController {
|
|
48
|
+
@GET('/')
|
|
49
|
+
@ApiOperation({ summary: 'Get all users' })
|
|
50
|
+
public getAllUsers() {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ControllerRegistry.getInstance().register(UserController);
|
|
56
|
+
const document = generator.generate();
|
|
57
|
+
|
|
58
|
+
expect(document.paths).toBeDefined();
|
|
59
|
+
expect(document.paths['/api/users']).toBeDefined();
|
|
60
|
+
expect(document.paths['/api/users'].get).toBeDefined();
|
|
61
|
+
expect(document.paths['/api/users'].get?.summary).toBe('Get all users');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should include controller tags in path items', () => {
|
|
65
|
+
@Controller('/api/users')
|
|
66
|
+
@ApiTags('Users')
|
|
67
|
+
class UserController {
|
|
68
|
+
@GET('/')
|
|
69
|
+
public getAllUsers() {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ControllerRegistry.getInstance().register(UserController);
|
|
75
|
+
const document = generator.generate();
|
|
76
|
+
|
|
77
|
+
const path = document.paths['/api/users'];
|
|
78
|
+
expect(path).toBeDefined();
|
|
79
|
+
expect(path.get?.tags).toBeDefined();
|
|
80
|
+
expect(path.get?.tags).toContain('Users');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should include path parameters', () => {
|
|
84
|
+
@Controller('/api/users')
|
|
85
|
+
class UserController {
|
|
86
|
+
@GET('/:id')
|
|
87
|
+
@ApiParam({ name: 'id', description: 'User ID', required: true })
|
|
88
|
+
public getUser(@Param('id') id: string) {
|
|
89
|
+
return { id };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ControllerRegistry.getInstance().register(UserController);
|
|
94
|
+
const document = generator.generate();
|
|
95
|
+
|
|
96
|
+
const path = document.paths['/api/users/{id}'];
|
|
97
|
+
expect(path).toBeDefined();
|
|
98
|
+
expect(path.get?.parameters).toBeDefined();
|
|
99
|
+
const param = path.get?.parameters?.find((p) => p.name === 'id');
|
|
100
|
+
expect(param).toBeDefined();
|
|
101
|
+
expect(param?.required).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('should include request body', () => {
|
|
105
|
+
@Controller('/api/users')
|
|
106
|
+
class UserController {
|
|
107
|
+
@POST('/')
|
|
108
|
+
@ApiBody({
|
|
109
|
+
description: 'User data',
|
|
110
|
+
schema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
name: { type: 'string' },
|
|
114
|
+
email: { type: 'string' },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
public createUser(@Body() data: { name: string; email: string }) {
|
|
119
|
+
return data;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
ControllerRegistry.getInstance().register(UserController);
|
|
124
|
+
const document = generator.generate();
|
|
125
|
+
|
|
126
|
+
const path = document.paths['/api/users'];
|
|
127
|
+
expect(path.post?.requestBody).toBeDefined();
|
|
128
|
+
expect(path.post?.requestBody?.content).toBeDefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('should include responses', () => {
|
|
132
|
+
@Controller('/api/users')
|
|
133
|
+
class UserController {
|
|
134
|
+
@GET('/')
|
|
135
|
+
@ApiResponse({ status: 200, description: 'Success' })
|
|
136
|
+
@ApiResponse({ status: 404, description: 'Not found' })
|
|
137
|
+
public getAllUsers() {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ControllerRegistry.getInstance().register(UserController);
|
|
143
|
+
const document = generator.generate();
|
|
144
|
+
|
|
145
|
+
const path = document.paths['/api/users'];
|
|
146
|
+
expect(path.get?.responses).toBeDefined();
|
|
147
|
+
expect(path.get?.responses?.['200']).toBeDefined();
|
|
148
|
+
expect(path.get?.responses?.['404']).toBeDefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('should handle query parameters', () => {
|
|
152
|
+
@Controller('/api/users')
|
|
153
|
+
class UserController {
|
|
154
|
+
@GET('/')
|
|
155
|
+
public getUsers(@Query('page') page?: number) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
ControllerRegistry.getInstance().register(UserController);
|
|
161
|
+
const document = generator.generate();
|
|
162
|
+
|
|
163
|
+
const path = document.paths['/api/users'];
|
|
164
|
+
expect(path.get).toBeDefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should convert path parameters to OpenAPI format', () => {
|
|
168
|
+
@Controller('/api/users')
|
|
169
|
+
class UserController {
|
|
170
|
+
@GET('/:id/posts/:postId')
|
|
171
|
+
public getUserPost(
|
|
172
|
+
@Param('id') id: string,
|
|
173
|
+
@Param('postId') postId: string,
|
|
174
|
+
) {
|
|
175
|
+
return { id, postId };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
ControllerRegistry.getInstance().register(UserController);
|
|
180
|
+
const document = generator.generate();
|
|
181
|
+
|
|
182
|
+
// 应该转换为 /api/users/{id}/posts/{postId}
|
|
183
|
+
const pathKey = Object.keys(document.paths).find((key) =>
|
|
184
|
+
key.includes('{id}'),
|
|
185
|
+
);
|
|
186
|
+
expect(pathKey).toBeDefined();
|
|
187
|
+
expect(pathKey).toContain('{id}');
|
|
188
|
+
expect(pathKey).toContain('{postId}');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('should handle empty controllers', () => {
|
|
192
|
+
@Controller('/api/empty')
|
|
193
|
+
class EmptyController {}
|
|
194
|
+
|
|
195
|
+
ControllerRegistry.getInstance().register(EmptyController);
|
|
196
|
+
const document = generator.generate();
|
|
197
|
+
|
|
198
|
+
// 没有路由的控制器不应该生成路径
|
|
199
|
+
expect(document.paths['/api/empty']).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|