test(e2e): UI↔backend auth-contract + magic-link round-trip integration tests (login-regression class) - #167
Merged
Conversation
…ation
Adds the missing end-to-end integration test for the AUTH-004 cookie-exchange
seam — the exact path that broke in the 2026-05-29 → 2026-05-30 prod-login
outage (web missing /auth/exchange POST; Accept header forcing a rejected
preflight; api missing access-control-allow-credentials). Unit tests were
green throughout; nothing exercised the UI↔backend contract end-to-end.
Existing gates already cover the CORS *envelope* (preflight headers + a
no-cookie cross-origin POST resolving + /auth/email/start=202): Layer-1
e2e/auth-contract.spec.ts (prod) and the api repo's Layer-2 compose spec.
This adds the full ROUND-TRIP no other test covers:
real session JWT in the bridge cookie (what /auth/email/callback sets)
→ browser credentials:'include' POST /auth/exchange (cross-origin)
→ cookie SENT cross-origin AND ACAC lets JS read {token}
→ that token as Authorization: Bearer on GET /auth/me → 200 + claimed email
It can't drive the literal /auth/email/callback (the single-use token lives
only in the api's magic_links table, is emailed, never API-returned; Brevo
sender is unvalidated so no inbox in prod/CI). Instead it reconstructs the
exact post-callback browser state: provision (/cache/new) → claim (/claim)
against the live api for a REAL user_id/team_id, then plants a session JWT
(HS256, claim shape mirrors api e2e makeSessionJWTWithUser) in the same
instanode_session_exchange cookie setExchangeCookie writes, and drives the
SPA's real exchange→Bearer→/auth/me path unchanged.
Gating / how it runs:
- Needs a NON-prod api (compose http://localhost:8080 or staging) + the
stack JWT_SECRET (E2E_JWT_SECRET). Self-skips LOUDLY when the secret is
absent or the provisioning backend returns 503 — skipped, never a false red.
- Refuses to target prod (planting a bridge cookie needs the stack secret,
which never enters this repo's CI).
- Wired as a workflow_dispatch-only `auth-roundtrip` job in
.github/workflows/auth-contract-e2e.yml (run_roundtrip input). The
authoritative pre-merge round-trip gate lives in the api repo's Layer-2
compose workflow (builds the api from PR source, has the stack-local
secret); this is the instanode-web-side companion for staging/compose.
Verified: tsc/build/vitest gate green (1107 pass); spec compiles + is
discovered by Playwright; self-skips cleanly with no secret; existing
Layer-1 contract smoke still passes against prod. Could not execute the
round-trip's live assertions locally — no reachable non-prod api available
(self-skips). Needs a compose/staging api to exercise.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mastermanas805
enabled auto-merge (squash)
June 3, 2026 20:21
size-limit report 📦
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Closes the integration-test gap behind the 2026-05-29 → 2026-05-30 prod-login outage. That regression shipped unnoticed because no test exercised the UI↔backend auth contract end-to-end: server AUTH-004 shipped without the client, the client preflight 403'd (Accept header forced a preflight
PreflightAllowlistrejected), and the api was missingAccess-Control-Allow-Credentials. Unit tests were green; the broken browser↔api contract was invisible.What already existed (honest survey)
e2e/auth-contract.spec.ts(prod target) + workflowauth-contract-e2e.yml: asserts the CORS envelope — OPTIONS preflight carriesACAO=<web origin>+ACAC=true, a real cross-origin no-cookie POST to/auth/exchangeresolves (no "Failed to fetch") and returns 4xx, and/auth/email/start→ 202.auth-contract-compose-pw.yml+e2e/browser/tests/auth-contract-local.spec.ts): the same envelope assertions against an api binary built from PR source in docker-compose — the authoritative pre-merge gate.These prove the CORS envelope. None of them exercises the full cookie-exchange round-trip — the precise seam that broke.
What this PR adds
e2e/auth-roundtrip.spec.ts(+playwright.auth-roundtrip.config.ts) — the missing end-to-end round-trip:The contract-envelope tests probe with no cookie and accept 4xx, so they can't catch a SameSite/Path/credentials/ACAC regression that stops the bridge cookie being sent or read. This spec asserts 200 + token from the exchange and 200 + the claimed email from
/auth/me— it reds the PR exactly when that seam breaks.Why it can't drive the literal
/auth/email/callbackThe single-use magic-link token lives only in the api's
magic_linkstable — it is emailed, never API-returned (anti-enumeration), and the Brevo sender is unvalidated in prod so no inbox exists in prod/CI. So the spec reconstructs the exact post-callback browser state: provision (/cache/new) → claim (/claim) against the live api for a realuser_id/team_id, then plants a session JWT (HS256, claim shape mirrors the api e2e helpermakeSessionJWTWithUser) in the sameinstanode_session_exchangecookiesetExchangeCookiewrites, and drives the SPA's real exchange→Bearer→/auth/mepath unchanged.How it runs / gating
http://localhost:8080or staging) + the stackJWT_SECRETviaE2E_JWT_SECRET.workflow_dispatch-onlyauth-roundtripjob inauth-contract-e2e.yml(run_roundtripinput; dispatch inputs validated throughenv:per workflow-injection hygiene). The authoritative pre-merge round-trip gate belongs in the api repo's Layer-2 compose workflow; this is the instanode-web-side companion an operator points at a staging/compose api.Verification
npm run gategreen: tsc + build + 1107 vitest pass.E2E_JWT_SECRETto exercise the real assertions.🤖 Generated with Claude Code