import { afterAll, describe, expect, it } from 'vitest'; import { createTestPrisma, hasTestDb, uniqueSuffix } from '../testing/test-db'; import { PrismaService } from '../prisma/prisma.service'; import { RateLimitService } from './rate-limit.service'; describe.skipIf(!hasTestDb)('RateLimitService (database)', () => { const prisma = hasTestDb ? (createTestPrisma() as unknown as PrismaService) : null!; const service = hasTestDb ? new RateLimitService(prisma) : null!; const scope = `test-${uniqueSuffix()}`; afterAll(async () => { if (!hasTestDb) return; await prisma.rateLimit.deleteMany({ where: { key: { startsWith: scope } } }); await prisma.$disconnect(); }); it('allows up to the limit and blocks beyond it', async () => { for (let i = 0; i < 3; i += 1) { expect((await service.hit(scope, 'ip:1', 3, 60)).allowed).toBe(true); } const blocked = await service.hit(scope, 'ip:1', 3, 60); expect(blocked.allowed).toBe(false); expect(blocked.retryAfterSeconds).toBeGreaterThan(0); }); it('counts concurrent hits without losing any', async () => { const results = await Promise.all( Array.from({ length: 20 }, () => service.hit(scope, 'ip:2', 10, 60)), ); expect(results.filter((r) => r.allowed)).toHaveLength(10); }); it('rolls the window over after it expires', async () => { expect((await service.hit(scope, 'ip:3', 1, 1)).allowed).toBe(true); expect((await service.hit(scope, 'ip:3', 1, 1)).allowed).toBe(false); await new Promise((resolve) => setTimeout(resolve, 1100)); expect((await service.hit(scope, 'ip:3', 1, 1)).allowed).toBe(true); }); it('sweeps counters from long-ended windows', async () => { await prisma.rateLimit.create({ data: { key: `${scope}:ip:old`, windowStart: new Date(Date.now() - 48 * 60 * 60 * 1000), count: 5, }, }); await service.cleanupExpired(); expect(await prisma.rateLimit.findUnique({ where: { key: `${scope}:ip:old` } })).toBeNull(); }); });