import { expect, test } from '@playwright/test' test('Gesundheitsendpunkt antwortet mit allen drei Abhaengigkeiten', async ({ request }) => { const response = await request.get('/api/health') expect([200, 503]).toContain(response.status()) const body = await response.json() expect(Object.keys(body.components).sort()).toEqual(['clamav', 'database', 'discordGateway']) }) test('Antwort traegt eine strikte Inhaltsrichtlinie', async ({ page }) => { const response = await page.goto('/anmelden') const csp = response?.headers()['content-security-policy'] ?? '' expect(csp).toContain("object-src 'none'") expect(csp).toContain("frame-ancestors 'none'") expect(csp).toMatch(/script-src [^;]*'nonce-/) expect(csp).not.toMatch(/script-src [^;]*'unsafe-inline'/) }) test('Anmeldeseite bietet den Discord-Weg an', async ({ page }) => { await page.goto('/anmelden') await expect(page.getByRole('button', { name: 'Mit Discord anmelden' })).toBeVisible() }) test('Startseite fuehrt Nichtangemeldete zur Anmeldung', async ({ page }) => { await page.goto('/') await expect(page).toHaveURL(/\/anmelden$/) }) test.describe('Geschuetzte Bereiche', () => { const geschuetzt = [ '/uebersicht', '/faelle', '/faelle/neu', '/personalakten', '/personalakten/neu', '/kanaele', '/suche', '/faelle/AfiS-2026-0001/export', '/einstellungen', '/dokumente/AfiS-2026-0001', '/admin/rollen', '/admin/system', ] for (const pfad of geschuetzt) { test(`${pfad} ist ohne Anmeldung nicht erreichbar`, async ({ page }) => { await page.goto(pfad) await expect(page).toHaveURL(/\/anmelden$/) }) } }) test.describe('Druckansichten', () => { // Sie liegen ausserhalb des angemeldeten Bereichs, weil der PDF-Dienst sie // mit einem kurzlebigen Zeichen aufruft und keine Sitzung hat. Ohne // gueltiges Zeichen gibt es sie nicht — 404 statt Weiterleitung, denn die // Existenz ist selbst eine Information. // Geprueft wird der Inhalt, nicht der Statuscode: Next streamt die Antwort, // der Kopf ist also schon raus, wenn notFound() greift. Entscheidend ist, // dass nichts aus der Akte auf der Seite landet. for (const pfad of ['/faelle/AfiS-2026-0001/druck', '/dokumente/AfiS-2026-0001/druck']) { for (const [name, ziel] of [ ['ohne Zeichen', pfad], ['mit erfundenem Zeichen', `${pfad}?token=abc.def`], ] as const) { test(`${pfad} zeigt ${name} nichts`, async ({ page }) => { await page.goto(ziel) await expect(page.getByRole('heading', { level: 1 })).toHaveText('Nicht gefunden') await expect(page.locator('.print-cover')).toHaveCount(0) await expect(page.locator('.sheet')).toHaveCount(0) }) } } }) test('Fehlerseite benennt den Grund der Ablehnung', async ({ page }) => { await page.goto('/anmelden/fehler?grund=keine-afis-rolle') await expect(page.getByRole('heading', { level: 1 })).toHaveText( 'Keine Berechtigung für das Aktensystem', ) await page.goto('/anmelden/fehler?grund=keine-mitgliedschaft') await expect(page.getByRole('heading', { level: 1 })).toHaveText( 'Du bist nicht auf dem AfiS-Discord', ) }) test('OAuth-Start nutzt Scope identify, state und PKCE', async ({ request, baseURL }) => { // Sichert die Anforderungen aus docs/03-auth-und-rechte.md ab. // // Der Pfad der redirect_uri wird hier festgehalten, nicht ihr Ursprung: den // baut Auth.js aus AUTH_URL, das in web/auth.ts aus APP_URL abgeleitet wird. // Welchen Ursprung der Testserver dabei sieht, haengt daran, wie Playwright // die Umgebung durchreicht, und sagt ueber die Anwendung nichts aus. Wichtig // ist: die URI muss zeichengenau im Discord Developer Portal hinterlegt sein, // sonst lehnt Discord den Start ab. const csrfResponse = await request.get('/api/auth/csrf') const { csrfToken } = await csrfResponse.json() const response = await request.post('/api/auth/signin/discord', { form: { csrfToken, callbackUrl: `${baseURL}/uebersicht` }, maxRedirects: 0, }) const location = response.headers()['location'] ?? '' expect(location).toContain('discord.com') const params = new URL(location).searchParams expect(params.get('scope')).toBe('identify') expect(params.get('state')).toBeTruthy() expect(params.get('code_challenge')).toBeTruthy() expect(params.get('code_challenge_method')).toBe('S256') const redirectUri = new URL(params.get('redirect_uri') ?? '') expect(redirectUri.pathname).toBe('/api/auth/callback/discord') expect(redirectUri.protocol).toMatch(/^https?:$/) })