@dangao/bun-server 1.0.1 → 1.1.2
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/dist/controller/controller.d.ts +1 -1
- package/dist/controller/controller.d.ts.map +1 -1
- package/dist/core/application.d.ts.map +1 -1
- package/dist/database/database-extension.d.ts.map +1 -1
- package/dist/database/database-module.d.ts.map +1 -1
- package/dist/database/orm/transaction-decorator.d.ts +1 -0
- package/dist/database/orm/transaction-decorator.d.ts.map +1 -1
- package/dist/database/orm/transaction-interceptor.d.ts +12 -3
- package/dist/database/orm/transaction-interceptor.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +678 -310
- package/dist/interceptor/base-interceptor.d.ts +94 -0
- package/dist/interceptor/base-interceptor.d.ts.map +1 -0
- package/dist/interceptor/builtin/cache-interceptor.d.ts +69 -0
- package/dist/interceptor/builtin/cache-interceptor.d.ts.map +1 -0
- package/dist/interceptor/builtin/index.d.ts +4 -0
- package/dist/interceptor/builtin/index.d.ts.map +1 -0
- package/dist/interceptor/builtin/log-interceptor.d.ts +56 -0
- package/dist/interceptor/builtin/log-interceptor.d.ts.map +1 -0
- package/dist/interceptor/builtin/permission-interceptor.d.ts +70 -0
- package/dist/interceptor/builtin/permission-interceptor.d.ts.map +1 -0
- package/dist/interceptor/index.d.ts +7 -0
- package/dist/interceptor/index.d.ts.map +1 -0
- package/dist/interceptor/interceptor-chain.d.ts +22 -0
- package/dist/interceptor/interceptor-chain.d.ts.map +1 -0
- package/dist/interceptor/interceptor-registry.d.ts +59 -0
- package/dist/interceptor/interceptor-registry.d.ts.map +1 -0
- package/dist/interceptor/metadata.d.ts +12 -0
- package/dist/interceptor/metadata.d.ts.map +1 -0
- package/dist/interceptor/types.d.ts +42 -0
- package/dist/interceptor/types.d.ts.map +1 -0
- package/dist/middleware/decorators.d.ts +2 -1
- package/dist/middleware/decorators.d.ts.map +1 -1
- package/dist/router/decorators.d.ts.map +1 -1
- package/dist/router/registry.d.ts +2 -1
- package/dist/router/registry.d.ts.map +1 -1
- package/dist/router/route.d.ts +3 -2
- package/dist/router/route.d.ts.map +1 -1
- package/dist/router/router.d.ts +2 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/websocket/decorators.d.ts +2 -1
- package/dist/websocket/decorators.d.ts.map +1 -1
- package/package.json +5 -3
- 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 +278 -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 +239 -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 +83 -0
- package/src/database/database-module.ts +121 -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 +76 -0
- package/src/database/orm/transaction-interceptor.ts +263 -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 +292 -0
- package/src/interceptor/base-interceptor.ts +203 -0
- package/src/interceptor/builtin/cache-interceptor.ts +169 -0
- package/src/interceptor/builtin/index.ts +28 -0
- package/src/interceptor/builtin/log-interceptor.ts +178 -0
- package/src/interceptor/builtin/permission-interceptor.ts +173 -0
- package/src/interceptor/index.ts +26 -0
- package/src/interceptor/interceptor-chain.ts +79 -0
- package/src/interceptor/interceptor-registry.ts +132 -0
- package/src/interceptor/metadata.ts +40 -0
- package/src/interceptor/types.ts +52 -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 +92 -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 +123 -0
- package/src/router/index.ts +6 -0
- package/src/router/registry.ts +99 -0
- package/src/router/route.ts +141 -0
- package/src/router/router.ts +242 -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 +53 -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/controller/path-combination.test.ts +207 -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/interceptor/builtin/cache-interceptor.test.ts +137 -0
- package/tests/interceptor/builtin/permission-interceptor.test.ts +182 -0
- package/tests/interceptor/interceptor-advanced-integration.test.ts +592 -0
- package/tests/interceptor/interceptor-arg-modification.test.ts +76 -0
- package/tests/interceptor/interceptor-chain.test.ts +199 -0
- package/tests/interceptor/interceptor-integration.test.ts +230 -0
- package/tests/interceptor/interceptor-registry.test.ts +200 -0
- package/tests/interceptor/perf/interceptor-performance.test.ts +341 -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 +46 -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,120 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { Application } from '../../src/core/application';
|
|
3
|
+
import { RouteRegistry } from '../../src/router/registry';
|
|
4
|
+
import { Context } from '../../src/core/context';
|
|
5
|
+
import { PerformanceHarness } from '../../src/testing/harness';
|
|
6
|
+
import { getTestPort } from '../utils/test-port';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 性能回归测试
|
|
10
|
+
* 确保关键路径的性能不会退化
|
|
11
|
+
*/
|
|
12
|
+
describe('Performance Regression Tests', () => {
|
|
13
|
+
test('router handle should be fast', async () => {
|
|
14
|
+
const registry = RouteRegistry.getInstance();
|
|
15
|
+
registry.get('/api/test', (ctx: Context) => {
|
|
16
|
+
return ctx.createResponse({ message: 'ok' });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const router = registry.getRouter();
|
|
20
|
+
const context = new Context(
|
|
21
|
+
new Request('http://localhost:3000/api/test'),
|
|
22
|
+
);
|
|
23
|
+
context.params = {};
|
|
24
|
+
|
|
25
|
+
const result = await PerformanceHarness.benchmark(
|
|
26
|
+
'router handle',
|
|
27
|
+
1000,
|
|
28
|
+
async () => {
|
|
29
|
+
await router.preHandle(context);
|
|
30
|
+
return await router.handle(context);
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// 路由处理应该快速完成
|
|
35
|
+
expect(result.durationMs).toBeLessThan(1000); // 1000次操作应该在1秒内完成
|
|
36
|
+
expect(result.opsPerSecond).toBeGreaterThan(1000);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('application request handling should be fast', async () => {
|
|
40
|
+
const port = getTestPort();
|
|
41
|
+
const app = new Application({ port });
|
|
42
|
+
const registry = RouteRegistry.getInstance();
|
|
43
|
+
registry.get('/api/ping', (ctx: Context) => {
|
|
44
|
+
return ctx.createResponse({ status: 'ok' });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await app.listen();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const result = await PerformanceHarness.benchmark(
|
|
51
|
+
'application request',
|
|
52
|
+
100,
|
|
53
|
+
async () => {
|
|
54
|
+
const response = await fetch(`http://localhost:${port}/api/ping`);
|
|
55
|
+
await response.text();
|
|
56
|
+
return response;
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// HTTP 请求处理应该快速完成(包含网络开销)
|
|
61
|
+
expect(result.durationMs).toBeLessThan(2000); // 100次请求应该在2秒内完成
|
|
62
|
+
expect(result.opsPerSecond).toBeGreaterThan(50);
|
|
63
|
+
} finally {
|
|
64
|
+
await app.stop();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('DI container resolve should be fast', async () => {
|
|
69
|
+
const { Container } = await import('../../src/di/container');
|
|
70
|
+
const { Injectable } = await import('../../src/di/decorators');
|
|
71
|
+
|
|
72
|
+
@Injectable()
|
|
73
|
+
class TestService {
|
|
74
|
+
public value = 'test';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const container = new Container();
|
|
78
|
+
container.register(TestService);
|
|
79
|
+
|
|
80
|
+
const result = await PerformanceHarness.benchmark(
|
|
81
|
+
'DI resolve',
|
|
82
|
+
10000,
|
|
83
|
+
() => {
|
|
84
|
+
return container.resolve(TestService);
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// DI 解析应该非常快(单例缓存)
|
|
89
|
+
expect(result.durationMs).toBeLessThan(100); // 10000次解析应该在100ms内完成
|
|
90
|
+
expect(result.opsPerSecond).toBeGreaterThan(10000);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('middleware pipeline should be fast', async () => {
|
|
94
|
+
const { MiddlewarePipeline } = await import('../../src/middleware/pipeline');
|
|
95
|
+
const { Context } = await import('../../src/core/context');
|
|
96
|
+
|
|
97
|
+
const pipeline = new MiddlewarePipeline();
|
|
98
|
+
pipeline.use(async (ctx, next) => {
|
|
99
|
+
return await next();
|
|
100
|
+
});
|
|
101
|
+
pipeline.use(async (ctx, next) => {
|
|
102
|
+
return await next();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const context = new Context(new Request('http://localhost:3000/test'));
|
|
106
|
+
const result = await PerformanceHarness.benchmark(
|
|
107
|
+
'middleware pipeline',
|
|
108
|
+
1000,
|
|
109
|
+
async () => {
|
|
110
|
+
return await pipeline.run(context, async () => {
|
|
111
|
+
return new Response('ok');
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// 中间件管道应该快速完成
|
|
117
|
+
expect(result.durationMs).toBeLessThan(500); // 1000次操作应该在500ms内完成
|
|
118
|
+
expect(result.opsPerSecond).toBeGreaterThan(1000);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } 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
|
+
QueueModule,
|
|
9
|
+
QueueService,
|
|
10
|
+
QUEUE_SERVICE_TOKEN,
|
|
11
|
+
MemoryQueueStore,
|
|
12
|
+
type QueueModuleOptions,
|
|
13
|
+
type Job,
|
|
14
|
+
} from '../../src/queue';
|
|
15
|
+
|
|
16
|
+
describe('QueueModule', () => {
|
|
17
|
+
let container: Container;
|
|
18
|
+
let moduleRegistry: ModuleRegistry;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
Reflect.deleteMetadata(MODULE_METADATA_KEY, QueueModule);
|
|
22
|
+
container = new Container();
|
|
23
|
+
moduleRegistry = ModuleRegistry.getInstance();
|
|
24
|
+
moduleRegistry.clear();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should register queue service provider', () => {
|
|
28
|
+
QueueModule.forRoot();
|
|
29
|
+
|
|
30
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, QueueModule);
|
|
31
|
+
expect(metadata).toBeDefined();
|
|
32
|
+
expect(metadata.providers).toBeDefined();
|
|
33
|
+
|
|
34
|
+
const queueProvider = metadata.providers.find(
|
|
35
|
+
(provider: any) => provider.provide === QUEUE_SERVICE_TOKEN,
|
|
36
|
+
);
|
|
37
|
+
expect(queueProvider).toBeDefined();
|
|
38
|
+
expect(queueProvider.useValue).toBeInstanceOf(QueueService);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('should use custom store when provided', () => {
|
|
42
|
+
const customStore = new MemoryQueueStore();
|
|
43
|
+
QueueModule.forRoot({
|
|
44
|
+
store: customStore,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, QueueModule);
|
|
48
|
+
const queueProvider = metadata.providers.find(
|
|
49
|
+
(provider: any) => provider.provide === QUEUE_SERVICE_TOKEN,
|
|
50
|
+
);
|
|
51
|
+
expect(queueProvider).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should configure default queue name', () => {
|
|
55
|
+
QueueModule.forRoot({
|
|
56
|
+
defaultQueue: 'custom-queue',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, QueueModule);
|
|
60
|
+
expect(metadata).toBeDefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should configure worker settings', () => {
|
|
64
|
+
QueueModule.forRoot({
|
|
65
|
+
enableWorker: false,
|
|
66
|
+
concurrency: 5,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, QueueModule);
|
|
70
|
+
expect(metadata).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('QueueService', () => {
|
|
75
|
+
let service: QueueService;
|
|
76
|
+
let store: MemoryQueueStore;
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
store = new MemoryQueueStore();
|
|
80
|
+
service = new QueueService({
|
|
81
|
+
store,
|
|
82
|
+
defaultQueue: 'default',
|
|
83
|
+
enableWorker: false, // 禁用工作进程以便测试
|
|
84
|
+
concurrency: 1,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
service.destroy();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('should add job to queue', async () => {
|
|
93
|
+
const jobId = await service.add('test-job', { data: 'test' });
|
|
94
|
+
expect(jobId).toBeDefined();
|
|
95
|
+
expect(typeof jobId).toBe('string');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('should get job by id', async () => {
|
|
99
|
+
const jobId = await service.add('test-job', { data: 'test' });
|
|
100
|
+
const job = await service.get(jobId);
|
|
101
|
+
expect(job).toBeDefined();
|
|
102
|
+
expect(job?.name).toBe('test-job');
|
|
103
|
+
expect(job?.data).toEqual({ data: 'test' });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('should delete job', async () => {
|
|
107
|
+
const jobId = await service.add('test-job', { data: 'test' });
|
|
108
|
+
const deleted = await service.delete(jobId);
|
|
109
|
+
expect(deleted).toBe(true);
|
|
110
|
+
const job = await service.get(jobId);
|
|
111
|
+
expect(job).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('should clear queue', async () => {
|
|
115
|
+
await service.add('test-job', { data: 'test' });
|
|
116
|
+
await service.add('test-job2', { data: 'test2' });
|
|
117
|
+
const cleared = await service.clear();
|
|
118
|
+
expect(cleared).toBe(true);
|
|
119
|
+
const count = await service.count();
|
|
120
|
+
expect(count).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('should count jobs in queue', async () => {
|
|
124
|
+
await service.add('test-job', { data: 'test' });
|
|
125
|
+
await service.add('test-job2', { data: 'test2' });
|
|
126
|
+
const count = await service.count();
|
|
127
|
+
expect(count).toBe(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('should register job handler', async () => {
|
|
131
|
+
let handlerCalled = false;
|
|
132
|
+
await service.registerHandler('test-job', async (job) => {
|
|
133
|
+
handlerCalled = true;
|
|
134
|
+
expect(job.name).toBe('test-job');
|
|
135
|
+
});
|
|
136
|
+
expect(handlerCalled).toBe(false); // Handler not called yet
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('should add job with options', async () => {
|
|
140
|
+
const jobId = await service.add(
|
|
141
|
+
'test-job',
|
|
142
|
+
{ data: 'test' },
|
|
143
|
+
{
|
|
144
|
+
delay: 1000,
|
|
145
|
+
priority: 10,
|
|
146
|
+
attempts: 3,
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
const job = await service.get(jobId);
|
|
150
|
+
expect(job).toBeDefined();
|
|
151
|
+
expect(job?.options?.delay).toBe(1000);
|
|
152
|
+
expect(job?.options?.priority).toBe(10);
|
|
153
|
+
expect(job?.options?.attempts).toBe(3);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('MemoryQueueStore', () => {
|
|
158
|
+
let store: MemoryQueueStore;
|
|
159
|
+
|
|
160
|
+
beforeEach(() => {
|
|
161
|
+
store = new MemoryQueueStore();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('should add and get job', async () => {
|
|
165
|
+
const jobId = await store.add('test-queue', {
|
|
166
|
+
name: 'test-job',
|
|
167
|
+
data: { test: 'data' },
|
|
168
|
+
});
|
|
169
|
+
expect(jobId).toBeDefined();
|
|
170
|
+
|
|
171
|
+
const job = await store.get('test-queue', jobId);
|
|
172
|
+
expect(job).toBeDefined();
|
|
173
|
+
expect(job?.name).toBe('test-job');
|
|
174
|
+
expect(job?.data).toEqual({ test: 'data' });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('should get next job', async () => {
|
|
178
|
+
await store.add('test-queue', {
|
|
179
|
+
name: 'job1',
|
|
180
|
+
data: { id: 1 },
|
|
181
|
+
});
|
|
182
|
+
await store.add('test-queue', {
|
|
183
|
+
name: 'job2',
|
|
184
|
+
data: { id: 2 },
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const nextJob = await store.getNext('test-queue');
|
|
188
|
+
expect(nextJob).toBeDefined();
|
|
189
|
+
expect(nextJob?.name).toBe('job1');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('should update job status', async () => {
|
|
193
|
+
const jobId = await store.add('test-queue', {
|
|
194
|
+
name: 'test-job',
|
|
195
|
+
data: {},
|
|
196
|
+
});
|
|
197
|
+
const updated = await store.updateStatus('test-queue', jobId, 'active');
|
|
198
|
+
expect(updated).toBe(true);
|
|
199
|
+
|
|
200
|
+
const job = await store.get('test-queue', jobId);
|
|
201
|
+
expect(job?.status).toBe('active');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('should count jobs', async () => {
|
|
205
|
+
await store.add('test-queue', { name: 'job1', data: {} });
|
|
206
|
+
await store.add('test-queue', { name: 'job2', data: {} });
|
|
207
|
+
const count = await store.count('test-queue');
|
|
208
|
+
expect(count).toBe(2);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('should clear queue', async () => {
|
|
212
|
+
await store.add('test-queue', { name: 'job1', data: {} });
|
|
213
|
+
await store.clear('test-queue');
|
|
214
|
+
const count = await store.count('test-queue');
|
|
215
|
+
expect(count).toBe(0);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { BodyParser } from '../../src/request/body-parser';
|
|
3
|
+
|
|
4
|
+
describe('BodyParser', () => {
|
|
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 body = await BodyParser.parse(request);
|
|
13
|
+
expect(body).toEqual({ name: 'John', age: 30 });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('should parse URLEncoded body', async () => {
|
|
17
|
+
const request = new Request('http://localhost:3000/api/users', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
20
|
+
body: 'name=John&age=30',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const body = await BodyParser.parse(request);
|
|
24
|
+
expect(body).toEqual({ name: 'John', age: '30' });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should parse text body', async () => {
|
|
28
|
+
const request = new Request('http://localhost:3000/api/users', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
31
|
+
body: 'Hello World',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const body = await BodyParser.parse(request);
|
|
35
|
+
expect(body).toBe('Hello World');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should return undefined for empty body', async () => {
|
|
39
|
+
const request = new Request('http://localhost:3000/api/users', {
|
|
40
|
+
method: 'GET',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const body = await BodyParser.parse(request);
|
|
44
|
+
expect(body).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('should parse JSON as default', async () => {
|
|
48
|
+
const request = new Request('http://localhost:3000/api/users', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
body: JSON.stringify({ name: 'John' }),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const body = await BodyParser.parse(request);
|
|
54
|
+
expect(body).toEqual({ name: 'John' });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should fall back to text when JSON parse fails', async () => {
|
|
58
|
+
const request = new Request('http://localhost:3000/api/users', {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
body: 'plain-text',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const body = await BodyParser.parse(request);
|
|
64
|
+
expect(body).toBe('plain-text');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should respect explicit zero content-length', async () => {
|
|
68
|
+
const request = new Request('http://localhost:3000/api/users', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'Content-Length': '0' },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const body = await BodyParser.parse(request);
|
|
74
|
+
expect(body).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should return undefined for HEAD requests', async () => {
|
|
78
|
+
const request = new Request('http://localhost:3000/api/users', {
|
|
79
|
+
method: 'HEAD',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const body = await BodyParser.parse(request);
|
|
83
|
+
expect(body).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('should parse text content when content headers missing but body exists', async () => {
|
|
87
|
+
const request = new Request('http://localhost:3000/api/users', {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
body: 'fallback text',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const body = await BodyParser.parse(request);
|
|
93
|
+
expect(body).toBe('fallback text');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { ResponseBuilder } from '../../src/request/response';
|
|
6
|
+
|
|
7
|
+
describe('ResponseBuilder', () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'response-builder-'));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
if (tmpDir) {
|
|
16
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should create JSON response', async () => {
|
|
21
|
+
const response = ResponseBuilder.json({ message: 'Hello' });
|
|
22
|
+
expect(response.status).toBe(200);
|
|
23
|
+
expect(response.headers.get('Content-Type')).toBe('application/json');
|
|
24
|
+
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
expect(data.message).toBe('Hello');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should create JSON response with custom status', async () => {
|
|
30
|
+
const response = ResponseBuilder.json({ error: 'Not Found' }, 404);
|
|
31
|
+
expect(response.status).toBe(404);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('should create text response', async () => {
|
|
35
|
+
const response = ResponseBuilder.text('Hello World');
|
|
36
|
+
expect(response.status).toBe(200);
|
|
37
|
+
expect(response.headers.get('Content-Type')).toBe('text/plain');
|
|
38
|
+
|
|
39
|
+
const text = await response.text();
|
|
40
|
+
expect(text).toBe('Hello World');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should create HTML response', async () => {
|
|
44
|
+
const response = ResponseBuilder.html('<h1>Hello</h1>');
|
|
45
|
+
expect(response.status).toBe(200);
|
|
46
|
+
expect(response.headers.get('Content-Type')).toBe('text/html');
|
|
47
|
+
|
|
48
|
+
const html = await response.text();
|
|
49
|
+
expect(html).toBe('<h1>Hello</h1>');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('should create empty response', () => {
|
|
53
|
+
const response = ResponseBuilder.empty(204);
|
|
54
|
+
expect(response.status).toBe(204);
|
|
55
|
+
expect(response.body).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('should create redirect response', () => {
|
|
59
|
+
const response = ResponseBuilder.redirect('http://example.com');
|
|
60
|
+
expect(response.status).toBe(302);
|
|
61
|
+
expect(response.headers.get('Location')).toBe('http://example.com');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should create error response', async () => {
|
|
65
|
+
const response = ResponseBuilder.error('Internal Server Error', 500);
|
|
66
|
+
expect(response.status).toBe(500);
|
|
67
|
+
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
expect(data.error).toBe('Internal Server Error');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('should create file response from path', async () => {
|
|
73
|
+
const filePath = join(tmpDir, 'hello.txt');
|
|
74
|
+
await Bun.write(filePath, 'file-content');
|
|
75
|
+
|
|
76
|
+
const response = ResponseBuilder.file(filePath, {
|
|
77
|
+
fileName: 'download.txt',
|
|
78
|
+
contentType: 'text/plain',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(response.status).toBe(200);
|
|
82
|
+
expect(response.headers.get('Content-Disposition')).toBe('attachment; filename="download.txt"');
|
|
83
|
+
expect(response.headers.get('Content-Type')).toBe('text/plain');
|
|
84
|
+
expect(await response.text()).toBe('file-content');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('should create file response from binary source', async () => {
|
|
88
|
+
const buffer = new TextEncoder().encode('binary-content');
|
|
89
|
+
const response = ResponseBuilder.file(buffer, {
|
|
90
|
+
status: 206,
|
|
91
|
+
headers: { ETag: '123' },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(response.status).toBe(206);
|
|
95
|
+
expect(response.headers.get('ETag')).toBe('123');
|
|
96
|
+
expect(await response.text()).toBe('binary-content');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { Controller, ControllerRegistry } from '../../src/controller/controller';
|
|
4
|
+
import { GET } from '../../src/router/decorators';
|
|
5
|
+
import { getRouteMetadata } from '../../src/controller/metadata';
|
|
6
|
+
import { RouteRegistry } from '../../src/router/registry';
|
|
7
|
+
|
|
8
|
+
describe('Router Decorators', () => {
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
RouteRegistry.getInstance().clear();
|
|
11
|
+
ControllerRegistry.getInstance().clear();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('should record metadata for controller methods', () => {
|
|
15
|
+
@Controller('/decorator')
|
|
16
|
+
class DecoratedController {
|
|
17
|
+
@GET('/list')
|
|
18
|
+
public list() {
|
|
19
|
+
return 'ok';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const metadata = getRouteMetadata(DecoratedController.prototype);
|
|
24
|
+
expect(metadata.length).toBe(1);
|
|
25
|
+
expect(metadata[0]?.path).toBe('/list');
|
|
26
|
+
expect(metadata[0]?.method).toBe('GET');
|
|
27
|
+
expect(typeof metadata[0]?.handler).toBe('function');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('should throw error when registering class without @Controller decorator', () => {
|
|
31
|
+
// 注意:由于装饰器应用顺序,@GET 装饰器会保存元数据,不会立即报错
|
|
32
|
+
class PlainHandlers {
|
|
33
|
+
@GET('/plain')
|
|
34
|
+
public handler() {
|
|
35
|
+
return { ok: true };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 但尝试注册时会报错,因为类没有 @Controller 装饰器
|
|
40
|
+
const registry = ControllerRegistry.getInstance();
|
|
41
|
+
expect(() => {
|
|
42
|
+
registry.register(PlainHandlers);
|
|
43
|
+
}).toThrow('Controller PlainHandlers must be decorated with @Controller()');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import { RouteRegistry } from '../../src/router/registry';
|
|
3
|
+
import { Context } from '../../src/core/context';
|
|
4
|
+
|
|
5
|
+
describe('RouteRegistry', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
// 重置单例(在实际使用中,单例会保持状态)
|
|
8
|
+
// 这里我们只是测试基本功能
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('should get singleton instance', () => {
|
|
12
|
+
const instance1 = RouteRegistry.getInstance();
|
|
13
|
+
const instance2 = RouteRegistry.getInstance();
|
|
14
|
+
|
|
15
|
+
expect(instance1).toBe(instance2);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should register route', () => {
|
|
19
|
+
const registry = RouteRegistry.getInstance();
|
|
20
|
+
const handler = (ctx: Context) => ctx.createResponse({});
|
|
21
|
+
|
|
22
|
+
registry.register('GET', '/api/users', handler);
|
|
23
|
+
|
|
24
|
+
const router = registry.getRouter();
|
|
25
|
+
const route = router.findRoute('GET', '/api/users');
|
|
26
|
+
expect(route).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should register GET route', () => {
|
|
30
|
+
const registry = RouteRegistry.getInstance();
|
|
31
|
+
const handler = (ctx: Context) => ctx.createResponse({});
|
|
32
|
+
|
|
33
|
+
registry.get('/api/users', handler);
|
|
34
|
+
|
|
35
|
+
const router = registry.getRouter();
|
|
36
|
+
const route = router.findRoute('GET', '/api/users');
|
|
37
|
+
expect(route).toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should register POST route', () => {
|
|
41
|
+
const registry = RouteRegistry.getInstance();
|
|
42
|
+
const handler = (ctx: Context) => ctx.createResponse({});
|
|
43
|
+
|
|
44
|
+
registry.post('/api/users', handler);
|
|
45
|
+
|
|
46
|
+
const router = registry.getRouter();
|
|
47
|
+
const route = router.findRoute('POST', '/api/users');
|
|
48
|
+
expect(route).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|