dorfteich/apps/web/e2e/auth.spec.ts
Claude Fable 5 1cea675983
Some checks failed
CD / Promote to Int (push) Blocked by required conditions
CD / Build and push images (push) Successful in 1m41s
CI / Lint, typecheck, test (push) Failing after 56s
CI / Auth e2e pack (push) Failing after 43s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Has been cancelled
Add auth e2e regression pack with fixtures and CI stack
The seed script now provisions the documented fixture matrix
(fixture-admin / fixture-user / fixture-pending, idempotent upserts,
rate-limit reset for disposable databases). A six-test Playwright pack
drives the real UI against a full local stack with Mailpit: complete
signup→mail→verify→first-login journey, wrong-password error, guarded
route redirect honoring ?next (race between the login page and the
anonymous guard fixed by teaching the guard about ?next), menu logout,
site-admin gating, and a profile rename reflected in the top bar. The
pack self-skips without E2E_MAILPIT_URL, so the CD smoke stage (now
pinned to smoke.spec.ts) stays untouched; a new CI job boots api +
web dev server against postgres/mailpit service containers and runs
the pack on every PR and push. Also fixed: the web api client choked
on empty 201 bodies. e2e/README.md documents targets and fixtures.

Closes #20

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 05:43:05 +02:00

108 lines
4.7 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { contextForUser, latestMailFor, tokenFromMail } from './helpers';
/**
* M1 auth regression pack. Runs against a full local stack with Mailpit
* (CI job `auth-e2e`, or `deploy/compose` dev stack locally); skipped
* where no mail catcher is available (e.g. the CD smoke run against a
* deployed stage — that pipeline runs e2e/smoke.spec.ts only anyway).
*/
const MAILPIT_URL = process.env.E2E_MAILPIT_URL;
test.skip(!MAILPIT_URL, 'requires a Mailpit instance (E2E_MAILPIT_URL)');
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
test('full signup journey: register, mail, verify, first login', async ({ page }) => {
const stamp = Date.now().toString(36);
const username = `e2e-${stamp}`;
const email = `${username}@dorfteich.test`;
const password = 'ein sehr langes e2e passwort';
await page.goto('/signup');
await page.getByLabel(/username|benutzername/i).fill(username);
await page.getByLabel(/e-mail/i).fill(email);
await page.getByLabel(/display name|anzeigename/i).fill(username);
await page.getByLabel(/^password|^passwort/i).fill(password);
await page.getByRole('button', { name: /register|registrieren/i }).click();
await expect(page.getByRole('heading', { name: /inbox|postfach/i })).toBeVisible();
const mail = await latestMailFor(MAILPIT_URL!, email);
const token = tokenFromMail(mail.text);
await page.goto(`/verify-email?token=${token}`);
await expect(page.getByRole('heading', { name: /confirmed|bestätigt/i })).toBeVisible();
await page.getByRole('link', { name: /sign-in|anmeldung/i }).click();
await page.getByLabel(/username or e-mail|benutzername oder e-mail/i).fill(username);
await page.getByLabel(/^password|^passwort/i).fill(password);
await page.getByRole('button', { name: /sign in|anmelden/i }).click();
// Signed in: the user menu shows the display name.
await expect(page.getByRole('button', { name: username })).toBeVisible();
});
test('login rejects a wrong password with a visible error', async ({ page }) => {
await page.goto('/login');
await page.getByLabel(/username or e-mail|benutzername oder e-mail/i).fill('fixture-user');
await page.getByLabel(/^password|^passwort/i).fill('definitiv falsch');
await page.getByRole('button', { name: /sign in|anmelden/i }).click();
await expect(page.getByRole('alert')).toBeVisible();
});
test('anonymous visitors are redirected to login and return after', async ({ page }) => {
await page.goto('/settings');
await expect(page).toHaveURL(/\/login\?next=%2Fsettings/);
await page.getByLabel(/username or e-mail|benutzername oder e-mail/i).fill('fixture-user');
await page.getByLabel(/^password|^passwort/i).fill('fixture passwort 123');
await page.getByRole('button', { name: /sign in|anmelden/i }).click();
await expect(page).toHaveURL(/\/settings/);
});
test('fixture user signs out via the menu', async ({ browser }) => {
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
const page = await context.newPage();
await page.goto('/');
await page.getByRole('button', { name: 'Fixture User' }).click();
await page.getByRole('menuitem', { name: /sign out|abmelden/i }).click();
await expect(page).toHaveURL(/\/login/);
await context.close();
});
test('admin menu and page are reserved for site admins', async ({ browser }) => {
const admin = await contextForUser(browser, BASE_URL, 'fixture-admin');
const adminPage = await admin.newPage();
await adminPage.goto('/admin');
await expect(adminPage.getByRole('heading', { name: /administration/i })).toBeVisible();
await admin.close();
const member = await contextForUser(browser, BASE_URL, 'fixture-user');
const memberPage = await member.newPage();
await memberPage.goto('/admin');
// Non-admins are bounced to the start page.
await expect(memberPage).toHaveURL(/\/$/);
await member.close();
});
test('profile display-name change shows up in the top bar', async ({ browser }) => {
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
const page = await context.newPage();
await page.goto('/settings');
const nameField = page.getByLabel(/display name|anzeigename/i);
await nameField.fill('Fixture Umbenannt');
await page
.getByRole('button', { name: /^save$|^speichern$/i })
.first()
.click();
await expect(page.getByRole('button', { name: 'Fixture Umbenannt' })).toBeVisible();
// Restore for the next run (idempotent pack).
await nameField.fill('Fixture User');
await page
.getByRole('button', { name: /^save$|^speichern$/i })
.first()
.click();
await expect(page.getByRole('button', { name: 'Fixture User' })).toBeVisible();
await context.close();
});