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', '/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('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?:$/) })