veza/tests/e2e/26-verify-email-header.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

91 lines
3.9 KiB
TypeScript

import { test, expect } from '@chromatic-com/playwright';
import { CONFIG } from './helpers';
/**
* v1.0.9 item 1.3 — verify-email token travels in the X-Verify-Token
* header. The legacy `?token=…` query param is still accepted for
* backward-compat with emails dispatched before v1.0.9 — both paths
* eventually flip is_verified, but only the header form keeps the
* token out of proxy / server access logs that record URLs.
*
* These specs exercise the *contract* (was the input read at all?)
* not the full inbox-to-verified flow, which is covered by
* `verify-email-audit.spec.ts`. The contract assertion is: with a
* bogus token the validator runs and rejects with a token-validation
* error message, distinct from the "no token at all" 400 we get when
* neither header nor query is provided. That distinction proves the
* handler actually reads the input from the path under test.
*/
test.describe('AUTH — verify-email accepts header (preferred) and query (deprecated) (v1.0.9 item 1.3)', () => {
test('25. POST /auth/verify-email without any token returns "Token required" 400 @critical', async ({
request,
}) => {
const response = await request.post(
`${CONFIG.apiURL}/api/v1/auth/verify-email`,
);
expect(response.status()).toBe(400);
const body = await response.json();
const message: string =
body?.error?.message ?? body?.error ?? body?.message ?? '';
// The handler returns "Token required" specifically (vs the
// validator's "Email verification failed" used when a token was
// read but rejected) — that's the signal that the read path
// produced nothing.
expect(message.toLowerCase()).toContain('token');
});
test('26. POST /auth/verify-email reads the X-Verify-Token header (validator runs, rejects bogus value) @critical', async ({
request,
}) => {
const response = await request.post(
`${CONFIG.apiURL}/api/v1/auth/verify-email`,
{
headers: { 'X-Verify-Token': 'e2e-bogus-but-non-empty-token' },
},
);
// Status: still 400 because the token is bogus. The proof that the
// header was READ is that the message is NOT "Token required" —
// we got past the read-path guard into the validator. If a
// refactor accidentally drops header support, this test fails
// with a "Token required" message and the regression is loud.
expect(response.status()).toBe(400);
const body = await response.json();
const message: string =
body?.error?.message ?? body?.error ?? body?.message ?? '';
expect(message.toLowerCase()).not.toBe('token required');
// Validator-level error language. The exact wording lives in
// `internal/handlers/auth.go` (apperrors.Wrap).
expect(message.toLowerCase()).toMatch(
/verification|invalid|expired|email/i,
);
});
test('27. POST /auth/verify-email accepts the legacy ?token=… query param (deprecated path)', async ({
request,
}) => {
// Emails dispatched before v1.0.9 land in inboxes with the token
// in the URL query. The frontend has been updated to hoist it to
// the header before sending, but a user pasting the URL into curl
// — or an external automation that hasn't migrated — must still
// verify. The handler logs a deprecation warning when this path
// is taken (`auth.go:VerifyEmail` calls `authService.GetLogger`)
// — not asserted here, but greppable in operational logs.
const response = await request.post(
`${CONFIG.apiURL}/api/v1/auth/verify-email?token=e2e-bogus-but-non-empty-legacy-token`,
);
expect(response.status()).toBe(400);
const body = await response.json();
const message: string =
body?.error?.message ?? body?.error ?? body?.message ?? '';
expect(message.toLowerCase()).not.toBe('token required');
expect(message.toLowerCase()).toMatch(
/verification|invalid|expired|email/i,
);
});
});