The password-reset E2E test was using button[type="submit"].last() to target the password change button on the profile page. The profile page has two submit buttons with identical text, so .last() is layout-order-dependent and breaks if the form order ever changes. Add data-testid="submit-password" to PasswordChangeForm and use getByTestId() in the test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
113 lines
5.2 KiB
TypeScript
113 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 }) => {
|
|
const testEmail = process.env.E2E_EMAIL ?? 'admin@familyarchive.local';
|
|
const originalPassword = process.env.E2E_PASSWORD ?? 'admin123';
|
|
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(process.env.E2E_USERNAME ?? 'admin');
|
|
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(process.env.E2E_USERNAME ?? 'admin');
|
|
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' });
|
|
});
|
|
});
|