import { describe, it, expect, beforeEach } from 'vitest'; import { Hono } from 'hono'; import { rateLimit, ipKey, _resetRateLimitForTests, } from '../src/middleware/rate-limit.ts'; describe('rateLimit middleware', () => { beforeEach(() => { _resetRateLimitForTests(); }); function buildApp(max: number) { const app = new Hono(); app.use('/probe', rateLimit({ scope: 't', windowMs: 60_000, max, keyOf: ipKey })); app.get('/probe', (c) => c.json({ ok: true })); return app; } it('lässt Calls unter dem Limit durch und zählt Remaining runter', async () => { const app = buildApp(3); const ip = { 'X-Forwarded-For': '10.0.0.1' }; const r1 = await app.request('/probe', { headers: ip }); expect(r1.status).toBe(200); expect(r1.headers.get('X-RateLimit-Remaining')).toBe('2'); const r2 = await app.request('/probe', { headers: ip }); expect(r2.status).toBe(200); expect(r2.headers.get('X-RateLimit-Remaining')).toBe('1'); const r3 = await app.request('/probe', { headers: ip }); expect(r3.status).toBe(200); expect(r3.headers.get('X-RateLimit-Remaining')).toBe('0'); }); it('antwortet 429 mit Retry-After wenn das Limit überschritten ist', async () => { const app = buildApp(2); const ip = { 'X-Forwarded-For': '10.0.0.2' }; await app.request('/probe', { headers: ip }); await app.request('/probe', { headers: ip }); const blocked = await app.request('/probe', { headers: ip }); expect(blocked.status).toBe(429); expect(blocked.headers.get('Retry-After')).toBeTruthy(); const body = (await blocked.json()) as { error: string; retry_after_s: number }; expect(body.error).toBe('rate_limited'); expect(body.retry_after_s).toBeGreaterThan(0); }); it('isoliert Buckets pro IP', async () => { const app = buildApp(1); const ipA = { 'X-Forwarded-For': '10.0.0.3' }; const ipB = { 'X-Forwarded-For': '10.0.0.4' }; await app.request('/probe', { headers: ipA }); const aBlocked = await app.request('/probe', { headers: ipA }); const bAllowed = await app.request('/probe', { headers: ipB }); expect(aBlocked.status).toBe(429); expect(bAllowed.status).toBe(200); }); it('benutzt nur den ersten X-Forwarded-For-Hop als Key', async () => { const app = buildApp(1); // Zwei Proxy-Hops im XFF — soll auf den Client-IP-Bucket fallen, nicht // auf den Edge-IP-Bucket. const headers1 = { 'X-Forwarded-For': '203.0.113.5, 10.0.0.9' }; const headers2 = { 'X-Forwarded-For': '203.0.113.5, 10.0.0.10' }; await app.request('/probe', { headers: headers1 }); const blocked = await app.request('/probe', { headers: headers2 }); expect(blocked.status).toBe(429); }); });