Skip to content

WebSocket auth handshake: tokens off URLs, in-band refresh #369

WebSocket auth handshake: tokens off URLs, in-band refresh

WebSocket auth handshake: tokens off URLs, in-band refresh #369

Workflow file for this run

name: Frontend E2E Integration
# Runs the Playwright integration spec at
# `frontend/tests/e2e/login-and-navigation.spec.ts` against a real
# Django + Postgres + Redis backend stack. The spec exercises the
# password-login flow and walks every routed view in `src/views/`.
#
# Coverage is captured on BOTH sides:
# - Frontend: Istanbul (vite-plugin-istanbul) → Codecov `frontend-e2e`
# - Backend: coverage.py wrapping Django runserver → Codecov `backend-e2e`
#
# Triggered on PRs that touch the frontend, the spec, or this workflow,
# plus pushes to release branches.
env:
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
defaults:
run:
working-directory: ./
on:
push:
branches:
- main
- v*
pull_request:
paths:
- "frontend/**"
- ".github/workflows/frontend-e2e.yml"
- "opencontractserver/users/migrations/0003_create_initial_superuser.py"
- "config/settings/**"
- "test.yml"
- "test.e2e-coverage.yml"
- "compose/local/django/**"
concurrency:
group: frontend-e2e-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
e2e:
name: Login + Navigate All Views
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
- name: Install Yarn
run: npm install -g yarn
- name: Install frontend dependencies
working-directory: ./frontend
run: yarn install --frozen-lockfile
- name: Install Playwright browsers
working-directory: ./frontend
run: yarn playwright install --with-deps chromium
# ────────────────────────────────────────────────────────────────
# Bring up the minimal backend stack.
#
# `test.yml` already reads from `.envs/.test/.django` and
# `.envs/.test/.postgres`, both of which are committed to the repo.
# The compose override `test.e2e-coverage.yml` swaps the default
# `/start` command for `/start-with-coverage`, which runs Django
# under coverage.py so every request exercised by the Playwright
# spec is measured.
#
# The start script runs `python manage.py migrate` — that triggers
# the `0003_create_initial_superuser` migration, creating the
# admin/Openc0ntracts_def@ult user that the Playwright spec
# logs in as.
#
# We use `--no-deps` for django so we don't have to pull the
# multi-gigabyte parser/embedder images. The `config.settings.test`
# module wires `DEFAULT_EMBEDDER = TestEmbedder` (no HTTP calls)
# and the spec only exercises login + view rendering, so those
# services are never reached at runtime.
# ────────────────────────────────────────────────────────────────
- name: Build backend image
run: docker compose -f test.yml build django
- name: Start postgres and redis
run: |
docker compose -f test.yml up -d postgres redis
echo "Waiting for postgres health…"
for i in {1..30}; do
state=$(docker inspect -f '{{.State.Health.Status}}' \
$(docker compose -f test.yml ps -q postgres) 2>/dev/null || echo "starting")
if [ "$state" = "healthy" ]; then echo "postgres healthy"; break; fi
if [ "$i" = "30" ]; then
echo "postgres did not become healthy in time"
docker compose -f test.yml logs postgres | tail -50
exit 1
fi
sleep 2
done
- name: Start django with coverage (no parser/embedder deps)
run: |
docker compose -f test.yml -f test.e2e-coverage.yml up -d --no-deps django
echo "Started django with backend coverage instrumentation"
- name: Wait for Django to be ready
run: |
echo "Waiting for Django to respond on /api/health/…"
for i in {1..90}; do
if curl -sf -o /dev/null http://localhost:8000/api/health/; then
echo "Django health endpoint responding"
break
fi
if [ "$i" = "90" ]; then
echo "Django did not become ready in time"
docker compose -f test.yml ps
docker compose -f test.yml logs django | tail -200
exit 1
fi
sleep 2
done
- name: Verify admin superuser exists
run: |
docker compose -f test.yml exec -T django python -c "
import django, os
# DATABASE_URL is constructed by /entrypoint at container start but is
# not visible to processes spawned by 'docker compose exec'. Build it
# from the individual POSTGRES_* vars that are injected via env_file.
pg = {k: os.environ[k] for k in ('POSTGRES_USER','POSTGRES_PASSWORD','POSTGRES_HOST','POSTGRES_PORT','POSTGRES_DB')}
os.environ.setdefault('DATABASE_URL', 'postgres://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}'.format(**pg))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.test')
django.setup()
from django.contrib.auth import get_user_model
User = get_user_model()
assert User.objects.filter(username='admin').exists(), 'admin user missing - migration 0003 did not run'
print('OK: admin superuser exists')
"
# ────────────────────────────────────────────────────────────────
# Run the Playwright integration spec.
#
# COVERAGE=true causes vite-plugin-istanbul to instrument the
# frontend source. The fixture in `tests/e2e/fixtures.ts` then
# extracts `window.__coverage__` after each test and dumps it
# under `coverage/e2e/.nyc_output/`. The npm script's `nyc report`
# invocation merges those JSON files into an lcov report.
#
# The Playwright config `webServer` block manages the vite dev
# server lifecycle (start before tests, kill after).
# ────────────────────────────────────────────────────────────────
- name: Run Playwright integration tests with coverage
working-directory: ./frontend
env:
CI: "true"
COVERAGE: "true"
E2E_TEST_USERNAME: admin
E2E_TEST_PASSWORD: "Openc0ntracts_def@ult"
run: yarn run test:e2e:coverage
# ────────────────────────────────────────────────────────────────
# Collect backend coverage.
#
# Gracefully stopping Django triggers coverage.py's atexit handler
# which writes /app/.coverage (visible on the host via the volume
# mount). We then spin up a throwaway container to convert to XML.
# ────────────────────────────────────────────────────────────────
- name: Stop Django gracefully (triggers coverage write)
if: success() || failure()
run: docker compose -f test.yml stop -t 15 django
- name: Export backend coverage to XML
if: success() || failure()
run: |
docker compose -f test.yml run --rm --no-deps django \
coverage xml -o /app/coverage-backend-e2e.xml
echo "Backend e2e coverage report:"
ls -la coverage-backend-e2e.xml
- name: Capture backend logs on failure
if: failure()
run: |
mkdir -p artifacts
docker compose -f test.yml ps > artifacts/docker-ps.txt || true
docker compose -f test.yml logs --no-color > artifacts/docker-compose-logs.txt || true
- name: Upload backend logs on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: e2e-backend-logs
path: artifacts/
- name: Upload Playwright HTML report on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-report-e2e
path: frontend/playwright-report-e2e/
if-no-files-found: ignore
# The `frontend` flag rides along so the same upload feeds both the
# per-suite drill-in and the merged Frontend coverage total that the
# README badge reads. See `frontend.yml` for the companion uploads.
- name: Upload frontend E2E coverage to Codecov
if: success() || failure()
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: frontend/coverage/e2e/lcov.info
flags: frontend-e2e,frontend
name: frontend-e2e-coverage
fail_ci_if_error: false
disable_search: true
- name: Upload backend E2E coverage to Codecov
if: success() || failure()
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage-backend-e2e.xml
flags: backend-e2e
name: backend-e2e-coverage
fail_ci_if_error: false
disable_search: true
- name: Tear down backend stack
if: always()
run: docker compose -f test.yml down -v