@dangao/bun-server 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/readme.md +163 -2
- package/src/auth/controller.ts +148 -0
- package/src/auth/decorators.ts +81 -0
- package/src/auth/index.ts +12 -0
- package/src/auth/jwt.ts +169 -0
- package/src/auth/oauth2.ts +244 -0
- package/src/auth/types.ts +248 -0
- package/src/cache/cache-module.ts +67 -0
- package/src/cache/decorators.ts +202 -0
- package/src/cache/index.ts +27 -0
- package/src/cache/service.ts +151 -0
- package/src/cache/types.ts +420 -0
- package/src/config/config-module.ts +76 -0
- package/src/config/index.ts +8 -0
- package/src/config/service.ts +93 -0
- package/src/config/types.ts +27 -0
- package/src/controller/controller.ts +251 -0
- package/src/controller/decorators.ts +84 -0
- package/src/controller/index.ts +7 -0
- package/src/controller/metadata.ts +27 -0
- package/src/controller/param-binder.ts +157 -0
- package/src/core/application.ts +233 -0
- package/src/core/context.ts +228 -0
- package/src/core/index.ts +4 -0
- package/src/core/server.ts +128 -0
- package/src/core/types.ts +2 -0
- package/src/database/connection-manager.ts +239 -0
- package/src/database/connection-pool.ts +322 -0
- package/src/database/database-extension.ts +62 -0
- package/src/database/database-module.ts +115 -0
- package/src/database/health-indicator.ts +51 -0
- package/src/database/index.ts +47 -0
- package/src/database/orm/decorators.ts +155 -0
- package/src/database/orm/drizzle-repository.ts +39 -0
- package/src/database/orm/index.ts +23 -0
- package/src/database/orm/repository-decorator.ts +39 -0
- package/src/database/orm/repository.ts +103 -0
- package/src/database/orm/service.ts +49 -0
- package/src/database/orm/transaction-decorator.ts +45 -0
- package/src/database/orm/transaction-interceptor.ts +243 -0
- package/src/database/orm/transaction-manager.ts +276 -0
- package/src/database/orm/transaction-types.ts +140 -0
- package/src/database/orm/types.ts +99 -0
- package/src/database/service.ts +221 -0
- package/src/database/types.ts +171 -0
- package/src/di/container.ts +398 -0
- package/src/di/decorators.ts +228 -0
- package/src/di/index.ts +4 -0
- package/src/di/module-registry.ts +188 -0
- package/src/di/module.ts +65 -0
- package/src/di/types.ts +67 -0
- package/src/error/error-codes.ts +222 -0
- package/src/error/filter.ts +43 -0
- package/src/error/handler.ts +66 -0
- package/src/error/http-exception.ts +115 -0
- package/src/error/i18n.ts +217 -0
- package/src/error/index.ts +16 -0
- package/src/extensions/index.ts +5 -0
- package/src/extensions/logger-extension.ts +31 -0
- package/src/extensions/logger-module.ts +69 -0
- package/src/extensions/types.ts +14 -0
- package/src/files/index.ts +5 -0
- package/src/files/static-middleware.ts +53 -0
- package/src/files/storage.ts +67 -0
- package/src/files/types.ts +33 -0
- package/src/files/upload-middleware.ts +45 -0
- package/src/health/controller.ts +76 -0
- package/src/health/health-module.ts +51 -0
- package/src/health/index.ts +12 -0
- package/src/health/types.ts +28 -0
- package/src/index.ts +270 -0
- package/src/metrics/collector.ts +209 -0
- package/src/metrics/controller.ts +40 -0
- package/src/metrics/index.ts +15 -0
- package/src/metrics/metrics-module.ts +58 -0
- package/src/metrics/middleware.ts +46 -0
- package/src/metrics/prometheus.ts +79 -0
- package/src/metrics/types.ts +103 -0
- package/src/middleware/builtin/cors.ts +60 -0
- package/src/middleware/builtin/error-handler.ts +90 -0
- package/src/middleware/builtin/file-upload.ts +42 -0
- package/src/middleware/builtin/index.ts +14 -0
- package/src/middleware/builtin/logger.ts +91 -0
- package/src/middleware/builtin/rate-limit.ts +252 -0
- package/src/middleware/builtin/static-file.ts +88 -0
- package/src/middleware/decorators.ts +91 -0
- package/src/middleware/index.ts +11 -0
- package/src/middleware/middleware.ts +13 -0
- package/src/middleware/pipeline.ts +93 -0
- package/src/queue/decorators.ts +110 -0
- package/src/queue/index.ts +26 -0
- package/src/queue/queue-module.ts +64 -0
- package/src/queue/service.ts +302 -0
- package/src/queue/types.ts +341 -0
- package/src/request/body-parser.ts +133 -0
- package/src/request/file-handler.ts +46 -0
- package/src/request/index.ts +5 -0
- package/src/request/request.ts +107 -0
- package/src/request/response.ts +150 -0
- package/src/router/decorators.ts +122 -0
- package/src/router/index.ts +6 -0
- package/src/router/registry.ts +98 -0
- package/src/router/route.ts +140 -0
- package/src/router/router.ts +241 -0
- package/src/router/types.ts +27 -0
- package/src/security/access-decision-manager.ts +34 -0
- package/src/security/authentication-manager.ts +47 -0
- package/src/security/context.ts +92 -0
- package/src/security/filter.ts +162 -0
- package/src/security/index.ts +8 -0
- package/src/security/providers/index.ts +3 -0
- package/src/security/providers/jwt-provider.ts +60 -0
- package/src/security/providers/oauth2-provider.ts +70 -0
- package/src/security/security-module.ts +145 -0
- package/src/security/types.ts +165 -0
- package/src/session/decorators.ts +45 -0
- package/src/session/index.ts +19 -0
- package/src/session/middleware.ts +143 -0
- package/src/session/service.ts +218 -0
- package/src/session/session-module.ts +69 -0
- package/src/session/types.ts +373 -0
- package/src/swagger/decorators.ts +133 -0
- package/src/swagger/generator.ts +234 -0
- package/src/swagger/index.ts +7 -0
- package/src/swagger/swagger-extension.ts +41 -0
- package/src/swagger/swagger-module.ts +83 -0
- package/src/swagger/types.ts +188 -0
- package/src/swagger/ui.ts +98 -0
- package/src/testing/harness.ts +96 -0
- package/src/validation/decorators.ts +95 -0
- package/src/validation/errors.ts +28 -0
- package/src/validation/index.ts +14 -0
- package/src/validation/types.ts +35 -0
- package/src/validation/validator.ts +63 -0
- package/src/websocket/decorators.ts +51 -0
- package/src/websocket/index.ts +12 -0
- package/src/websocket/registry.ts +133 -0
- package/tests/cache/cache-module.test.ts +212 -0
- package/tests/config/config-module.test.ts +151 -0
- package/tests/controller/controller.test.ts +189 -0
- package/tests/core/application.test.ts +57 -0
- package/tests/core/context-body.test.ts +44 -0
- package/tests/core/context.test.ts +86 -0
- package/tests/core/edge-cases.test.ts +432 -0
- package/tests/database/database-module.test.ts +385 -0
- package/tests/database/orm.test.ts +164 -0
- package/tests/database/postgres-mysql-integration.test.ts +395 -0
- package/tests/database/transaction.test.ts +238 -0
- package/tests/di/container.test.ts +264 -0
- package/tests/di/module.test.ts +128 -0
- package/tests/error/error-codes.test.ts +121 -0
- package/tests/error/error-handler.test.ts +68 -0
- package/tests/error/error-handling.test.ts +254 -0
- package/tests/error/http-exception.test.ts +37 -0
- package/tests/error/i18n-integration.test.ts +175 -0
- package/tests/extensions/logger-extension.test.ts +40 -0
- package/tests/files/static-middleware.test.ts +67 -0
- package/tests/files/upload-middleware.test.ts +43 -0
- package/tests/health/health-module.test.ts +116 -0
- package/tests/integration/application-router.test.ts +85 -0
- package/tests/integration/body-parsing.test.ts +88 -0
- package/tests/integration/cache-e2e.test.ts +114 -0
- package/tests/integration/oauth2-e2e.test.ts +615 -0
- package/tests/integration/session-e2e.test.ts +207 -0
- package/tests/metrics/metrics-module.test.ts +178 -0
- package/tests/middleware/builtin.test.ts +206 -0
- package/tests/middleware/file-upload.test.ts +41 -0
- package/tests/middleware/middleware.test.ts +120 -0
- package/tests/middleware/pipeline.test.ts +72 -0
- package/tests/middleware/rate-limit.test.ts +314 -0
- package/tests/middleware/static-file.test.ts +62 -0
- package/tests/perf/harness.test.ts +48 -0
- package/tests/perf/optimization.test.ts +183 -0
- package/tests/perf/regression.test.ts +120 -0
- package/tests/queue/queue-module.test.ts +217 -0
- package/tests/request/body-parser.test.ts +96 -0
- package/tests/request/response.test.ts +99 -0
- package/tests/router/decorators.test.ts +48 -0
- package/tests/router/registry.test.ts +51 -0
- package/tests/router/route.test.ts +71 -0
- package/tests/router/router-normalization.test.ts +106 -0
- package/tests/router/router.test.ts +133 -0
- package/tests/security/access-decision-manager.test.ts +84 -0
- package/tests/security/authentication-manager.test.ts +81 -0
- package/tests/security/context.test.ts +302 -0
- package/tests/security/filter.test.ts +225 -0
- package/tests/security/jwt-provider.test.ts +106 -0
- package/tests/security/oauth2-provider.test.ts +269 -0
- package/tests/security/security-module.test.ts +143 -0
- package/tests/session/session-module.test.ts +307 -0
- package/tests/stress/di-stress.test.ts +30 -0
- package/tests/swagger/decorators.test.ts +153 -0
- package/tests/swagger/generator.test.ts +202 -0
- package/tests/swagger/swagger-extension.test.ts +72 -0
- package/tests/swagger/swagger-module.test.ts +79 -0
- package/tests/utils/test-port.ts +10 -0
- package/tests/validation/controller-validation.test.ts +64 -0
- package/tests/validation/validation.test.ts +42 -0
- package/tests/websocket/gateway.test.ts +68 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
3
|
+
|
|
4
|
+
import { Application } from '../../src/core/application';
|
|
5
|
+
import { Controller, ControllerRegistry } from '../../src/controller/controller';
|
|
6
|
+
import { GET, POST } from '../../src/router/decorators';
|
|
7
|
+
import { UseMiddleware } from '../../src/middleware';
|
|
8
|
+
import type { Middleware } from '../../src/middleware';
|
|
9
|
+
import { RouteRegistry } from '../../src/router/registry';
|
|
10
|
+
import { Body } from '../../src/controller/decorators';
|
|
11
|
+
import { getTestPort } from '../utils/test-port';
|
|
12
|
+
|
|
13
|
+
describe('Middleware Integration', () => {
|
|
14
|
+
let app: Application;
|
|
15
|
+
let port: number;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
port = getTestPort();
|
|
19
|
+
app = new Application({ port });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await app.stop();
|
|
24
|
+
RouteRegistry.getInstance().clear();
|
|
25
|
+
ControllerRegistry.getInstance().clear();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('should apply global middleware', async () => {
|
|
29
|
+
const headerMiddleware: Middleware = async (ctx, next) => {
|
|
30
|
+
ctx.setHeader('x-global-middleware', 'enabled');
|
|
31
|
+
return await next();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
app.use(headerMiddleware);
|
|
35
|
+
|
|
36
|
+
@Controller('/api/middleware/global')
|
|
37
|
+
class GlobalController {
|
|
38
|
+
@GET('/')
|
|
39
|
+
public getData() {
|
|
40
|
+
return { ok: true };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
app.registerController(GlobalController);
|
|
45
|
+
await app.listen();
|
|
46
|
+
|
|
47
|
+
const response = await fetch(`http://localhost:${port}/api/middleware/global`);
|
|
48
|
+
expect(response.headers.get('x-global-middleware')).toBe('enabled');
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
expect(data).toEqual({ ok: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should apply class and method level middleware', async () => {
|
|
54
|
+
const calls: string[] = [];
|
|
55
|
+
|
|
56
|
+
const classMiddleware: Middleware = async (ctx, next) => {
|
|
57
|
+
calls.push('class');
|
|
58
|
+
return await next();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const methodMiddleware: Middleware = async (ctx, next) => {
|
|
62
|
+
calls.push('method');
|
|
63
|
+
return await next();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
@UseMiddleware(classMiddleware)
|
|
67
|
+
@Controller('/api/middleware/controller')
|
|
68
|
+
class ControllerWithMiddleware {
|
|
69
|
+
@UseMiddleware(methodMiddleware)
|
|
70
|
+
@GET('/')
|
|
71
|
+
public getData() {
|
|
72
|
+
calls.push('handler');
|
|
73
|
+
return { value: 'ok' };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
app.registerController(ControllerWithMiddleware);
|
|
78
|
+
await app.listen();
|
|
79
|
+
|
|
80
|
+
const response = await fetch(`http://localhost:${port}/api/middleware/controller`);
|
|
81
|
+
const data = await response.json();
|
|
82
|
+
|
|
83
|
+
expect(data).toEqual({ value: 'ok' });
|
|
84
|
+
expect(calls).toEqual(['class', 'method', 'handler']);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('should allow middleware to short-circuit request', async () => {
|
|
88
|
+
const guard: Middleware = async () => {
|
|
89
|
+
return new Response(JSON.stringify({ blocked: true }), {
|
|
90
|
+
status: 403,
|
|
91
|
+
headers: { 'Content-Type': 'application/json' },
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
app.use(guard);
|
|
96
|
+
|
|
97
|
+
@Controller('/api/middleware/blocked')
|
|
98
|
+
class BlockedController {
|
|
99
|
+
@POST('/')
|
|
100
|
+
public createUser(@Body() user: { name: string }) {
|
|
101
|
+
return { id: '1', ...user };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
app.registerController(BlockedController);
|
|
106
|
+
await app.listen();
|
|
107
|
+
|
|
108
|
+
const response = await fetch(`http://localhost:${port}/api/middleware/blocked`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
body: JSON.stringify({ name: 'John' }),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(response.status).toBe(403);
|
|
115
|
+
const data = await response.json();
|
|
116
|
+
expect(data).toEqual({ blocked: true });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { MiddlewarePipeline } from '../../src/middleware/pipeline';
|
|
4
|
+
import type { Middleware } from '../../src/middleware';
|
|
5
|
+
import { Context } from '../../src/core/context';
|
|
6
|
+
|
|
7
|
+
function createContext(): Context {
|
|
8
|
+
const request = new Request('http://localhost:3000/api/test');
|
|
9
|
+
return new Context(request);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('MiddlewarePipeline', () => {
|
|
13
|
+
test('should execute middlewares in order and call final handler', async () => {
|
|
14
|
+
const pipeline = new MiddlewarePipeline();
|
|
15
|
+
const calls: string[] = [];
|
|
16
|
+
|
|
17
|
+
const first: Middleware = async (ctx, next) => {
|
|
18
|
+
calls.push('first:before');
|
|
19
|
+
const response = await next();
|
|
20
|
+
calls.push('first:after');
|
|
21
|
+
return response;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const second: Middleware = async (ctx, next) => {
|
|
25
|
+
calls.push('second:before');
|
|
26
|
+
const response = await next();
|
|
27
|
+
calls.push('second:after');
|
|
28
|
+
return response;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
pipeline.use(first);
|
|
32
|
+
pipeline.use(second);
|
|
33
|
+
|
|
34
|
+
const context = createContext();
|
|
35
|
+
const response = await pipeline.run(context, async () => {
|
|
36
|
+
calls.push('handler');
|
|
37
|
+
return context.createResponse({ ok: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
expect(data).toEqual({ ok: true });
|
|
42
|
+
expect(calls).toEqual([
|
|
43
|
+
'first:before',
|
|
44
|
+
'second:before',
|
|
45
|
+
'handler',
|
|
46
|
+
'second:after',
|
|
47
|
+
'first:after',
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should allow middleware to short circuit the pipeline', async () => {
|
|
52
|
+
const pipeline = new MiddlewarePipeline();
|
|
53
|
+
|
|
54
|
+
const blocker: Middleware = async (ctx) => {
|
|
55
|
+
ctx.setStatus(401);
|
|
56
|
+
return ctx.createResponse({ error: 'Unauthorized' });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
pipeline.use(blocker);
|
|
60
|
+
|
|
61
|
+
const context = createContext();
|
|
62
|
+
const response = await pipeline.run(context, async () => {
|
|
63
|
+
return context.createResponse({ ok: true });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(response.status).toBe(401);
|
|
67
|
+
const data = await response.json();
|
|
68
|
+
expect(data).toEqual({ error: 'Unauthorized' });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
|
|
@@ -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
|
+
|