Files
lernbar/src/lib/auth/drossel.test.ts
T
Florian WildeandClaude Opus 5.5 31d7779f45 Bremse gegen Passwortraten
Nach fuenf Fehlversuchen wird eine E-Mail-Adresse gesperrt, erst eine
Minute, dann doppelt so lang, hoechstens 15 Minuten. Gezaehlt wird je
Adresse, auch fuer unbekannte - sonst verriete die Sperre, welche Konten
es gibt. Waehrend der Sperre kommt auch das richtige Passwort nicht durch.

Jeder Versuch zaehlt sofort und wird bei Erfolg zurueckgenommen. Zwischen
Sperrpruefung und Zaehlen liegt damit kein await; gleichzeitige Anfragen
kommen so nicht alle an der Sperre vorbei.

Der Zaehler liegt im Speicher des einen Prozesses. Ein Neustart setzt ihn
zurueck - der Notausgang, falls sich das einzige Ausbilderkonto aussperrt.
Beim Aufraeumen wird eine laufende Sperre nie verdraengt.

Die Containerabnahme prueft die Sperre im laufenden Abbild. Das
Abnahmeprotokoll ist gegen diesen Stand neu erzeugt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:25:04 +02:00

114 lines
3.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
Drossel,
FREIE_VERSUCHE,
GRUNDSPERRE_MS,
HOECHSTSPERRE_MS,
VERGESSEN_NACH_MS,
sperreFuer,
wartezeitText,
} from './drossel';
/** Eine Uhr, die nur weiterläuft, wenn man sie stellt. */
function stehendeUhr(start = 1_000_000) {
let zeit = start;
return {
uhr: () => zeit,
vor: (ms: number) => {
zeit += ms;
},
};
}
function scheitern(drossel: Drossel, schluessel: string, mal: number) {
for (let i = 0; i < mal; i += 1) drossel.fehlversuch(schluessel);
}
describe('sperreFuer', () => {
it('lässt die ersten Fehlversuche frei', () => {
for (let n = 1; n < FREIE_VERSUCHE; n += 1) expect(sperreFuer(n)).toBe(0);
});
it('sperrt ab dem fünften Fehlversuch und verdoppelt dann', () => {
expect(sperreFuer(FREIE_VERSUCHE)).toBe(GRUNDSPERRE_MS);
expect(sperreFuer(FREIE_VERSUCHE + 1)).toBe(2 * GRUNDSPERRE_MS);
expect(sperreFuer(FREIE_VERSUCHE + 2)).toBe(4 * GRUNDSPERRE_MS);
});
it('bleibt bei der Obergrenze stehen', () => {
expect(sperreFuer(FREIE_VERSUCHE + 20)).toBe(HOECHSTSPERRE_MS);
});
});
describe('Drossel', () => {
it('sperrt nach fünf Fehlversuchen, nicht vorher', () => {
const { uhr } = stehendeUhr();
const drossel = new Drossel(uhr);
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE - 1);
expect(drossel.wartezeit('a@x.de')).toBe(0);
drossel.fehlversuch('a@x.de');
expect(drossel.wartezeit('a@x.de')).toBe(GRUNDSPERRE_MS);
});
it('gibt nach Ablauf der Sperre wieder frei', () => {
const { uhr, vor } = stehendeUhr();
const drossel = new Drossel(uhr);
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE);
vor(GRUNDSPERRE_MS);
expect(drossel.wartezeit('a@x.de')).toBe(0);
});
it('verlängert beim nächsten Fehlversuch nach der Sperre', () => {
const { uhr, vor } = stehendeUhr();
const drossel = new Drossel(uhr);
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE);
vor(GRUNDSPERRE_MS);
drossel.fehlversuch('a@x.de');
expect(drossel.wartezeit('a@x.de')).toBe(2 * GRUNDSPERRE_MS);
});
it('setzt nach erfolgreicher Anmeldung zurück', () => {
const { uhr } = stehendeUhr();
const drossel = new Drossel(uhr);
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE - 1);
drossel.erfolg('a@x.de');
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE - 1);
expect(drossel.wartezeit('a@x.de')).toBe(0);
});
it('vergisst Fehlversuche nach einer Stunde Ruhe', () => {
const { uhr, vor } = stehendeUhr();
const drossel = new Drossel(uhr);
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE - 1);
vor(VERGESSEN_NACH_MS);
drossel.fehlversuch('a@x.de');
expect(drossel.wartezeit('a@x.de')).toBe(0);
});
it('zählt jede Adresse für sich', () => {
const { uhr } = stehendeUhr();
const drossel = new Drossel(uhr);
scheitern(drossel, 'a@x.de', FREIE_VERSUCHE);
expect(drossel.wartezeit('a@x.de')).toBeGreaterThan(0);
expect(drossel.wartezeit('b@x.de')).toBe(0);
});
it('räumt auf, lässt eine laufende Sperre aber nicht verdrängen', () => {
const { uhr } = stehendeUhr();
const drossel = new Drossel(uhr);
scheitern(drossel, 'opfer@x.de', FREIE_VERSUCHE);
for (let i = 0; i < 10_050; i += 1) drossel.fehlversuch(`erfunden-${i}@x.de`);
expect(drossel.groesse).toBeLessThanOrEqual(10_001);
expect(drossel.wartezeit('opfer@x.de')).toBe(GRUNDSPERRE_MS);
});
});
describe('wartezeitText', () => {
it('rundet auf ganze Minuten auf', () => {
expect(wartezeitText(1)).toBe('in einer Minute');
expect(wartezeitText(60_000)).toBe('in einer Minute');
expect(wartezeitText(60_001)).toBe('in 2 Minuten');
expect(wartezeitText(HOECHSTSPERRE_MS)).toBe('in 15 Minuten');
});
});