Files
explorer-monorepo/scripts/e2e-full.sh

124 lines
4.3 KiB
Bash
Raw Normal View History

test(e2e): add make e2e-full target, full-stack Playwright spec, CI wiring, docs Closes the 'e2e tests only hit production; no local full-stack harness' finding from the review. The existing e2e suite (scripts/e2e-explorer-frontend.spec.ts) runs against explorer.d-bis.org and so can't validate a PR before it merges -- it's a production canary, not a pre-merge gate. This PR adds a parallel harness that stands the entire stack up locally (postgres + elasticsearch + redis via docker-compose, backend API, and a production build of the frontend) and runs a Playwright smoke spec against it. It is wired into Make and into a dedicated CI workflow. Changes: scripts/e2e-full.sh (new, chmod +x): - docker compose -p explorer-e2e up -d postgres elasticsearch redis. - Waits for postgres readiness (pg_isready loop). - Runs database/migrations/migrate.go so schema + seeds including the new 0016_jwt_revocations table from PR #8 are applied. - Starts 'go run ./backend/api/rest' on :8080; waits for /healthz. - Builds + starts 'npm run start' on :3000; waits for a 200. - npx playwright install --with-deps chromium; runs the full-stack spec; tears down docker and kills the backend+frontend processes via an EXIT trap. E2E_KEEP_STACK=1 bypasses teardown for interactive debugging. - Generates an ephemeral JWT_SECRET per run so stale tokens don't bleed across runs (and the fail-fast check from PR #3 passes). - Provides a dev-safe CSP_HEADER default so PR #3's hardened production CSP check doesn't reject localhost connections. scripts/e2e-full-stack.spec.ts (new): - Playwright spec that exercises public routes + a couple of backend endpoints. Takes a full-page screenshot of each route into test-results/screenshots/<route>.png so reviewers can eyeball the render from CI artefacts. - Covers: /healthz, /, /blocks, /transactions, /addresses, /tokens, /pools, /search, /wallet, /routes, /api/v1/access/products (YAML catalogue from PR #7), /api/v1/auth/nonce (SIWE kickoff). - Sticks to Track-1 (no wallet auth needed) so it can run in CI without provisioning a test wallet. playwright.config.ts: - Broadened testMatch from a single filename to /e2e-.*\.spec\.ts/ so the new spec is picked up alongside the existing production canary spec. fullyParallel, worker, timeout, reporter, and project configuration unchanged. Makefile: - New 'e2e-full' target -> ./scripts/e2e-full.sh. Listed in 'help'. - test-e2e (production canary) left untouched. .github/workflows/e2e-full.yml (new): - Dedicated workflow, NOT on every push/PR (the full stack takes minutes and requires docker). Triggers: * workflow_dispatch (manual) * PRs labelled run-e2e-full (opt-in for changes that touch migrations, auth, or routing) * nightly schedule (04:00 UTC) - Uses Go 1.23.x and Node 20 to match PR #5's pinning. - Uploads two artefacts on every run: e2e-screenshots (test-results/screenshots/) and playwright-report. docs/TESTING.md (new): - Four-tier test pyramid: unit -> static analysis -> production canary -> full-stack Playwright. - Env var reference table for e2e-full.sh. - How to trigger the CI workflow. Verification: bash -n scripts/e2e-full.sh clean The spec imports compile cleanly against the existing @playwright /test v1.40 declared in the root package.json; no new runtime dependencies are added. Existing scripts/e2e-explorer-frontend.spec.ts still matched by the broadened testMatch regex. Advances completion criterion 7 (end-to-end coverage): 'make e2e-full boots the real stack, Playwright runs against it, CI uploads screenshots, a nightly job catches regressions that only show up when all services are live.'
2026-04-18 19:26:34 +00:00
#!/usr/bin/env bash
# scripts/e2e-full.sh
#
# Boots the full explorer stack (postgres, elasticsearch, redis, backend
# API, frontend), waits for readiness, runs the Playwright full-stack
# smoke spec against it, and tears everything down. Used by the
# `make e2e-full` target and by the e2e-full CI workflow.
#
# Env vars:
# E2E_KEEP_STACK=1 # don't tear down on exit (for debugging)
# E2E_SKIP_DOCKER=1 # assume backend + deps already running
# EXPLORER_URL # defaults to http://localhost:3000
# EXPLORER_API_URL # defaults to http://localhost:8080
# E2E_SCREENSHOT_DIR # defaults to test-results/screenshots
# JWT_SECRET # required; generated ephemerally if unset
# CSP_HEADER # required; a dev-safe default is injected
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
COMPOSE="deployment/docker-compose.yml"
COMPOSE_PROJECT="${COMPOSE_PROJECT:-explorer-e2e}"
export EXPLORER_URL="${EXPLORER_URL:-http://localhost:3000}"
export EXPLORER_API_URL="${EXPLORER_API_URL:-http://localhost:8080}"
export E2E_SCREENSHOT_DIR="${E2E_SCREENSHOT_DIR:-$ROOT/test-results/screenshots}"
mkdir -p "$E2E_SCREENSHOT_DIR"
# Generate ephemeral JWT secret if the caller didn't set one. Real
# deployments use fail-fast validation (see PR #3); for a local run we
# want a fresh value each invocation so stale tokens don't bleed across
# runs.
export JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}"
export CSP_HEADER="${CSP_HEADER:-default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' http://localhost:8080 ws://localhost:8080}"
log() { printf '[e2e-full] %s\n' "$*"; }
teardown() {
local ec=$?
if [[ "${E2E_KEEP_STACK:-0}" == "1" ]]; then
log "E2E_KEEP_STACK=1; leaving stack running."
return $ec
fi
log "tearing down stack"
if [[ "${E2E_SKIP_DOCKER:-0}" != "1" ]]; then
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE" down -v --remove-orphans >/dev/null 2>&1 || true
fi
if [[ -n "${BACKEND_PID:-}" ]]; then kill "$BACKEND_PID" 2>/dev/null || true; fi
if [[ -n "${FRONTEND_PID:-}" ]]; then kill "$FRONTEND_PID" 2>/dev/null || true; fi
return $ec
}
trap teardown EXIT
wait_for() {
local url="$1" label="$2" retries="${3:-60}"
log "waiting for $label at $url"
for ((i=0; i<retries; i++)); do
if curl -fsS "$url" >/dev/null 2>&1; then
log " $label ready"
return 0
fi
sleep 2
done
log " $label never became ready"
return 1
}
if [[ "${E2E_SKIP_DOCKER:-0}" != "1" ]]; then
log "starting postgres, elasticsearch, redis via docker compose"
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE" up -d postgres elasticsearch redis
log "waiting for postgres"
for ((i=0; i<60; i++)); do
if docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE" exec -T postgres pg_isready -U explorer >/dev/null 2>&1; then
break
fi
sleep 2
done
fi
export DB_HOST="${DB_HOST:-localhost}"
export DB_PORT="${DB_PORT:-5432}"
export DB_USER="${DB_USER:-explorer}"
export DB_PASSWORD="${DB_PASSWORD:-changeme}"
export DB_NAME="${DB_NAME:-explorer}"
export REDIS_HOST="${REDIS_HOST:-localhost}"
export REDIS_PORT="${REDIS_PORT:-6379}"
export ELASTICSEARCH_URL="${ELASTICSEARCH_URL:-http://localhost:9200}"
log "running migrations"
(cd backend && go run database/migrations/migrate.go) || {
log "migrations failed; continuing so tests can report the real backend state"
}
log "starting backend API on :8080"
(cd backend/api/rest && go run . >/tmp/e2e-backend.log 2>&1) &
BACKEND_PID=$!
wait_for "$EXPLORER_API_URL/healthz" backend 120 || {
log "backend log tail:"; tail -n 60 /tmp/e2e-backend.log || true
exit 1
}
log "building frontend"
(cd frontend && npm ci --no-audit --no-fund --loglevel=error && npm run build)
log "starting frontend on :3000"
(cd frontend && PORT=3000 HOST=127.0.0.1 NEXT_PUBLIC_API_URL="$EXPLORER_API_URL" npm run start >/tmp/e2e-frontend.log 2>&1) &
FRONTEND_PID=$!
wait_for "$EXPLORER_URL" frontend 60 || {
log "frontend log tail:"; tail -n 60 /tmp/e2e-frontend.log || true
exit 1
}
log "running Playwright full-stack smoke"
npx playwright install --with-deps chromium >/dev/null
EXPLORER_URL="$EXPLORER_URL" EXPLORER_API_URL="$EXPLORER_API_URL" \
npx playwright test scripts/e2e-full-stack.spec.ts --reporter=list
log "done; screenshots in $E2E_SCREENSHOT_DIR"