porchlight/tests/e2e/full-flow.spec.js
Johan Lundberg baef5e0e2e
fix(security): require CSRF-protected POST to consume a registration link
GET /register/{token} consumed the magic-link token and created a session, so
a side-effecting state change happened on a safe method — link prefetchers,
email scanners, or a cross-site GET could trigger account setup/login.

Split the flow: GET validates the token (without consuming) and renders a
confirmation form; POST /register/{token} consumes the token, runs the
existing checks, and establishes the session. The POST carries a CSRF token
and the session is reset on login as for other auth paths.

Refs: porchlight-9k0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:40:30 +02:00

66 lines
2.7 KiB
JavaScript

// @ts-check
const { test, expect } = require('@playwright/test');
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
test.describe('Full user journey', () => {
test('register via magic link, set password, logout, login', async ({ page }) => {
// Verify fixtures are loaded
expect(fixtures.register_token).toBeTruthy();
// ---- Step 1: Register via magic link ----
// GET shows a confirmation page; submitting it (POST) consumes the token.
await page.goto(`/register/${fixtures.register_token}`);
await page.click('button[type="submit"]');
// Should redirect to /manage/credentials?setup=1
await page.waitForURL('**/manage/credentials?setup=1', { timeout: 5000 });
expect(page.url()).toContain('/manage/credentials');
// Should show welcome message
const welcome = page.locator('[role="status"]');
await expect(welcome).toBeVisible();
const welcomeText = await welcome.textContent();
expect(welcomeText).toContain('Welcome');
// Page title should contain Porchlight
await expect(page).toHaveTitle(/Porchlight/);
// ---- Step 2: Set password ----
const passwordInput = page.locator('#password');
const confirmInput = page.locator('#confirm');
await expect(passwordInput).toBeVisible();
await expect(confirmInput).toBeVisible();
await passwordInput.fill('purple-tiger-mountain-42');
await confirmInput.fill('purple-tiger-mountain-42');
await page.click('#password-section button[type="submit"]');
// Wait for success message
const successMsg = page.locator('#password-section [role="status"]');
await expect(successMsg).toBeVisible({ timeout: 5000 });
await expect(successMsg).toContainText('Password updated');
// ---- Step 3: Logout ----
// Clear the session cookie via the browser context (the request fixture
// has a separate cookie jar and cannot clear the page's session).
await page.context().clearCookies();
// Navigate to credentials — should redirect to login since we're logged out
await page.goto('/manage/credentials');
await page.waitForURL('**/login', { timeout: 5000 });
expect(page.url()).toContain('/login');
// ---- Step 4: Login with the password we just set ----
await page.fill('#username', fixtures.register_username);
await page.fill('#password', 'purple-tiger-mountain-42');
await page.click('form[hx-post="/login/password"] button[type="submit"]');
// Wait for redirect to credentials page
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
expect(page.url()).toContain('/manage/credentials');
// Should NOT show setup message (no ?setup=1)
await expect(page.locator('[role="status"]:has-text("Welcome")')).toHaveCount(0);
});
});