test: add E2E to CI and Makefile, rework auth fixture for token rotation #149
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |