@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,314 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import { Application } from '../../src/core/application';
|
|
3
|
+
import { Context } from '../../src/core/context';
|
|
4
|
+
import { Controller } from '../../src/controller';
|
|
5
|
+
import { GET } from '../../src/router/decorators';
|
|
6
|
+
import { createRateLimitMiddleware, MemoryRateLimitStore, type RateLimitOptions } from '../../src/middleware/builtin/rate-limit';
|
|
7
|
+
import { RateLimit } from '../../src/middleware/decorators';
|
|
8
|
+
|
|
9
|
+
describe('Rate Limit Middleware', () => {
|
|
10
|
+
let app: Application;
|
|
11
|
+
let port: number;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
port = 3000 + Math.floor(Math.random() * 10000);
|
|
15
|
+
app = new Application({ port });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should allow requests within limit', async () => {
|
|
19
|
+
const middleware = createRateLimitMiddleware({
|
|
20
|
+
max: 5,
|
|
21
|
+
windowMs: 60000,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let requestCount = 0;
|
|
25
|
+
const handler = async (context: Context) => {
|
|
26
|
+
requestCount++;
|
|
27
|
+
return context.createResponse({ count: requestCount });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const context1 = new Context(new Request('http://localhost:3000/test'));
|
|
31
|
+
const response1 = await middleware(context1, async () => handler(context1));
|
|
32
|
+
|
|
33
|
+
expect(response1.status).toBe(200);
|
|
34
|
+
expect(response1.headers.get('RateLimit-Limit')).toBe('5');
|
|
35
|
+
expect(response1.headers.get('RateLimit-Remaining')).toBe('4');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should block requests exceeding limit', async () => {
|
|
39
|
+
const store = new MemoryRateLimitStore();
|
|
40
|
+
const middleware = createRateLimitMiddleware({
|
|
41
|
+
max: 2,
|
|
42
|
+
windowMs: 60000,
|
|
43
|
+
store,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const handler = async (context: Context) => {
|
|
47
|
+
return context.createResponse({ success: true });
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const context1 = new Context(new Request('http://localhost:3000/test'));
|
|
51
|
+
const context2 = new Context(new Request('http://localhost:3000/test'));
|
|
52
|
+
const context3 = new Context(new Request('http://localhost:3000/test'));
|
|
53
|
+
|
|
54
|
+
// 前两次请求应该成功
|
|
55
|
+
const response1 = await middleware(context1, async () => handler(context1));
|
|
56
|
+
expect(response1.status).toBe(200);
|
|
57
|
+
|
|
58
|
+
const response2 = await middleware(context2, async () => handler(context2));
|
|
59
|
+
expect(response2.status).toBe(200);
|
|
60
|
+
expect(response2.headers.get('RateLimit-Remaining')).toBe('0');
|
|
61
|
+
|
|
62
|
+
// 第三次请求应该被限制
|
|
63
|
+
const response3 = await middleware(context3, async () => handler(context3));
|
|
64
|
+
expect(response3.status).toBe(429);
|
|
65
|
+
const data3 = await response3.json();
|
|
66
|
+
expect(data3.error).toBe('Too Many Requests');
|
|
67
|
+
expect(data3.retryAfter).toBeDefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('should use different keys for different IPs', async () => {
|
|
71
|
+
const store = new MemoryRateLimitStore();
|
|
72
|
+
const middleware = createRateLimitMiddleware({
|
|
73
|
+
max: 1,
|
|
74
|
+
windowMs: 60000,
|
|
75
|
+
store,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const handler = async (context: Context) => {
|
|
79
|
+
return context.createResponse({ success: true });
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// IP 1 的请求
|
|
83
|
+
const request1 = new Request('http://localhost:3000/test', {
|
|
84
|
+
headers: { 'X-Forwarded-For': '192.168.1.1' },
|
|
85
|
+
});
|
|
86
|
+
const context1 = new Context(request1);
|
|
87
|
+
const response1 = await middleware(context1, async () => handler(context1));
|
|
88
|
+
expect(response1.status).toBe(200);
|
|
89
|
+
|
|
90
|
+
// IP 2 的请求(应该也成功,因为使用不同的键)
|
|
91
|
+
const request2 = new Request('http://localhost:3000/test', {
|
|
92
|
+
headers: { 'X-Forwarded-For': '192.168.1.2' },
|
|
93
|
+
});
|
|
94
|
+
const context2 = new Context(request2);
|
|
95
|
+
const response2 = await middleware(context2, async () => handler(context2));
|
|
96
|
+
expect(response2.status).toBe(200);
|
|
97
|
+
|
|
98
|
+
// IP 1 的第二次请求(应该被限制)
|
|
99
|
+
const request3 = new Request('http://localhost:3000/test', {
|
|
100
|
+
headers: { 'X-Forwarded-For': '192.168.1.1' },
|
|
101
|
+
});
|
|
102
|
+
const context3 = new Context(request3);
|
|
103
|
+
const response3 = await middleware(context3, async () => handler(context3));
|
|
104
|
+
expect(response3.status).toBe(429);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should reset after window expires', async () => {
|
|
108
|
+
const store = new MemoryRateLimitStore();
|
|
109
|
+
const middleware = createRateLimitMiddleware({
|
|
110
|
+
max: 1,
|
|
111
|
+
windowMs: 100, // 100ms 窗口
|
|
112
|
+
store,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const handler = async (context: Context) => {
|
|
116
|
+
return context.createResponse({ success: true });
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const context1 = new Context(new Request('http://localhost:3000/test'));
|
|
120
|
+
const response1 = await middleware(context1, async () => handler(context1));
|
|
121
|
+
expect(response1.status).toBe(200);
|
|
122
|
+
|
|
123
|
+
// 立即再次请求应该被限制
|
|
124
|
+
const context2 = new Context(new Request('http://localhost:3000/test'));
|
|
125
|
+
const response2 = await middleware(context2, async () => handler(context2));
|
|
126
|
+
expect(response2.status).toBe(429);
|
|
127
|
+
|
|
128
|
+
// 等待窗口过期
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
130
|
+
|
|
131
|
+
// 应该可以再次请求
|
|
132
|
+
const context3 = new Context(new Request('http://localhost:3000/test'));
|
|
133
|
+
const response3 = await middleware(context3, async () => handler(context3));
|
|
134
|
+
expect(response3.status).toBe(200);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should work with @RateLimit decorator', async () => {
|
|
138
|
+
@Controller('/api/rate-limit')
|
|
139
|
+
class RateLimitController {
|
|
140
|
+
@RateLimit({ max: 2, windowMs: 60000 })
|
|
141
|
+
@GET('/test')
|
|
142
|
+
public test() {
|
|
143
|
+
return { message: 'ok' };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
app.registerController(RateLimitController);
|
|
148
|
+
app.listen();
|
|
149
|
+
|
|
150
|
+
// 前两次请求应该成功
|
|
151
|
+
const response1 = await fetch(`http://localhost:${port}/api/rate-limit/test`);
|
|
152
|
+
expect(response1.status).toBe(200);
|
|
153
|
+
expect(response1.headers.get('RateLimit-Limit')).toBe('2');
|
|
154
|
+
|
|
155
|
+
const response2 = await fetch(`http://localhost:${port}/api/rate-limit/test`);
|
|
156
|
+
expect(response2.status).toBe(200);
|
|
157
|
+
|
|
158
|
+
// 第三次请求应该被限制
|
|
159
|
+
const response3 = await fetch(`http://localhost:${port}/api/rate-limit/test`);
|
|
160
|
+
expect(response3.status).toBe(429);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should support custom key generator', async () => {
|
|
164
|
+
const store = new MemoryRateLimitStore();
|
|
165
|
+
const middleware = createRateLimitMiddleware({
|
|
166
|
+
max: 1,
|
|
167
|
+
windowMs: 60000,
|
|
168
|
+
store,
|
|
169
|
+
keyGenerator: (context) => {
|
|
170
|
+
const userId = context.getHeader('X-User-Id');
|
|
171
|
+
return userId ? `user:${userId}` : `ip:${context.getClientIp()}`;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const handler = async (context: Context) => {
|
|
176
|
+
return context.createResponse({ success: true });
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// User 1 的请求
|
|
180
|
+
const request1 = new Request('http://localhost:3000/test', {
|
|
181
|
+
headers: { 'X-User-Id': 'user1' },
|
|
182
|
+
});
|
|
183
|
+
const context1 = new Context(request1);
|
|
184
|
+
const response1 = await middleware(context1, async () => handler(context1));
|
|
185
|
+
expect(response1.status).toBe(200);
|
|
186
|
+
|
|
187
|
+
// User 2 的请求(应该也成功)
|
|
188
|
+
const request2 = new Request('http://localhost:3000/test', {
|
|
189
|
+
headers: { 'X-User-Id': 'user2' },
|
|
190
|
+
});
|
|
191
|
+
const context2 = new Context(request2);
|
|
192
|
+
const response2 = await middleware(context2, async () => handler(context2));
|
|
193
|
+
expect(response2.status).toBe(200);
|
|
194
|
+
|
|
195
|
+
// User 1 的第二次请求(应该被限制)
|
|
196
|
+
const request3 = new Request('http://localhost:3000/test', {
|
|
197
|
+
headers: { 'X-User-Id': 'user1' },
|
|
198
|
+
});
|
|
199
|
+
const context3 = new Context(request3);
|
|
200
|
+
const response3 = await middleware(context3, async () => handler(context3));
|
|
201
|
+
expect(response3.status).toBe(429);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('should support custom message and status code', async () => {
|
|
205
|
+
const middleware = createRateLimitMiddleware({
|
|
206
|
+
max: 1,
|
|
207
|
+
windowMs: 60000,
|
|
208
|
+
message: 'Rate limit exceeded',
|
|
209
|
+
statusCode: 503,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const handler = async (context: Context) => {
|
|
213
|
+
return context.createResponse({ success: true });
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const context1 = new Context(new Request('http://localhost:3000/test'));
|
|
217
|
+
await middleware(context1, async () => handler(context1));
|
|
218
|
+
|
|
219
|
+
const context2 = new Context(new Request('http://localhost:3000/test'));
|
|
220
|
+
const response2 = await middleware(context2, async () => handler(context2));
|
|
221
|
+
expect(response2.status).toBe(503);
|
|
222
|
+
const data = await response2.json();
|
|
223
|
+
expect(data.error).toBe('Rate limit exceeded');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('should include standard headers', async () => {
|
|
227
|
+
const middleware = createRateLimitMiddleware({
|
|
228
|
+
max: 10,
|
|
229
|
+
windowMs: 60000,
|
|
230
|
+
standardHeaders: true,
|
|
231
|
+
legacyHeaders: true,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const handler = async (context: Context) => {
|
|
235
|
+
return context.createResponse({ success: true });
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const context = new Context(new Request('http://localhost:3000/test'));
|
|
239
|
+
const response = await middleware(context, async () => handler(context));
|
|
240
|
+
|
|
241
|
+
expect(response.headers.get('RateLimit-Limit')).toBe('10');
|
|
242
|
+
expect(response.headers.get('RateLimit-Remaining')).toBe('9');
|
|
243
|
+
expect(response.headers.get('RateLimit-Reset')).toBeDefined();
|
|
244
|
+
expect(response.headers.get('X-RateLimit-Limit')).toBe('10');
|
|
245
|
+
expect(response.headers.get('X-RateLimit-Remaining')).toBe('9');
|
|
246
|
+
expect(response.headers.get('X-RateLimit-Reset')).toBeDefined();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('should get client IP from X-Forwarded-For header', () => {
|
|
250
|
+
const request = new Request('http://localhost:3000/test', {
|
|
251
|
+
headers: { 'X-Forwarded-For': '192.168.1.100, 10.0.0.1' },
|
|
252
|
+
});
|
|
253
|
+
const context = new Context(request);
|
|
254
|
+
const ip = context.getClientIp();
|
|
255
|
+
expect(ip).toBe('192.168.1.100');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('should get client IP from X-Real-IP header', () => {
|
|
259
|
+
const request = new Request('http://localhost:3000/test', {
|
|
260
|
+
headers: { 'X-Real-IP': '192.168.1.200' },
|
|
261
|
+
});
|
|
262
|
+
const context = new Context(request);
|
|
263
|
+
const ip = context.getClientIp();
|
|
264
|
+
expect(ip).toBe('192.168.1.200');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('MemoryRateLimitStore', () => {
|
|
269
|
+
let store: MemoryRateLimitStore;
|
|
270
|
+
|
|
271
|
+
beforeEach(() => {
|
|
272
|
+
store = new MemoryRateLimitStore();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('should increment count', async () => {
|
|
276
|
+
const count1 = await store.increment('test-key', 1000);
|
|
277
|
+
expect(count1).toBe(1);
|
|
278
|
+
|
|
279
|
+
const count2 = await store.increment('test-key', 1000);
|
|
280
|
+
expect(count2).toBe(2);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('should reset count after window expires', async () => {
|
|
284
|
+
await store.increment('test-key', 100);
|
|
285
|
+
await store.increment('test-key', 100);
|
|
286
|
+
|
|
287
|
+
// 等待窗口过期
|
|
288
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
289
|
+
|
|
290
|
+
const count = await store.get('test-key');
|
|
291
|
+
expect(count).toBe(0);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('should reset count manually', async () => {
|
|
295
|
+
await store.increment('test-key', 1000);
|
|
296
|
+
await store.reset('test-key');
|
|
297
|
+
|
|
298
|
+
const count = await store.get('test-key');
|
|
299
|
+
expect(count).toBe(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('should cleanup expired entries', async () => {
|
|
303
|
+
await store.increment('key1', 100);
|
|
304
|
+
await store.increment('key2', 1000);
|
|
305
|
+
|
|
306
|
+
// 等待 key1 过期
|
|
307
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
308
|
+
|
|
309
|
+
store.cleanup();
|
|
310
|
+
|
|
311
|
+
expect(await store.get('key1')).toBe(0);
|
|
312
|
+
expect(await store.get('key2')).toBeGreaterThan(0);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdir, rm } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
import { createStaticFileMiddleware } from '../../src/middleware/builtin/static-file';
|
|
6
|
+
import { Context } from '../../src/core/context';
|
|
7
|
+
|
|
8
|
+
const TMP_DIR = join(process.cwd(), 'tmp-static-test');
|
|
9
|
+
|
|
10
|
+
async function writeFile(path: string, content: string): Promise<void> {
|
|
11
|
+
await Bun.write(path, content);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('StaticFileMiddleware', () => {
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
await mkdir(TMP_DIR, { recursive: true });
|
|
17
|
+
await writeFile(join(TMP_DIR, 'hello.txt'), 'hello world');
|
|
18
|
+
await mkdir(join(TMP_DIR, 'nested'), { recursive: true });
|
|
19
|
+
await writeFile(join(TMP_DIR, 'nested', 'index.html'), '<h1>Nested</h1>');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterAll(async () => {
|
|
23
|
+
await rm(TMP_DIR, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should serve static files under prefix', async () => {
|
|
27
|
+
const middleware = createStaticFileMiddleware({
|
|
28
|
+
root: TMP_DIR,
|
|
29
|
+
prefix: '/static',
|
|
30
|
+
enableCache: false,
|
|
31
|
+
});
|
|
32
|
+
const ctx = new Context(new Request('http://localhost/static/hello.txt'));
|
|
33
|
+
const response = await middleware(ctx, async () => ctx.createResponse({ ok: false }));
|
|
34
|
+
expect(response.status).toBe(200);
|
|
35
|
+
expect(await response.text()).toBe('hello world');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should serve index file for directory', async () => {
|
|
39
|
+
const middleware = createStaticFileMiddleware({
|
|
40
|
+
root: TMP_DIR,
|
|
41
|
+
prefix: '/static',
|
|
42
|
+
enableCache: false,
|
|
43
|
+
});
|
|
44
|
+
const ctx = new Context(new Request('http://localhost/static/nested/'));
|
|
45
|
+
const response = await middleware(ctx, async () => ctx.createResponse({ ok: false }));
|
|
46
|
+
expect(response.status).toBe(200);
|
|
47
|
+
expect(await response.text()).toContain('Nested');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('should block path traversal', async () => {
|
|
51
|
+
const middleware = createStaticFileMiddleware({
|
|
52
|
+
root: TMP_DIR,
|
|
53
|
+
prefix: '/static',
|
|
54
|
+
});
|
|
55
|
+
const ctx = new Context(new Request('http://localhost/static/secret.txt'));
|
|
56
|
+
ctx.path = '/static/../secret.txt';
|
|
57
|
+
const response = await middleware(ctx, async () => ctx.createResponse({ ok: true }));
|
|
58
|
+
expect(response.status).toBe(403);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { PerformanceHarness, StressTester } from '../../src/testing/harness';
|
|
4
|
+
import { Router } from '../../src/router/router';
|
|
5
|
+
import { Context } from '../../src/core/context';
|
|
6
|
+
|
|
7
|
+
describe('PerformanceHarness', () => {
|
|
8
|
+
test('should produce benchmark metrics', async () => {
|
|
9
|
+
let total = 0;
|
|
10
|
+
const result = await PerformanceHarness.benchmark('increment', 50, () => {
|
|
11
|
+
total += 1;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(result.name).toBe('increment');
|
|
15
|
+
expect(result.iterations).toBe(50);
|
|
16
|
+
expect(result.opsPerSecond).toBeGreaterThan(0);
|
|
17
|
+
expect(total).toBe(50);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should benchmark router lookups', async () => {
|
|
21
|
+
const router = new Router();
|
|
22
|
+
for (let i = 0; i < 100; i++) {
|
|
23
|
+
router.get(`/static/${i}`, (ctx: Context) => ctx.createResponse({ i }));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = await PerformanceHarness.benchmark('router:static', 200, (iteration) => {
|
|
27
|
+
const index = iteration % 100;
|
|
28
|
+
const route = router.findRoute('GET', `/static/${index}`);
|
|
29
|
+
expect(route).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(result.opsPerSecond).toBeGreaterThan(0);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('StressTester', () => {
|
|
37
|
+
test('should execute tasks concurrently', async () => {
|
|
38
|
+
let executed = 0;
|
|
39
|
+
const result = await StressTester.run('stress:noop', 40, 5, async () => {
|
|
40
|
+
executed += 1;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result.iterations).toBe(40);
|
|
44
|
+
expect(result.errors).toBe(0);
|
|
45
|
+
expect(executed).toBe(40);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { Router } from '../../src/router/router';
|
|
3
|
+
import { Context } from '../../src/core/context';
|
|
4
|
+
import { MiddlewarePipeline } from '../../src/middleware/pipeline';
|
|
5
|
+
import { Container } from '../../src/di/container';
|
|
6
|
+
import { Injectable, Inject } from '../../src/di/decorators';
|
|
7
|
+
import { PerformanceHarness } from '../../src/testing/harness';
|
|
8
|
+
import 'reflect-metadata';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 性能优化验证测试
|
|
12
|
+
* 验证路由匹配缓存、中间件管道优化、DI 容器优化的效果
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
describe('Performance Optimization Tests', () => {
|
|
16
|
+
test('route matching cache should improve performance', async () => {
|
|
17
|
+
// 创建两个独立的 router 实例,确保测试准确性
|
|
18
|
+
const createRouter = () => {
|
|
19
|
+
const r = new Router();
|
|
20
|
+
// 注册多个路由
|
|
21
|
+
for (let i = 0; i < 100; i++) {
|
|
22
|
+
r.get(`/api/users/${i}`, (ctx: Context) => ctx.createResponse({ id: i }));
|
|
23
|
+
}
|
|
24
|
+
// 注册动态路由(这个路由会在最后匹配,因为前面有100个静态路由)
|
|
25
|
+
r.get('/api/users/:id', (ctx: Context) => ctx.createResponse({ id: ctx.getParam('id') }));
|
|
26
|
+
return r;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// 第一次匹配(无缓存)- 每次迭代都创建新的 router,确保没有缓存
|
|
30
|
+
const result1 = await PerformanceHarness.benchmark(
|
|
31
|
+
'route match (first, no cache)',
|
|
32
|
+
10000,
|
|
33
|
+
async () => {
|
|
34
|
+
const r = createRouter();
|
|
35
|
+
return r.findRoute('GET', '/api/users/123');
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// 第二次匹配(有缓存)- 使用同一个 router 实例,缓存已建立
|
|
40
|
+
const router2 = createRouter();
|
|
41
|
+
// 预热缓存
|
|
42
|
+
router2.findRoute('GET', '/api/users/123');
|
|
43
|
+
|
|
44
|
+
const result2 = await PerformanceHarness.benchmark(
|
|
45
|
+
'route match (cached)',
|
|
46
|
+
10000,
|
|
47
|
+
async () => {
|
|
48
|
+
return router2.findRoute('GET', '/api/users/123');
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// 缓存后的性能应该更好或至少相当(允许一定的性能波动)
|
|
53
|
+
// 使用更宽松的断言:缓存版本不应该明显更慢(允许15%的性能波动)
|
|
54
|
+
// 因为性能测试本身存在波动性,特别是在高频操作时
|
|
55
|
+
const performanceRatio = result2.durationMs / result1.durationMs;
|
|
56
|
+
expect(performanceRatio).toBeLessThanOrEqual(1.15); // 缓存版本不应该比无缓存版本慢超过15%
|
|
57
|
+
|
|
58
|
+
// 验证缓存确实被使用:缓存版本的性能应该至少相当
|
|
59
|
+
expect(result2.opsPerSecond).toBeGreaterThan(result1.opsPerSecond * 0.85);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('middleware pipeline optimization should reduce memory allocation', async () => {
|
|
63
|
+
const pipeline = new MiddlewarePipeline();
|
|
64
|
+
|
|
65
|
+
// 添加多个中间件
|
|
66
|
+
for (let i = 0; i < 50; i++) {
|
|
67
|
+
pipeline.use(async (ctx, next) => {
|
|
68
|
+
return await next();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const context = new Context(new Request('http://localhost:3000/test'));
|
|
73
|
+
|
|
74
|
+
const result = await PerformanceHarness.benchmark(
|
|
75
|
+
'middleware pipeline',
|
|
76
|
+
1000,
|
|
77
|
+
async () => {
|
|
78
|
+
return await pipeline.run(context, async () => {
|
|
79
|
+
return new Response('ok');
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// 优化后的中间件管道应该快速执行
|
|
85
|
+
expect(result.durationMs).toBeLessThan(1000); // 1000次操作应该在1秒内完成
|
|
86
|
+
expect(result.opsPerSecond).toBeGreaterThan(1000);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('DI container dependency plan cache should improve resolution', async () => {
|
|
90
|
+
@Injectable()
|
|
91
|
+
class Level1 {
|
|
92
|
+
public name = 'level1';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Injectable()
|
|
96
|
+
class Level2 {
|
|
97
|
+
public constructor(@Inject(Level1) public level1: Level1) {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@Injectable()
|
|
101
|
+
class Level3 {
|
|
102
|
+
public constructor(@Inject(Level2) public level2: Level2) {}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const container = new Container();
|
|
106
|
+
container.register(Level1);
|
|
107
|
+
container.register(Level2);
|
|
108
|
+
container.register(Level3);
|
|
109
|
+
|
|
110
|
+
// 第一次解析(构建依赖计划)
|
|
111
|
+
const result1 = await PerformanceHarness.benchmark(
|
|
112
|
+
'DI resolve (first, build plan)',
|
|
113
|
+
1000,
|
|
114
|
+
() => {
|
|
115
|
+
return container.resolve(Level3);
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// 第二次解析(使用缓存的依赖计划)
|
|
120
|
+
const result2 = await PerformanceHarness.benchmark(
|
|
121
|
+
'DI resolve (cached plan)',
|
|
122
|
+
1000,
|
|
123
|
+
() => {
|
|
124
|
+
return container.resolve(Level3);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// 使用缓存计划后的性能应该更好(虽然单例也会影响)
|
|
129
|
+
// 主要验证不会退化
|
|
130
|
+
expect(result2.durationMs).toBeLessThanOrEqual(result1.durationMs * 1.5);
|
|
131
|
+
expect(result2.opsPerSecond).toBeGreaterThan(5000);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('router should handle many routes efficiently', async () => {
|
|
135
|
+
const router = new Router();
|
|
136
|
+
|
|
137
|
+
// 注册大量路由
|
|
138
|
+
for (let i = 0; i < 1000; i++) {
|
|
139
|
+
router.get(`/api/items/${i}`, (ctx: Context) => ctx.createResponse({ id: i }));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const context = new Context(new Request('http://localhost:3000/api/items/500'));
|
|
143
|
+
|
|
144
|
+
const result = await PerformanceHarness.benchmark(
|
|
145
|
+
'router with many routes',
|
|
146
|
+
100,
|
|
147
|
+
async () => {
|
|
148
|
+
return router.findRoute('GET', '/api/items/500');
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// 即使有大量路由,匹配应该快速完成
|
|
153
|
+
expect(result.durationMs).toBeLessThan(500); // 100次操作应该在500ms内完成
|
|
154
|
+
expect(result.opsPerSecond).toBeGreaterThan(200);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('middleware pipeline should handle many middlewares efficiently', async () => {
|
|
158
|
+
const pipeline = new MiddlewarePipeline();
|
|
159
|
+
|
|
160
|
+
// 添加大量中间件
|
|
161
|
+
for (let i = 0; i < 200; i++) {
|
|
162
|
+
pipeline.use(async (ctx, next) => {
|
|
163
|
+
return await next();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const context = new Context(new Request('http://localhost:3000/test'));
|
|
168
|
+
|
|
169
|
+
const result = await PerformanceHarness.benchmark(
|
|
170
|
+
'middleware pipeline (many middlewares)',
|
|
171
|
+
100,
|
|
172
|
+
async () => {
|
|
173
|
+
return await pipeline.run(context, async () => {
|
|
174
|
+
return new Response('ok');
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// 即使有大量中间件,执行应该快速完成
|
|
180
|
+
expect(result.durationMs).toBeLessThan(2000); // 100次操作应该在2秒内完成
|
|
181
|
+
expect(result.opsPerSecond).toBeGreaterThan(50);
|
|
182
|
+
});
|
|
183
|
+
});
|