Skip to content

test: add E2E to CI and Makefile, rework auth fixture for token rotation #149

test: add E2E to CI and Makefile, rework auth fixture for token rotation

test: add E2E to CI and Makefile, rework auth fixture for token rotation #149

Workflow file for this run

name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
backend:
name: Backend
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: zerotrust
POSTGRES_PASSWORD: zerotrust_secret
POSTGRES_DB: zerotrust_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U zerotrust -d zerotrust_test"
--health-interval 5s
--health-timeout 5s
--health-retries 12
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 12
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: backend/go.mod
cache-dependency-path: backend/go.sum
- name: Build
run: go build ./...
- name: Vet
run: go vet ./...
- name: Test
run: go test -count=1 -p 1 -json -coverprofile=coverage.out ./... | tee test-results.json
env:
TEST_DATABASE_URL: postgres://zerotrust:zerotrust_secret@localhost:5432/zerotrust_test?sslmode=disable
TEST_REDIS_ADDR: localhost:6379
TEST_REDIS_PASSWORD: ""
- name: Generate badge data
run: |
TOTAL=$(jq -s '[.[] | select(.Action == "pass" and has("Test"))] | length' test-results.json)
COV=$(go tool cover -func=coverage.out | tail -n 1 | awk '{print $3}')
mkdir -p badges
printf '{"schemaVersion":1,"label":"backend tests","message":"%s passing","color":"brightgreen"}' "$TOTAL" > badges/backend-tests.json
printf '{"schemaVersion":1,"label":"backend coverage","message":"%s","color":"brightgreen"}' "$COV" > badges/backend-coverage.json
- name: Upload badge data
uses: actions/upload-artifact@v7
with:
name: badges-backend
path: backend/badges/
retention-days: 1
frontend:
name: Frontend
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "24"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
run: npm ci
- name: Type check
run: npx tsc --noEmit
- name: Lint
run: npm run lint
- name: Test
run: npm run test:cover 2>&1 | tee test-output.txt
- name: Generate badge data
run: |
TOTAL=$(grep -oP 'Tests\s+\K\d+(?= passed)' test-output.txt | tail -n 1)
COV=$(jq -r '.total.lines.pct' coverage/coverage-summary.json)
mkdir -p badges
printf '{"schemaVersion":1,"label":"frontend tests","message":"%s passing","color":"brightgreen"}' "$TOTAL" > badges/frontend-tests.json
printf '{"schemaVersion":1,"label":"frontend coverage","message":"%s%%","color":"brightgreen"}' "$COV" > badges/frontend-coverage.json
- name: Upload badge data
uses: actions/upload-artifact@v7
with:
name: badges-frontend
path: frontend/badges/
retention-days: 1
- name: Build
run: npm run build
env:
NEXT_TELEMETRY_DISABLED: 1
e2e:
name: E2E (Playwright)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: zerotrust
POSTGRES_PASSWORD: zerotrust_secret
POSTGRES_DB: zerotrust_db
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U zerotrust -d zerotrust_db"
--health-interval 5s
--health-timeout 5s
--health-retries 12
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: backend/go.mod
cache-dependency-path: backend/go.sum
- uses: actions/setup-node@v5
with:
node-version: "24"
cache: npm
cache-dependency-path: frontend/package-lock.json
# DEV_MODE=true makes the backend default to
# postgres://zerotrust:zerotrust_secret@localhost:5432/zerotrust_db and
# REDIS_PASSWORD=zerotrust_secret, matching the services started here.
- name: Start Redis (dev password)
run: docker run -d -p 6379:6379 redis:7-alpine redis-server --requirepass zerotrust_secret
- name: Build and start backend
working-directory: backend
env:
DEV_MODE: "true"
REGISTRATION_ENABLED: "true"
MFA_ENABLED: "false"
TLS_ENABLED: "false"
COOKIES_SECURE: "false"
CORS_ALLOWED_ORIGINS: "http://localhost:3000"
PUBLIC_APP_URL: "http://localhost:3000"
WEBAUTHN_RP_ID: "localhost"
run: |
go build -o server ./cmd/server
nohup ./server > backend-e2e.log 2>&1 &
for i in $(seq 1 60); do
curl -sf http://localhost:8080/health && exit 0
sleep 1
done
echo "backend did not become healthy; log follows"
cat backend-e2e.log
exit 1
# Throwaway users for the ephemeral CI database; pgcrypto's
# crypt(gen_salt('bf', 12)) produces the bcrypt hashes the backend
# verifies. Mirrors the local 'make test-e2e' setup.
- name: Create E2E test users
env:
PGPASSWORD: zerotrust_secret
run: |
psql -h localhost -U zerotrust -d zerotrust_db -q -c "\
INSERT INTO users (email, first_name, last_name, password_hash, locale, email_hash) \
VALUES ('[email protected]', 'E2E', 'User', crypt('E2eUser!Passw0rd', gen_salt('bf', 12)), 'en', \
encode(digest(lower(btrim('[email protected]')), 'sha256'), 'hex')), \
('[email protected]', 'E2E', 'Admin', crypt('E2eAdmin!Passw0rd', gen_salt('bf', 12)), 'en', \
encode(digest(lower(btrim('[email protected]')), 'sha256'), 'hex')) \
ON CONFLICT (email) DO NOTHING;"
psql -h localhost -U zerotrust -d zerotrust_db -q -c "\
INSERT INTO user_roles (user_id, role_id) \
SELECT u.id, r.id FROM users u, roles r \
WHERE u.email = '[email protected]' AND r.name = 'admin' \
ON CONFLICT DO NOTHING;"
- name: Install frontend dependencies
working-directory: frontend
run: npm ci
# Playwright's webServer block starts the Vite dev server on :3000 and
# the suite uses the runner's preinstalled Chrome (channel: "chrome").
- name: Run Playwright E2E tests
working-directory: frontend
env:
E2E_USER_EMAIL: [email protected]
E2E_USER_PASSWORD: E2eUser!Passw0rd
E2E_ADMIN_EMAIL: [email protected]
E2E_ADMIN_PASSWORD: E2eAdmin!Passw0rd
run: npm run test:e2e
- name: Upload failure artifacts
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-results
path: frontend/test-results/
retention-days: 7
security:
name: Security Scans
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: backend/go.mod
cache-dependency-path: backend/go.sum
- name: Run govulncheck
run: make govulncheck
- uses: actions/setup-node@v5
with:
node-version: "24"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
working-directory: frontend
run: npm ci
- name: Run npm audit (high+)
working-directory: frontend
# GHSA-qwww-vcr4-c8h2 (react-router RSC-mode CSRF) is accepted: it only
# affects React Server Components / server actions, which this Vite SPA
# does not use. Any other high/critical advisory fails the job.
# Re-evaluate when a patched react-router release ships.
run: |
npm audit --audit-level=high --json > audit.json || true
OTHER=$(jq '[.vulnerabilities | to_entries[] | select(.key != "react-router" and .key != "react-router-dom") | select(.value.severity == "high" or .value.severity == "critical")] | length' audit.json)
RR=$(jq '[.vulnerabilities["react-router"].via[]? | select(type == "object") | select(.url | endswith("GHSA-qwww-vcr4-c8h2") | not)] | length' audit.json)
if [ "$OTHER" != "0" ] || [ "$RR" != "0" ]; then
echo "Unexpected high/critical vulnerabilities found:"
npm audit --audit-level=high || true
exit 1
fi
echo "npm audit clean (only accepted GHSA-qwww-vcr4-c8h2 present)"
badges:
name: Update badge data
needs: [backend, frontend]
# Only refresh badge data on green pushes to the default branch.
if: github.event_name == 'push' && needs.backend.result == 'success' && needs.frontend.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- uses: actions/download-artifact@v8
with:
name: badges-backend
path: docs/badges
- uses: actions/download-artifact@v8
with:
name: badges-frontend
path: docs/badges
- name: Commit badge data
run: |
# Author the commit as the user who triggered the push (resolved to
# their noreply address via the API) so github-actions[bot] does not
# pile up commits and pollute the contributors graph.
ACTOR="${{ github.actor }}"
ACTOR_ID=$(curl -sf "https://api.github.com/users/$ACTOR" | jq -r .id)
git config user.name "$ACTOR"
git config user.email "[email protected]"
git add docs/badges/
if git diff --cached --quiet; then
echo "Badge data unchanged"
exit 0
fi
# [skip ci] prevents this commit from re-triggering this workflow;
# pages.yml still picks it up via its docs/** path filter.
git commit -m "chore: update test and coverage badge data [skip ci]"
git pull --rebase --autostash origin "${GITHUB_REF_NAME}"
git push