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
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
147 lines
5.6 KiB
TypeScript
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?:$/)
|
|
})
|