Skip to content

test(e2e): UI↔backend auth-contract + magic-link round-trip integration tests (login-regression class) - #167

Merged
mastermanas805 merged 1 commit into
mainfrom
test/auth-roundtrip-integration
Jun 3, 2026
Merged

test(e2e): UI↔backend auth-contract + magic-link round-trip integration tests (login-regression class)#167
mastermanas805 merged 1 commit into
mainfrom
test/auth-roundtrip-integration

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

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 PreflightAllowlist rejected), and the api was missing Access-Control-Allow-Credentials. Unit tests were green; the broken browser↔api contract was invisible.

What already existed (honest survey)

  • Layer-1 e2e/auth-contract.spec.ts (prod target) + workflow auth-contract-e2e.yml: asserts the CORS envelope — OPTIONS preflight carries ACAO=<web origin> + ACAC=true, a real cross-origin no-cookie POST to /auth/exchange resolves (no "Failed to fetch") and returns 4xx, and /auth/email/start → 202.
  • Layer-2 (api repo, 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.
  • The worker auth-probe: post-deploy delivery + CORS, 5-min cadence.

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:

real session JWT in the bridge cookie  (what /auth/email/callback sets)
  → browser credentials:'include' POST /auth/exchange  (cross-origin)
  → cookie is SENT cross-origin AND ACAC lets JS read {token}
  → that token as Authorization: Bearer on GET /auth/me  → 200 + claimed email

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/callback

The single-use magic-link token lives only in the api's magic_links table — 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 real user_id/team_id, then plants a session JWT (HS256, claim shape mirrors the api e2e helper makeSessionJWTWithUser) in the same instanode_session_exchange cookie setExchangeCookie writes, and drives the SPA's real exchange→Bearer→/auth/me path unchanged.

How it runs / gating

  • Needs a NON-prod api (compose http://localhost:8080 or staging) + the stack JWT_SECRET via E2E_JWT_SECRET.
  • Self-skips loudly when the secret is absent or the provisioning backend returns 503 → reports 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 auth-contract-e2e.yml (run_roundtrip input; dispatch inputs validated through env: 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 gate green: tsc + build + 1107 vitest pass.
  • Round-trip spec compiles + is discovered by Playwright; self-skips cleanly with no secret (exit 0, "1 skipped").
  • Existing Layer-1 contract smoke still passes against prod (3/3).
  • Could NOT execute the round-trip's live assertions locally — no reachable non-prod api was available, so it self-skipped. Needs a compose/staging api + E2E_JWT_SECRET to exercise the real assertions.

🤖 Generated with Claude Code

…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
mastermanas805 enabled auto-merge (squash) June 3, 2026 20:21
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
dist/assets/index-D-PLKbcO.js 163.79 KB (0%)
dist/assets/index-BsJUZYRr.css 6.13 KB (0%)

@mastermanas805
mastermanas805 merged commit d93577c into main Jun 3, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant