Files
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

147 lines
5.6 KiB
TypeScript

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',
'/personalakten/irgendeine',
'/abteilungen',
'/abteilungen/SHD',
'/admin',
'/admin/abteilungen',
'/admin/protokoll',
'/suche',
'/faelle/AfiS-2026-0001/export',
'/faelle/AfiS-2026-0001/beteiligte',
'/faelle/AfiS-2026-0001/dokumente',
'/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.describe('Schmale Schirme', () => {
// Die Seite darf auf keinem Gerät waagerecht scrollen. Ein Formular, das
// seitlich hinausragt, ist auf dem Telefon unbedienbar - und genau dort
// wird eine Akte unterwegs gelesen. Siehe docs/09-performance-und-ux.md.
for (const pfad of ['/anmelden', '/anmelden/fehler?grund=keine-afis-rolle']) {
test(`${pfad} laeuft nicht seitlich ueber`, async ({ page }) => {
await page.goto(pfad)
const ueberlauf = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
)
expect(ueberlauf).toBeLessThanOrEqual(1)
})
}
test('Druckansicht laeuft nicht seitlich ueber', async ({ page }) => {
await page.goto('/faelle/AfiS-2026-0001/druck')
const ueberlauf = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
)
expect(ueberlauf).toBeLessThanOrEqual(1)
})
})
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?:$/)
})