import { expect, test } from '@playwright/test' import { anmelden, fehlversucheLoeschen, umgebung } from './hilfen' /** * Hinweis zu den Selektoren: Meldungen werden immer INNERHALB des * Formulars gesucht (`page.locator('form').getByRole('alert')`). Die * Fehleranzeige von Next.js traegt im Entwicklungsmodus ebenfalls * role="alert" - ein ungenauer Selektor findet zwei Elemente und der * Test schlaegt aus dem falschen Grund fehl. * * Der wichtigste Ablauf ueberhaupt: Ohne Anmeldung kommt niemand an * Gesundheits- und Vermoegensdaten. Diese Datei prueft die Zusage, dass * der zweite Faktor Pflicht ist und sich nicht umgehen laesst. */ test.describe('Anmeldung', () => { // Ein Test loest die Sperre absichtlich aus - danach muss aufgeraeumt // werden, sonst haengen die folgenden Tests an ihr fest. test.afterEach(async () => { await fehlversucheLoeschen() }) test('verlangt alle drei Angaben und laesst danach herein', async ({ page }) => { await page.goto('/anmelden') await expect(page.getByRole('heading', { name: 'Anmelden' })).toBeVisible() await expect(page.locator('input[name="email"]')).toBeVisible() await expect(page.locator('input[name="passwort"]')).toBeVisible() await expect(page.locator('input[name="code"]')).toBeVisible() await anmelden(page) await expect(page).toHaveURL(/\/heute/) }) test('verraet nicht, welche der drei Angaben falsch war', async ({ page }) => { const env = await umgebung() // Zwei Fehlversuche, die sich nur in EINER Angabe unterscheiden. // Beide muessen dieselbe Meldung liefern - sonst liesse sich daraus // ablesen, ob eine E-Mail-Adresse existiert oder ob das Passwort // bereits stimmte. const meldungen: string[] = [] for (const versuch of [ { email: env.SEED_ADMIN_EMAIL!, passwort: 'ganz-sicher-falsch-2026' }, { email: 'gibt-es-nicht@example.invalid', passwort: 'ganz-sicher-falsch-2026' }, ]) { await page.goto('/anmelden') await page.fill('input[name="email"]', versuch.email) await page.fill('input[name="passwort"]', versuch.passwort) await page.fill('input[name="code"]', '123456') await page.click('button[type="submit"]') const meldung = page.locator('form').getByRole('alert') await expect(meldung).toBeVisible() meldungen.push(((await meldung.textContent()) ?? '').trim()) await expect(page).toHaveURL(/\/anmelden/) } // Entweder beide Male die neutrale Meldung, oder beide Male die // Sperrmeldung - aber nie zwei verschiedene Auskuenfte. expect(meldungen[0]).toBe(meldungen[1]) // Und in keinem Fall ein Hinweis auf das einzelne Feld. for (const meldung of meldungen) { expect(meldung).not.toMatch(/unbekannt|nicht registriert|existiert nicht/i) expect(meldung).not.toMatch(/Passwort ist falsch|falsches Passwort/i) } }) test('sperrt nach mehreren Fehlversuchen und sagt das auch', async ({ page }) => { const env = await umgebung() // LOGIN_MAX_VERSUCHE steht in der .env, Standard 5. Sechs Versuche // reichen also in jedem Fall. let sperrmeldung = '' for (let i = 0; i < 6; i++) { await page.goto('/anmelden') await page.fill('input[name="email"]', env.SEED_ADMIN_EMAIL!) await page.fill('input[name="passwort"]', `falsch-${i}`) await page.fill('input[name="code"]', '123456') await page.click('button[type="submit"]') // Auf die Meldung warten, statt sie sofort zu lesen: die Anmeldung // wird bewusst auf eine Mindestdauer gestreckt, damit die Antwortzeit // nichts verraet. const meldung = page.locator('form').getByRole('alert') await expect(meldung).toBeVisible({ timeout: 20_000 }) const text = ((await meldung.textContent()) ?? '').trim() if (/gesperrt|zu viele/i.test(text)) { sperrmeldung = text break } } expect(sperrmeldung, 'Nach sechs Fehlversuchen muss gesperrt werden').not.toBe('') // Die Sperre wird benannt - sonst probiert man ratlos weiter. expect(sperrmeldung).toMatch(/gesperrt|zu viele/i) }) test('schickt Unangemeldete von Fachseiten zur Anmeldung', async ({ page }) => { for (const pfad of ['/heute', '/stammdaten', '/fristen', '/protokoll']) { await page.goto(pfad) await expect(page, `${pfad} muss geschuetzt sein`).toHaveURL(/\/anmelden/) } }) test('setzt eine strenge Content-Security-Policy und laedt nichts von aussen', async ({ page, }) => { const fremdeAnfragen: string[] = [] page.on('request', (anfrage) => { const url = new URL(anfrage.url()) if (!['localhost', '127.0.0.1'].includes(url.hostname)) { fremdeAnfragen.push(anfrage.url()) } }) const antwort = await page.goto('/anmelden') const kopf = antwort?.headers() ?? {} expect(kopf['content-security-policy']).toBeTruthy() expect(kopf['content-security-policy']).toContain("default-src 'self'") expect(kopf['content-security-policy']).toContain("frame-ancestors 'none'") expect(kopf['content-security-policy']).toContain("object-src 'none'") expect(kopf['x-content-type-options']).toBe('nosniff') expect(kopf['x-frame-options']).toBe('DENY') await page.waitForLoadState('networkidle') // Keine Schriften von Google, kein CDN, keine Analysedienste. expect(fremdeAnfragen, `Fremde Anfragen: ${fremdeAnfragen.join(', ')}`).toHaveLength(0) }) test('meldet ab und sperrt danach wieder aus', async ({ page }) => { await anmelden(page) await expect(page).toHaveURL(/\/heute/) // Am Handy liegt das Benutzermenue in der Kopfzeile, am Schreibtisch // unten in der Seitenleiste - der Knopf traegt in beiden Faellen den // Namen des angemeldeten Benutzers. await page.getByRole('button', { name: /Florian Wilde|Betreuer|Administrator/i }).first().click() await page.getByRole('menuitem', { name: 'Abmelden' }).click() await page.waitForURL(/\/anmelden/, { timeout: 30_000 }) await page.goto('/stammdaten') await expect(page).toHaveURL(/\/anmelden/) }) })