import { PrismaThrottlerStorage } from '../common/throttler/prisma-throttler.storage'; import { API_KEY_FAILURE_LIMIT, API_KEY_FAILURE_THROTTLER, FailedAttemptLimiter, } from './failed-attempt-limiter'; describe('FailedAttemptLimiter', () => { const increment = jest.fn(); const limiter = new FailedAttemptLimiter({ increment, } as unknown as PrismaThrottlerStorage); function record(totalHits: number, isBlocked: boolean) { return { totalHits, timeToExpire: 600, isBlocked, timeToBlockExpire: isBlocked ? 900 : 0, }; } beforeEach(() => { increment.mockReset(); increment.mockResolvedValue(record(1, false)); }); it('zählt in der gemeinsamen Zähler-Tabelle, getrennt vom Anfrage-Limit', async () => { await limiter.recordFailure('203.0.113.7'); // Ein Aufruf = eine Upsert-Anweisung mit DB-Uhr (PrismaThrottlerStorage), // unter eigenem Throttler-Namen und eigenem Schlüssel-Präfix. expect(increment).toHaveBeenCalledTimes(1); expect(increment).toHaveBeenCalledWith( `${API_KEY_FAILURE_THROTTLER}:203.0.113.7`, API_KEY_FAILURE_LIMIT.windowMs, API_KEY_FAILURE_LIMIT.maxFailures, API_KEY_FAILURE_LIMIT.windowMs, API_KEY_FAILURE_THROTTLER, ); expect(API_KEY_FAILURE_THROTTLER).toBe('api-key-failures'); }); it('behält die vereinbarten Grenzen: 20 Fehlversuche in 15 Minuten', () => { expect(API_KEY_FAILURE_LIMIT).toEqual({ maxFailures: 20, windowMs: 15 * 60 * 1000, }); }); it('sperrt nicht unterhalb der Grenze', async () => { increment.mockResolvedValueOnce( record(API_KEY_FAILURE_LIMIT.maxFailures - 1, false), ); await expect(limiter.recordFailure('203.0.113.7')).resolves.toEqual({ blocked: false, limitReached: false, }); }); it('meldet genau den Fehlversuch, mit dem die Grenze erreicht ist', async () => { increment.mockResolvedValueOnce( record(API_KEY_FAILURE_LIMIT.maxFailures, false), ); await expect(limiter.recordFailure('203.0.113.7')).resolves.toEqual({ blocked: false, limitReached: true, }); }); it('sperrt, sobald die Datenbank die Quelle als gesperrt meldet', async () => { increment.mockResolvedValueOnce( record(API_KEY_FAILURE_LIMIT.maxFailures + 1, true), ); await expect(limiter.recordFailure('203.0.113.7')).resolves.toEqual({ blocked: true, limitReached: false, }); }); it('gibt Datenbankfehler weiter, statt still durchzulassen', async () => { increment.mockRejectedValueOnce(new Error('db down')); await expect(limiter.recordFailure('203.0.113.7')).rejects.toThrow( 'db down', ); }); });