Skip to content

Commit d93577c

Browse files
test(e2e): UI↔backend auth-contract cookie-exchange round-trip integration (#167)
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>
1 parent 7aed876 commit d93577c

3 files changed

Lines changed: 461 additions & 0 deletions

File tree

.github/workflows/auth-contract-e2e.yml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ on:
3939
description: 'Override E2E_WEB_ORIGIN'
4040
required: false
4141
default: 'https://instanode.dev'
42+
run_roundtrip:
43+
description: 'Also run the cookie-exchange ROUND-TRIP spec (needs a NON-prod api + E2E_JWT_SECRET secret)'
44+
type: boolean
45+
required: false
46+
default: false
4247
repository_dispatch:
4348
types: [auth-contract-e2e-from-api]
4449

@@ -118,3 +123,84 @@ jobs:
118123
playwright-report-auth-contract/
119124
if-no-files-found: ignore
120125
retention-days: 14
126+
127+
# ── Cookie-exchange ROUND-TRIP (manual / staging only) ───────────────────
128+
#
129+
# The Layer-1 smoke above probes the CORS envelope with NO cookie. This job
130+
# runs the full round-trip spec (e2e/auth-roundtrip.spec.ts): provision →
131+
# claim → plant the bridge cookie → cross-origin exchange → Bearer
132+
# /auth/me 200. That needs a NON-prod api it can mint a bridge cookie for
133+
# (E2E_JWT_SECRET = the api's JWT_SECRET) + a provisioning backend.
134+
#
135+
# WHY workflow_dispatch-only (not on every PR): instanode-web CI cannot
136+
# build/boot the api binary (cross-repo), and we never put the prod
137+
# JWT_SECRET in this repo's CI. The AUTHORITATIVE pre-merge round-trip gate
138+
# lives in the api repo's Layer-2 docker-compose workflow (which builds the
139+
# api from PR source + has the stack-local secret). This job is the
140+
# instanode-web-side companion an operator points at a staging/compose api
141+
# on demand. The spec self-skips loudly when the secret is absent or the
142+
# backend returns 503, so a misfire reports as skipped, never a false red.
143+
auth-roundtrip:
144+
name: Cookie-exchange round-trip (staging/compose, on demand)
145+
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.run_roundtrip == 'true' }}
146+
runs-on: ubuntu-latest
147+
timeout-minutes: 8
148+
env:
149+
# Route dispatch inputs through env: + validate (workflow-injection
150+
# hygiene — same pattern as the smoke job above). The round-trip MUST
151+
# NOT target prod (planting a bridge cookie needs the stack JWT_SECRET).
152+
RAW_API_URL: ${{ github.event.inputs.api_url || '' }}
153+
RAW_WEB_ORIGIN: ${{ github.event.inputs.web_origin || '' }}
154+
# E2E_JWT_SECRET is the api's JWT_SECRET for the TARGET (staging/compose)
155+
# api — provided as a repo/environment secret by the operator who runs
156+
# this. Never the prod secret.
157+
E2E_JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }}
158+
steps:
159+
- uses: actions/checkout@v6
160+
161+
- uses: actions/setup-node@v6
162+
with:
163+
node-version: '22'
164+
cache: 'npm'
165+
166+
- name: Validate + resolve round-trip targets
167+
run: |
168+
set -euo pipefail
169+
api="${RAW_API_URL:-}"
170+
web="${RAW_WEB_ORIGIN:-http://localhost:5173}"
171+
if [ -z "$api" ]; then
172+
echo "::error::run_roundtrip requires a non-empty api_url pointing at a staging/compose api (NOT prod)."
173+
exit 1
174+
fi
175+
# Hard refuse the prod api — the round-trip would need the prod
176+
# JWT_SECRET, which must never enter this repo's CI.
177+
case "$api" in
178+
https://api.instanode.dev|https://api.instanode.dev/)
179+
echo "::error::round-trip cannot target prod api (api.instanode.dev). Point it at a staging/compose api."
180+
exit 1 ;;
181+
esac
182+
api="${api%/}"
183+
web="${web%/}"
184+
echo "E2E_API_URL=$api" >> "$GITHUB_ENV"
185+
echo "E2E_WEB_ORIGIN=$web" >> "$GITHUB_ENV"
186+
echo "Resolved round-trip E2E_API_URL=$api E2E_WEB_ORIGIN=$web"
187+
188+
- run: npm ci
189+
- run: npx playwright install --with-deps chromium
190+
191+
- name: Run cookie-exchange round-trip
192+
# Spec self-skips (loudly) when E2E_JWT_SECRET is empty or the
193+
# provisioning backend returns 503 — so a missing secret reports as
194+
# skipped, not a hard failure.
195+
run: npx playwright test --config=playwright.auth-roundtrip.config.ts
196+
197+
- name: Upload round-trip trace on failure
198+
if: failure()
199+
uses: actions/upload-artifact@v4
200+
with:
201+
name: auth-roundtrip-trace-${{ github.run_id }}
202+
path: |
203+
test-results/
204+
playwright-report-auth-roundtrip/
205+
if-no-files-found: ignore
206+
retention-days: 14

0 commit comments

Comments
 (0)