import { FailedAttemptLimiter } from './failed-attempt-limiter'; describe('FailedAttemptLimiter', () => { let now: number; let limiter: FailedAttemptLimiter; beforeEach(() => { now = 1_000_000; limiter = new FailedAttemptLimiter({ maxFailures: 3, windowMs: 60_000, maxTrackedSources: 100, clock: () => now, }); }); it('sperrt eine Quelle erst, wenn die Obergrenze erreicht ist', () => { expect(limiter.recordFailure('203.0.113.7')).toBe(false); expect(limiter.recordFailure('203.0.113.7')).toBe(false); expect(limiter.isBlocked('203.0.113.7')).toBe(false); // Der Fehlversuch, mit dem die Grenze erreicht wird, meldet die Sperre. expect(limiter.recordFailure('203.0.113.7')).toBe(true); expect(limiter.isBlocked('203.0.113.7')).toBe(true); }); it('meldet die Sperre nur einmal, nicht bei jedem weiteren Versuch', () => { limiter.recordFailure('203.0.113.7'); limiter.recordFailure('203.0.113.7'); expect(limiter.recordFailure('203.0.113.7')).toBe(true); expect(limiter.recordFailure('203.0.113.7')).toBe(false); }); it('trennt Quellen voneinander', () => { for (let i = 0; i < 3; i++) limiter.recordFailure('203.0.113.7'); expect(limiter.isBlocked('203.0.113.7')).toBe(true); expect(limiter.isBlocked('198.51.100.1')).toBe(false); }); it('gibt die Quelle nach Ablauf des Fensters wieder frei', () => { for (let i = 0; i < 3; i++) limiter.recordFailure('203.0.113.7'); now += 60_001; expect(limiter.isBlocked('203.0.113.7')).toBe(false); // Neues Fenster: der nächste Fehlversuch zählt wieder ab eins. expect(limiter.recordFailure('203.0.113.7')).toBe(false); expect(limiter.isBlocked('203.0.113.7')).toBe(false); }); it('vergisst alte Quellen, statt unbegrenzt Speicher zu belegen', () => { const small = new FailedAttemptLimiter({ maxFailures: 1, windowMs: 60_000, maxTrackedSources: 2, clock: () => now, }); small.recordFailure('a'); small.recordFailure('b'); now += 60_001; // Beim dritten Eintrag wird aufgeräumt: a und b sind abgelaufen. small.recordFailure('c'); expect(small.trackedSources()).toBe(1); expect(small.isBlocked('c')).toBe(true); }); it('verdrängt bei voller Liste den ältesten Eintrag, auch wenn er noch läuft', () => { const small = new FailedAttemptLimiter({ maxFailures: 5, windowMs: 60_000, maxTrackedSources: 2, clock: () => now, }); small.recordFailure('a'); small.recordFailure('b'); small.recordFailure('c'); expect(small.trackedSources()).toBe(2); }); });