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>
121 lines
4.6 KiB
TypeScript
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/);
|
|
});
|
|
});
|