Files
familienarchiv/frontend/e2e/password-reset.spec.ts
Marcel 39e7ee2c71
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m34s
CI / OCR Service Tests (push) Successful in 37s
CI / Backend Unit Tests (push) Failing after 3m13s
fix(e2e): use dedicated reset user instead of admin in password-reset test
Introduces a separate reset@familyarchive.local / reset123 seed account
(e2e profile only) so the password-reset flow test never touches the
shared admin credentials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 21:17:00 +02:00

114 lines
5.2 KiB
TypeScript

import { test, expect } from '@playwright/test';
/**
* Password-reset E2E tests.
*
* These tests run WITHOUT a stored session because they test unauthenticated flows.
*
* They rely on the "e2e" Spring profile being active in CI (see playwright.config.ts /
* docker-compose.e2e.yml). The profile exposes GET /api/auth/reset-token-for-test?email=
* so we can retrieve the generated token without a real mail server.
*/
test.use({ storageState: { cookies: [], origins: [] } });
// The backend is accessible directly for E2E helper calls (no SvelteKit proxy needed).
const BACKEND_URL = process.env.E2E_BACKEND_URL ?? 'http://localhost:8080';
async function getResetToken(email: string): Promise<string> {
const res = await fetch(
`${BACKEND_URL}/api/auth/reset-token-for-test?email=${encodeURIComponent(email)}`
);
if (!res.ok) throw new Error(`Could not retrieve reset token for ${email}: ${res.status}`);
return res.text();
}
test.describe('Password reset', () => {
test('forgot-password page is accessible without login', async ({ page }) => {
await page.goto('/forgot-password');
await expect(page).toHaveURL('/forgot-password');
await expect(page.getByRole('heading', { name: /Passwort vergessen/i })).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/password-reset-form.png' });
});
test('forgot-password shows success banner for any email (prevents user enumeration)', async ({
page
}) => {
await page.goto('/forgot-password');
await page.getByLabel(/E-Mail/i).fill('nonexistent@example.com');
await page.getByRole('button', { name: /Link anfordern/i }).click();
// Always shows success — never reveals whether the email exists
await expect(page.locator('.bg-green-50')).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/password-reset-success-banner.png' });
});
test('full password reset flow', async ({ page }) => {
// Uses a dedicated low-privilege test account so the admin account is never touched.
const testEmail = 'reset@familyarchive.local';
const originalPassword = 'reset123';
const newPassword = 'NewP@ssw0rd_E2E!';
// 1. Request reset
await page.goto('/forgot-password');
await page.getByLabel(/E-Mail/i).fill(testEmail);
await page.getByRole('button', { name: /Link anfordern/i }).click();
await expect(page.locator('.bg-green-50')).toBeVisible();
// 2. Fetch the token via the test helper endpoint
const token = await getResetToken(testEmail);
expect(token.length).toBeGreaterThan(0);
// 3. Open the reset-password page with the token
await page.goto(`/reset-password?token=${token}`);
await expect(page.getByRole('heading', { name: /Neues Passwort/i })).toBeVisible();
await page.getByLabel(/^Neues Passwort$/i).fill(newPassword);
await page.getByLabel(/Passwort bestätigen/i).fill(newPassword);
await page.getByRole('button', { name: /Passwort speichern/i }).click();
// 4. Success banner — then navigate to login
await expect(page.locator('.bg-green-50')).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/password-reset-changed.png' });
await page.getByRole('link', { name: /Zurück zum Login/i }).click();
// 5. Log in with new password
await expect(page).toHaveURL(/\/login/);
await page.getByLabel('Benutzername').fill(testEmail);
await page.getByLabel('Passwort').fill(newPassword);
await page.getByRole('button', { name: 'Anmelden' }).click();
await expect(page).toHaveURL('/');
// 6. Restore original password via profile page
await page.goto('/profile');
await page.locator('input[name="currentPassword"]').fill(newPassword);
await page.locator('input[name="newPassword"]').fill(originalPassword);
await page.locator('input[name="confirmPassword"]').fill(originalPassword);
await page.getByTestId('submit-password').click();
// After changing password, auth_token is stale → redirect to login
await expect(page).toHaveURL(/\/login/);
// 7. Log back in with original password to confirm restore worked
await page.getByLabel('Benutzername').fill(testEmail);
await page.getByLabel('Passwort').fill(originalPassword);
await page.getByRole('button', { name: 'Anmelden' }).click();
await expect(page).toHaveURL('/');
await page.screenshot({ path: 'test-results/e2e/password-reset-restored.png' });
});
test('reset-password page shows error for invalid token', async ({ page }) => {
await page.goto('/reset-password?token=invalidtoken000');
await page.getByLabel(/^Neues Passwort$/i).fill('somepassword');
await page.getByLabel(/Passwort bestätigen/i).fill('somepassword');
await page.getByRole('button', { name: /Passwort speichern/i }).click();
await expect(page.locator('.text-red-600')).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/password-reset-invalid-token.png' });
});
test('reset-password page shows mismatch error when passwords differ', async ({ page }) => {
await page.goto('/reset-password?token=anytoken');
await page.getByLabel(/^Neues Passwort$/i).fill('password1');
await page.getByLabel(/Passwort bestätigen/i).fill('password2');
await page.getByRole('button', { name: /Passwort speichern/i }).click();
await expect(page.locator('.text-red-600')).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/password-reset-mismatch.png' });
});
});