veza/tests/e2e/25-register-defer-jwt.spec.ts
senke 083b5718a7 feat(auth): defer JWT to post-verify + verify-email header (v1.0.9 items 1.3+1.4)
Item 1.4 — Register no longer issues an access+refresh token pair. The
prior flow set httpOnly cookies at register but the AuthMiddleware
refused them on every protected route until the user had verified
their email (`core/auth/service.go:527`). Users ended up with dead
credentials and a "logged in but locked out" UX. Register now returns
{user, verification_required: true, message} and the SPA's existing
"check your email" notice fires naturally.

Item 1.3 — `POST /auth/verify-email` reads the token from the
`X-Verify-Token` header in preference to the `?token=…` query param.
Query param logged a deprecation warning but stays accepted so emails
dispatched before this release still work. Headers don't leak through
proxy/CDN access logs that record URL but not headers.

Tests: 18 test files updated (sed `_, _, err :=` → `_, err :=` for the
new Register signature). `core/auth/handler_test.go` gets a
`registerVerifyLogin` helper for tests that exercise post-login flows
(refresh, logout). Two new E2E `@critical` specs lock in the defer-JWT
contract and the header read-path.

OpenAPI + orval regenerated to reflect the new RegisterResponse shape
and the verify-email header parameter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:56:31 +02:00

121 lines
4.6 KiB
TypeScript

import { test, expect } from '@chromatic-com/playwright';
import { CONFIG } from './helpers';
/**
* v1.0.9 item 1.4 — Register no longer issues JWT cookies. The user
* must verify their email and POST /auth/login. Locks in the new
* contract so a future "let's set the cookie at register again for
* faster onboarding" PR breaks loud rather than silently re-opening
* the dead-credentials window the audit closed.
*/
test.describe('AUTH — Register defers JWT until verified (v1.0.9 item 1.4)', () => {
test('25. POST /auth/register returns verification_required and NO token block @critical', async ({
request,
}) => {
const uniqueSuffix = Date.now();
const email = `e2e-defer-${uniqueSuffix}@veza.test`;
const password = 'SecurePass123!@#';
const response = await request.post(`${CONFIG.apiURL}/api/v1/auth/register`, {
data: {
email,
username: `e2e-defer-${uniqueSuffix}`,
password,
password_confirmation: password,
},
});
expect(response.status()).toBe(201);
const body = await response.json();
const data = body?.data ?? body;
expect(data?.user?.email).toBe(email);
expect(data?.verification_required).toBe(true);
expect(typeof data?.message).toBe('string');
expect(data?.message.length).toBeGreaterThan(0);
// The legacy `token` block must NOT be present — that was the v1.0.8
// shape and any backend that re-emits it is regressing item 1.4.
expect(data?.token).toBeUndefined();
// No auth cookies on the response either. httpOnly cookies are set
// via Set-Cookie header; Playwright surfaces them via headersArray().
const setCookieHeaders = response
.headersArray()
.filter((h) => h.name.toLowerCase() === 'set-cookie')
.map((h) => h.value.toLowerCase());
for (const cookie of setCookieHeaders) {
expect(cookie).not.toContain('access_token=');
expect(cookie).not.toContain('refresh_token=');
}
});
test('26. POST /auth/login is rejected with 403 until email is verified @critical', async ({
request,
}) => {
const uniqueSuffix = Date.now();
const email = `e2e-unverified-${uniqueSuffix}@veza.test`;
const password = 'SecurePass123!@#';
const registerResponse = await request.post(
`${CONFIG.apiURL}/api/v1/auth/register`,
{
data: {
email,
username: `e2e-unverified-${uniqueSuffix}`,
password,
password_confirmation: password,
},
},
);
expect(registerResponse.status()).toBe(201);
// Immediately try to log in — without verifying. Pre-v1.0.9 the
// backend would have set cookies at register, so a /me call would
// succeed despite the unverified state. Post-v1.0.9 the only path
// to a usable token is verify → login, and login refuses unverified
// accounts with 403 (`core/auth/service.go:527`).
const loginResponse = await request.post(
`${CONFIG.apiURL}/api/v1/auth/login`,
{
data: { email, password, remember_me: false },
},
);
expect(loginResponse.status()).toBe(403);
});
test('27. UI flow: submitting register lands on the "check your email" notice (no dashboard redirect)', async ({
page,
}) => {
test.setTimeout(60_000);
await page.goto(`${CONFIG.baseURL}/register`, { waitUntil: 'domcontentloaded' });
await expect(page.getByTestId('register-form')).toBeVisible({ timeout: 10_000 });
const uniqueSuffix = Date.now();
const email = `e2e-ui-${uniqueSuffix}@veza.test`;
await page.locator('#register-username').fill(`e2e-ui-${uniqueSuffix}`);
await page.locator('#register-email').fill(email);
await page.locator('#register-password').fill('SecurePass123!@#');
await page.locator('#register-password_confirm').fill('SecurePass123!@#');
await page.locator('#register-terms').click({ force: true });
await page.waitForTimeout(300);
await page.getByTestId('register-submit').click();
// Must show the verification notice copy AND must NOT redirect to
// /dashboard. The pre-v1.0.9 buggy flow used to flash the dashboard
// before the auth middleware kicked the user back to /login.
const notice = page
.getByText(/inscription réussie|check your email|vérification|verify/i)
.first();
await expect(notice).toBeVisible({ timeout: 15_000 });
// Stay on /register (the page renders the verification notice
// inline). Or land on /verify-email / /login — never /dashboard.
await page.waitForTimeout(500); // settle any pending router transitions
expect(page.url()).not.toMatch(/\/dashboard/);
});
});