import { describe, it, expect, beforeEach, afterEach, vi, } from 'vitest' import {createRateLimiter} from '../rate-limit.js' describe('createRateLimiter', () => { beforeEach(() => { vi.useFakeTimers() vi.setSystemTime(0) // Temps fixe et prévisible }) afterEach(() => { vi.useRealTimers() }) it('autorise les requêtes dans la limite', () => { const check = createRateLimiter({windowMs: 60_000, max: 3}) expect(check('ip1').success).toBe(true) expect(check('ip1').success).toBe(true) expect(check('ip1').success).toBe(true) }) it('bloque quand la limite est atteinte', () => { const check = createRateLimiter({windowMs: 60_000, max: 2}) check('ip1') check('ip1') const result = check('ip1') expect(result.success).toBe(false) }) it('renvoie retryAfter en secondes quand bloqué', () => { const check = createRateLimiter({windowMs: 60_000, max: 1}) check('ip1') // start = 0 const result = check('ip1') // now = 0, retryAfter = ceil((0 + 60000 - 0) / 1000) expect(result.success).toBe(false) expect(result.retryAfter).toBe(60) }) it('retryAfter décroît avec le temps écoulé', () => { const check = createRateLimiter({windowMs: 60_000, max: 1}) check('ip1') // start = 0 vi.advanceTimersByTime(30_000) // 30s plus tard const result = check('ip1') // retryAfter = ceil((0 + 60000 - 30000) / 1000) = 30 expect(result.success).toBe(false) expect(result.retryAfter).toBe(30) }) it('réinitialise le compteur après expiration de la fenêtre', () => { const check = createRateLimiter({windowMs: 60_000, max: 1}) check('ip1') expect(check('ip1').success).toBe(false) vi.advanceTimersByTime(61_000) // Fenêtre expirée expect(check('ip1').success).toBe(true) }) it('suit les clés indépendamment', () => { const check = createRateLimiter({windowMs: 60_000, max: 1}) check('ip1') expect(check('ip1').success).toBe(false) expect(check('ip2').success).toBe(true) // ip2 non affectée }) it('réinitialise proprement à la requête suivante après expiration', () => { const check = createRateLimiter({windowMs: 60_000, max: 2}) check('ip1') check('ip1') expect(check('ip1').success).toBe(false) vi.advanceTimersByTime(61_000) // Nouvelle fenêtre : 2 requêtes autorisées à nouveau expect(check('ip1').success).toBe(true) expect(check('ip1').success).toBe(true) expect(check('ip1').success).toBe(false) }) it('purge les entrées expirées lors du cleanup', () => { const check = createRateLimiter({windowMs: 60_000, max: 5}) // Plusieurs IPs remplissent le store check('ip1') check('ip2') check('ip3') // Avancer d'une fenêtre complète pour déclencher le cleanup vi.advanceTimersByTime(61_000) // ip1 repart de zéro (entrée purgée) expect(check('ip1').success).toBe(true) }) })