Fix #38: Sicherheitsprüfung abarbeiten (v1.11.0) #39
+3
-2
@@ -9,6 +9,7 @@ import {
|
|||||||
anmeldungErlaubt,
|
anmeldungErlaubt,
|
||||||
aufMindestdauerWarten,
|
aufMindestdauerWarten,
|
||||||
fehlversucheZuruecksetzen,
|
fehlversucheZuruecksetzen,
|
||||||
|
sperreVermerken,
|
||||||
versuchFesthalten,
|
versuchFesthalten,
|
||||||
} from '@/lib/auth/ratelimit'
|
} from '@/lib/auth/ratelimit'
|
||||||
import { protokollieren } from '@/lib/audit'
|
import { protokollieren } from '@/lib/audit'
|
||||||
@@ -110,7 +111,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||||||
// 1. Sperre pruefen, bevor teuer gerechnet wird.
|
// 1. Sperre pruefen, bevor teuer gerechnet wird.
|
||||||
const sperre = await anmeldungErlaubt(email, ip)
|
const sperre = await anmeldungErlaubt(email, ip)
|
||||||
if (sperre.gesperrt) {
|
if (sperre.gesperrt) {
|
||||||
await versuchFesthalten(email, ip, false, 'gesperrt')
|
await sperreVermerken(email, ip)
|
||||||
throw new AnmeldeFehler('gesperrt', sperre.nochSekunden)
|
throw new AnmeldeFehler('gesperrt', sperre.nochSekunden)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +275,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||||||
// anmeldet - erraten laesst sich ein Passkey ohnehin nicht.
|
// anmeldet - erraten laesst sich ein Passkey ohnehin nicht.
|
||||||
const sperre = await anmeldungErlaubt(benutzer.email, ip)
|
const sperre = await anmeldungErlaubt(benutzer.email, ip)
|
||||||
if (sperre.gesperrt) {
|
if (sperre.gesperrt) {
|
||||||
await versuchFesthalten(benutzer.email, ip, false, 'gesperrt')
|
await sperreVermerken(benutzer.email, ip)
|
||||||
throw new AnmeldeFehler('gesperrt', sperre.nochSekunden)
|
throw new AnmeldeFehler('gesperrt', sperre.nochSekunden)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+82
-14
@@ -14,8 +14,32 @@ import { ipHashen } from '@/lib/crypto/hash'
|
|||||||
* Gezaehlt wird nach E-Mail UND nach Anschluss (IP-Hash). Damit hilft es
|
* Gezaehlt wird nach E-Mail UND nach Anschluss (IP-Hash). Damit hilft es
|
||||||
* weder, dieselbe Adresse von vielen Anschluessen zu probieren, noch viele
|
* weder, dieselbe Adresse von vielen Anschluessen zu probieren, noch viele
|
||||||
* Adressen von einem Anschluss.
|
* Adressen von einem Anschluss.
|
||||||
|
*
|
||||||
|
* ## Warum abgewiesene Versuche nicht mitzaehlen
|
||||||
|
*
|
||||||
|
* Frueher wurde jeder Aufruf festgehalten, auch der, der wegen einer
|
||||||
|
* laufenden Sperre gar nicht geprueft wurde. Zusammen mit einer Frist, die
|
||||||
|
* vom juengsten Fehlversuch aus gerechnet wurde, ergab das eine
|
||||||
|
* Rueckkopplung: Jeder abgewiesene Aufruf schob die Frist um die volle
|
||||||
|
* Sperrdauer nach hinten. Wer alle zehn Minuten ein falsches Passwort an
|
||||||
|
* eine fremde Adresse schickte, sperrte deren Inhaber dauerhaft aus - ohne
|
||||||
|
* das Passwort zu kennen. Und jeder dieser Aufrufe hinterliess eine Zeile,
|
||||||
|
* die erst der Wartungslauf nach 30 Tagen wieder los wurde.
|
||||||
|
*
|
||||||
|
* Deshalb jetzt zwei getrennte Dinge:
|
||||||
|
*
|
||||||
|
* - Ein *geprueft* fehlgeschlagener Versuch (falsches Passwort, falscher
|
||||||
|
* Code, unbekannte Adresse) zaehlt und wird einzeln festgehalten.
|
||||||
|
* - Ein *abgewiesener* Aufruf waehrend der Sperre zaehlt nicht und wird
|
||||||
|
* hoechstens einmal je Sperre vermerkt.
|
||||||
|
*
|
||||||
|
* Damit steht die Frist fest, sobald sie einmal gesetzt ist, und die
|
||||||
|
* Tabelle waechst nicht mit der Zahl der Angriffsversuche.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** Grund fuer eine Zeile, die nur den abgewiesenen Aufruf dokumentiert. */
|
||||||
|
const GRUND_ABGEWIESEN = 'gesperrt'
|
||||||
|
|
||||||
export type SperrErgebnis =
|
export type SperrErgebnis =
|
||||||
| { gesperrt: false; verbleibendeVersuche: number }
|
| { gesperrt: false; verbleibendeVersuche: number }
|
||||||
| { gesperrt: true; nochSekunden: number }
|
| { gesperrt: true; nochSekunden: number }
|
||||||
@@ -29,33 +53,49 @@ function fensterStart(): Date {
|
|||||||
* Passwortvergleich aufgerufen - ein gesperrter Zugang soll gar nicht erst
|
* Passwortvergleich aufgerufen - ein gesperrter Zugang soll gar nicht erst
|
||||||
* Rechenzeit fuer Argon2 kosten.
|
* Rechenzeit fuer Argon2 kosten.
|
||||||
*/
|
*/
|
||||||
export async function anmeldungErlaubt(
|
export async function anmeldungErlaubt(email: string, ip: string | null): Promise<SperrErgebnis> {
|
||||||
email: string,
|
const jetzt = Date.now()
|
||||||
ip: string | null,
|
|
||||||
): Promise<SperrErgebnis> {
|
|
||||||
const seit = fensterStart()
|
|
||||||
const kennung = email.trim().toLowerCase()
|
const kennung = email.trim().toLowerCase()
|
||||||
const ipH = ipHashen(ip)
|
const ipH = ipHashen(ip)
|
||||||
|
|
||||||
const fehlversuche = await db.loginVersuch.findMany({
|
const fehlversuche = await db.loginVersuch.findMany({
|
||||||
where: {
|
where: {
|
||||||
erfolg: false,
|
erfolg: false,
|
||||||
zeitpunkt: { gte: seit },
|
// Abgewiesene Aufrufe zaehlen nicht mit - sonst haelt sich die Sperre
|
||||||
|
// selbst am Leben, siehe Kopf dieser Datei.
|
||||||
|
grund: { not: GRUND_ABGEWIESEN },
|
||||||
|
zeitpunkt: { gte: new Date(jetzt - env.loginFensterSekunden * 1000) },
|
||||||
OR: [{ email: kennung }, ...(ipH ? [{ ipHash: ipH }] : [])],
|
OR: [{ email: kennung }, ...(ipH ? [{ ipHash: ipH }] : [])],
|
||||||
},
|
},
|
||||||
orderBy: { zeitpunkt: 'desc' },
|
orderBy: { zeitpunkt: 'asc' },
|
||||||
select: { zeitpunkt: true },
|
select: { zeitpunkt: true },
|
||||||
take: env.loginMaxVersuche + 1,
|
// Deckel gegen eine ausufernde Abfrage. Mehr als eine Handvoll Runden
|
||||||
|
// passt ohnehin nicht in ein Zeitfenster, in dem jede Runde gesperrt
|
||||||
|
// wird.
|
||||||
|
take: 200,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (fehlversuche.length >= env.loginMaxVersuche) {
|
// Von alt nach neu durchgehen und die Runden nachvollziehen: Sobald das
|
||||||
const juengster = fehlversuche[0]!.zeitpunkt.getTime()
|
// Mass voll ist, beginnt an genau diesem Versuch eine Sperre, und der
|
||||||
const frei = juengster + env.loginSperreSekunden * 1000
|
// Zaehler faengt von vorn an. Ist ihre Frist inzwischen abgelaufen, sind
|
||||||
const nochSekunden = Math.max(1, Math.ceil((frei - Date.now()) / 1000))
|
// die Versuche davor abgegolten - sonst bliebe der Zugang bis zum Ende
|
||||||
return { gesperrt: true, nochSekunden }
|
// des Zeitfensters dicht, obwohl die Sperre laengst um ist.
|
||||||
|
let zaehler = 0
|
||||||
|
let sperreBis = 0
|
||||||
|
|
||||||
|
for (const versuch of fehlversuche) {
|
||||||
|
zaehler += 1
|
||||||
|
if (zaehler >= env.loginMaxVersuche) {
|
||||||
|
sperreBis = versuch.zeitpunkt.getTime() + env.loginSperreSekunden * 1000
|
||||||
|
zaehler = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { gesperrt: false, verbleibendeVersuche: env.loginMaxVersuche - fehlversuche.length }
|
if (sperreBis > jetzt) {
|
||||||
|
return { gesperrt: true, nochSekunden: Math.max(1, Math.ceil((sperreBis - jetzt) / 1000)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { gesperrt: false, verbleibendeVersuche: env.loginMaxVersuche - zaehler }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Haelt einen Versuch fest. Der Grund hilft spaeter beim Nachvollziehen. */
|
/** Haelt einen Versuch fest. Der Grund hilft spaeter beim Nachvollziehen. */
|
||||||
@@ -75,6 +115,34 @@ export async function versuchFesthalten(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vermerkt einen abgewiesenen Aufruf - aber nur den ersten je Sperre.
|
||||||
|
*
|
||||||
|
* Dass eine Sperre gegriffen hat, will man im Protokoll sehen. Wie oft
|
||||||
|
* dagegen geklopft wurde, ist die Information nicht wert: Sie kostet eine
|
||||||
|
* Zeile pro Anfrage, und genau daraus laesst sich die Tabelle vollschreiben.
|
||||||
|
* Eine Zeile je Sperre reicht, um den Vorgang zu erkennen.
|
||||||
|
*
|
||||||
|
* @returns true, wenn tatsaechlich vermerkt wurde.
|
||||||
|
*/
|
||||||
|
export async function sperreVermerken(email: string, ip: string | null): Promise<boolean> {
|
||||||
|
const kennung = email.trim().toLowerCase()
|
||||||
|
const vorhanden = await db.loginVersuch.findFirst({
|
||||||
|
where: {
|
||||||
|
email: kennung,
|
||||||
|
erfolg: false,
|
||||||
|
grund: GRUND_ABGEWIESEN,
|
||||||
|
zeitpunkt: { gte: fensterStart() },
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (vorhanden) return false
|
||||||
|
|
||||||
|
await versuchFesthalten(kennung, ip, false, GRUND_ABGEWIESEN)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nach einer erfolgreichen Anmeldung werden die Fehlversuche dieser
|
* Nach einer erfolgreichen Anmeldung werden die Fehlversuche dieser
|
||||||
* Adresse geloescht - sonst bliebe der Zaehler bis zum Fensterende stehen.
|
* Adresse geloescht - sonst bliebe der Zaehler bis zum Fensterende stehen.
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Anmeldesperre - und warum sie sich frueher selbst am Leben hielt.
|
||||||
|
*
|
||||||
|
* Bis v1.10.2 wurde jeder Aufruf festgehalten, auch der, der wegen einer
|
||||||
|
* laufenden Sperre gar nicht geprueft wurde. Gleichzeitig lief die Frist ab
|
||||||
|
* dem juengsten Fehlversuch. Beides zusammen ergab eine Rueckkopplung: Wer
|
||||||
|
* regelmaessig gegen eine gesperrte Adresse klopfte, verlaengerte die Sperre
|
||||||
|
* bei jedem Klopfen um die volle Dauer - und sperrte den Inhaber damit
|
||||||
|
* dauerhaft aus, ohne dessen Passwort zu kennen. Nebenbei wuchs die Tabelle
|
||||||
|
* mit jedem Versuch.
|
||||||
|
*
|
||||||
|
* Diese Tests laufen gegen eine Attrappe der Datenbank. Das reicht: Was hier
|
||||||
|
* geprueft wird, ist die Rechnung, nicht die Abfrage.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Kuerzere Sperre als Zeitfenster - nur so wird sichtbar, ob die Frist
|
||||||
|
// wirklich ablaeuft oder bis zum Fensterende stehen bleibt.
|
||||||
|
process.env.LOGIN_MAX_VERSUCHE = '5'
|
||||||
|
process.env.LOGIN_FENSTER_SEKUNDEN = '3600'
|
||||||
|
process.env.LOGIN_SPERRE_SEKUNDEN = '900'
|
||||||
|
|
||||||
|
type Zeile = {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
ipHash: string | null
|
||||||
|
erfolg: boolean
|
||||||
|
grund: string | null
|
||||||
|
zeitpunkt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
let tabelle: Zeile[] = []
|
||||||
|
let laufendeNummer = 0
|
||||||
|
|
||||||
|
/** Nur die Abfrageformen, die in ratelimit.ts wirklich vorkommen. */
|
||||||
|
function passt(zeile: Zeile, wo: Record<string, unknown>): boolean {
|
||||||
|
if ('erfolg' in wo && zeile.erfolg !== wo.erfolg) return false
|
||||||
|
if ('email' in wo && zeile.email !== wo.email) return false
|
||||||
|
|
||||||
|
if ('grund' in wo) {
|
||||||
|
const bedingung = wo.grund
|
||||||
|
if (typeof bedingung === 'object' && bedingung !== null && 'not' in bedingung) {
|
||||||
|
if (zeile.grund === (bedingung as { not: string }).not) return false
|
||||||
|
} else if (zeile.grund !== bedingung) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('zeitpunkt' in wo) {
|
||||||
|
const { gte, lt } = wo.zeitpunkt as { gte?: Date; lt?: Date }
|
||||||
|
if (gte && zeile.zeitpunkt < gte) return false
|
||||||
|
if (lt && zeile.zeitpunkt >= lt) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('OR' in wo) {
|
||||||
|
const zweige = wo.OR as Record<string, unknown>[]
|
||||||
|
if (!zweige.some((zweig) => passt(zeile, zweig))) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('ipHash' in wo && zeile.ipHash !== wo.ipHash) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('@/lib/db', () => ({
|
||||||
|
db: {
|
||||||
|
loginVersuch: {
|
||||||
|
findMany: async ({
|
||||||
|
where,
|
||||||
|
orderBy,
|
||||||
|
take,
|
||||||
|
}: {
|
||||||
|
where: Record<string, unknown>
|
||||||
|
orderBy?: { zeitpunkt?: 'asc' | 'desc' }
|
||||||
|
take?: number
|
||||||
|
}) => {
|
||||||
|
const treffer = tabelle.filter((zeile) => passt(zeile, where))
|
||||||
|
treffer.sort((a, b) =>
|
||||||
|
orderBy?.zeitpunkt === 'desc'
|
||||||
|
? b.zeitpunkt.getTime() - a.zeitpunkt.getTime()
|
||||||
|
: a.zeitpunkt.getTime() - b.zeitpunkt.getTime(),
|
||||||
|
)
|
||||||
|
return take ? treffer.slice(0, take) : treffer
|
||||||
|
},
|
||||||
|
|
||||||
|
findFirst: async ({ where }: { where: Record<string, unknown> }) =>
|
||||||
|
tabelle.find((zeile) => passt(zeile, where)) ?? null,
|
||||||
|
|
||||||
|
create: async ({ data }: { data: Omit<Zeile, 'id' | 'zeitpunkt'> }) => {
|
||||||
|
laufendeNummer += 1
|
||||||
|
const zeile: Zeile = {
|
||||||
|
id: `z${laufendeNummer}`,
|
||||||
|
email: data.email,
|
||||||
|
ipHash: data.ipHash ?? null,
|
||||||
|
erfolg: data.erfolg,
|
||||||
|
grund: data.grund ?? null,
|
||||||
|
zeitpunkt: new Date(),
|
||||||
|
}
|
||||||
|
tabelle.push(zeile)
|
||||||
|
return zeile
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteMany: async ({ where }: { where: Record<string, unknown> }) => {
|
||||||
|
const vorher = tabelle.length
|
||||||
|
tabelle = tabelle.filter((zeile) => !passt(zeile, where))
|
||||||
|
return { count: vorher - tabelle.length }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { anmeldungErlaubt, sperreVermerken, versuchFesthalten, fehlversucheZuruecksetzen } =
|
||||||
|
await import('@/lib/auth/ratelimit')
|
||||||
|
|
||||||
|
const OPFER = 'opfer@example.de'
|
||||||
|
const ANSCHLUSS = '198.51.100.7'
|
||||||
|
const FREMD = '203.0.113.9'
|
||||||
|
const START = new Date('2026-08-24T12:00:00Z').getTime()
|
||||||
|
|
||||||
|
/** Setzt die Uhr auf START plus so viele Sekunden. */
|
||||||
|
function beiSekunde(sekunden: number): void {
|
||||||
|
vi.setSystemTime(new Date(START + sekunden * 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fehlversuch(sekunde: number): Promise<void> {
|
||||||
|
beiSekunde(sekunde)
|
||||||
|
await versuchFesthalten(OPFER, ANSCHLUSS, false, 'passwort-falsch')
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Anmeldesperre', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
tabelle = []
|
||||||
|
laufendeNummer = 0
|
||||||
|
vi.useFakeTimers()
|
||||||
|
beiSekunde(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sperrt erst, wenn das Mass voll ist', async () => {
|
||||||
|
for (let i = 0; i < 4; i += 1) await fehlversuch(i)
|
||||||
|
|
||||||
|
beiSekunde(10)
|
||||||
|
expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(false)
|
||||||
|
|
||||||
|
await fehlversuch(11)
|
||||||
|
beiSekunde(12)
|
||||||
|
expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('laesst die Frist ablaufen, obwohl die Fehlversuche noch im Zeitfenster stehen', async () => {
|
||||||
|
for (let i = 0; i < 5; i += 1) await fehlversuch(i)
|
||||||
|
|
||||||
|
beiSekunde(890)
|
||||||
|
expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(true)
|
||||||
|
|
||||||
|
// Kurz danach offen - und mit vollem Kontingent, nicht mit null
|
||||||
|
// Restversuchen. Das Zeitfenster laeuft noch (3600 s), die fuenf
|
||||||
|
// Fehlversuche stehen also weiterhin in der Tabelle.
|
||||||
|
beiSekunde(905)
|
||||||
|
const frei = await anmeldungErlaubt(OPFER, ANSCHLUSS)
|
||||||
|
expect(frei.gesperrt).toBe(false)
|
||||||
|
if (!frei.gesperrt) expect(frei.verbleibendeVersuche).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('laesst sich durch Klopfen waehrend der Sperre nicht verlaengern', async () => {
|
||||||
|
for (let i = 0; i < 5; i += 1) await fehlversuch(i)
|
||||||
|
|
||||||
|
// Der Angreifer klopft mitten in der Sperre weiter. Genau das hat die
|
||||||
|
// Frist frueher jedes Mal um volle 900 s nach hinten geschoben.
|
||||||
|
for (const sekunde of [100, 300, 600, 880]) {
|
||||||
|
beiSekunde(sekunde)
|
||||||
|
expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(true)
|
||||||
|
await sperreVermerken(OPFER, FREMD)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Die Frist haengt am fuenften echten Fehlversuch (Sekunde 4), nicht am
|
||||||
|
// letzten Klopfen (Sekunde 880).
|
||||||
|
beiSekunde(905)
|
||||||
|
expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rechnet einen abgewiesenen Aufruf nicht als Fehlversuch', async () => {
|
||||||
|
// Vier echte Fehlversuche - das Mass ist noch nicht voll.
|
||||||
|
for (let i = 0; i < 4; i += 1) await fehlversuch(i)
|
||||||
|
|
||||||
|
// Dazu ein Vermerk ueber einen abgewiesenen Aufruf. Er steht als
|
||||||
|
// fuenfte Zeile in der Tabelle, darf aber nicht mitzaehlen: Geprueft
|
||||||
|
// wurde bei ihm nichts, also sagt er ueber die Zugangsdaten nichts aus.
|
||||||
|
beiSekunde(5)
|
||||||
|
await sperreVermerken(OPFER, FREMD)
|
||||||
|
expect(tabelle.length).toBe(5)
|
||||||
|
|
||||||
|
beiSekunde(6)
|
||||||
|
const stand = await anmeldungErlaubt(OPFER, ANSCHLUSS)
|
||||||
|
expect(stand.gesperrt).toBe(false)
|
||||||
|
if (!stand.gesperrt) expect(stand.verbleibendeVersuche).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rechnet die Frist ab dem Versuch, der das Mass vollgemacht hat', async () => {
|
||||||
|
// Vier Fehlversuche ganz frueh, der fuenfte deutlich spaeter. Die
|
||||||
|
// Sperre beginnt beim fuenften - nicht beim ersten und nicht beim
|
||||||
|
// naechsten, der noch kommt.
|
||||||
|
for (let i = 0; i < 4; i += 1) await fehlversuch(i)
|
||||||
|
await fehlversuch(600)
|
||||||
|
|
||||||
|
beiSekunde(1400)
|
||||||
|
expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(true)
|
||||||
|
|
||||||
|
beiSekunde(1510)
|
||||||
|
expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('schreibt je Sperre hoechstens eine Zeile, egal wie oft geklopft wird', async () => {
|
||||||
|
for (let i = 0; i < 5; i += 1) await fehlversuch(i)
|
||||||
|
const nachSperre = tabelle.length
|
||||||
|
|
||||||
|
for (let i = 0; i < 200; i += 1) {
|
||||||
|
beiSekunde(10 + i)
|
||||||
|
await sperreVermerken(OPFER, FREMD)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(tabelle.length).toBe(nachSperre + 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('zaehlt die naechste Runde erst nach Ablauf der Sperre wieder hoch', async () => {
|
||||||
|
for (let i = 0; i < 5; i += 1) await fehlversuch(i)
|
||||||
|
|
||||||
|
await fehlversuch(906)
|
||||||
|
const stand = await anmeldungErlaubt(OPFER, ANSCHLUSS)
|
||||||
|
expect(stand.gesperrt).toBe(false)
|
||||||
|
if (!stand.gesperrt) expect(stand.verbleibendeVersuche).toBe(4)
|
||||||
|
|
||||||
|
// Vier weitere - dann greift die zweite Sperre.
|
||||||
|
for (let i = 1; i < 5; i += 1) await fehlversuch(906 + i)
|
||||||
|
beiSekunde(915)
|
||||||
|
expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('raeumt die Fehlversuche nach erfolgreicher Anmeldung ab', async () => {
|
||||||
|
for (let i = 0; i < 5; i += 1) await fehlversuch(i)
|
||||||
|
beiSekunde(10)
|
||||||
|
expect((await anmeldungErlaubt(OPFER, ANSCHLUSS)).gesperrt).toBe(true)
|
||||||
|
|
||||||
|
await fehlversucheZuruecksetzen(OPFER)
|
||||||
|
|
||||||
|
const frei = await anmeldungErlaubt(OPFER, ANSCHLUSS)
|
||||||
|
expect(frei.gesperrt).toBe(false)
|
||||||
|
if (!frei.gesperrt) expect(frei.verbleibendeVersuche).toBe(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user