Auth.js baut die redirect_uri nicht aus dem Host-Header, sondern aus AUTH_URL. Ohne die Variable bleibt sie fest auf http://localhost:3000 - hinter dem Cloudflare Tunnel wäre die Anmeldung damit kaputt gewesen, und zwar mit einer Fehlermeldung von Discord, die nicht auf die Ursache zeigt. APP_URL ist ohnehin die kanonische öffentliche Adresse und wird jetzt durchgereicht. Nachweis mit dem gebauten Standalone-Server: mit gesetztem APP_URL kommt https://afis.example.de/api/auth/callback/discord heraus, ohne fällt sie auf localhost zurück. Ein E2E-Test hält Scope identify, state, PKCE mit S256 und den Pfad der Callback-URL fest. Nebenbefund, eigenständig behoben: loadEnvFromRepoRoot hat gesetzte Umgebungsvariablen mit den Werten der Entwicklungs-.env überschrieben. process.loadEnvFile tut das von sich aus. Eine .env ist ein Rückfall für die lokale Entwicklung, kein Vorrang vor dem, was der Aufrufer ausdrücklich mitgibt - sonst überschreibt sie etwa die APP_URL eines Testlaufs und die Fehlersuche führt weit weg von der Ursache.
80 lines
3.2 KiB
TypeScript
80 lines
3.2 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', () => {
|
|
for (const pfad of ['/uebersicht', '/admin/rollen', '/admin/system']) {
|
|
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?:$/)
|
|
})
|