punimtag/e2e/README.md
ilia c0c997f796
All checks were successful
CI / skip-ci-check (pull_request) Successful in 5s
CI / docker-ci (pull_request) Successful in 7s
CI / secret-scan (pull_request) Successful in 12s
CI / viewer-unit (pull_request) Successful in 1m29s
CI / e2e (pull_request) Successful in 4m8s
CI / admin-unit (pull_request) Successful in 4m24s
test: timing budgets + CI hardening (artifact v3 pin, npm cache retry)
Closes three items from the outstanding e2e/CI gap list:

- Timing budgets: timings.measure() only ever recorded durations for the
  (still-unwired) Pushgateway export — nothing failed CI when a step got
  slow. Add e2e/timing-budgets.ts (expectWithinBudget + shared BUDGET_MS
  buckets) and wire it into every measure() call site across the suite.
  Mail-wait steps are deliberately left unbudgeted (external mail-trap
  delivery latency, not a code performance signal).

- actions/upload-artifact@v4 doesn't work against this Gitea/act runner's
  artifact backend — pin to v3 for the e2e failure-report upload.

- Shared act_runner npm cache has corrupted platform-native tarballs before
  (@next/swc-linux-x64-musl) and reds viewer-unit/admin-unit/e2e with no
  product bug involved. All three npm ci steps now retry once after
  `npm cache clean --force` on first failure.

Verified: full local suite green against DEV (37 passed, 6 skipped, no
budget assertion failures) before wiring into CI.
2026-07-15 08:56:31 -04:00

5.2 KiB

PunimTag e2e (Playwright + @levkin/playkit)

Smoke tests against the public DEV URL so we catch LAN-redirect bugs (e.g. NEXTAUTH_URL=http://10.0.10.121:3001 → Kolby #57).

Quick start

cd e2e
cp .env.example .env   # edit credentials
npm ci
npx playwright install chromium
npm test

Private Gitea installs need auth once: set a local git insteadOf with a token before npm ci, then unset it. CI does this in .gitea/workflows/ci.yml. Never commit lockfile URLs that embed tokens.

Environment

Variable Required Purpose
PLAYKIT_BASE_URL yes (default in env-defaults.json) Public viewer host
PLAYKIT_API_BASE_URL no LAN API (see env-defaults.json)
E2E_ADMIN_EMAIL for auth specs Prefer e2e@levkine.canot admin@admin.com
E2E_ADMIN_PASSWORD for auth specs Dedicated e2e password (Vaultwarden / Infisical)
E2E_API_USERNAME / E2E_API_PASSWORD optional FastAPI bearer login, admin role (separate DB from NextAuth; mirrors e2e@levkine.ca)
E2E_API_VIEWER_USERNAME / E2E_API_VIEWER_PASSWORD optional FastAPI bearer login, viewer role — role-permission gate specs only
E2E_VIEWER_EMAIL / E2E_VIEWER_PASSWORD optional NextAuth auth-DB viewer, hasWriteAccess=false (third, independent store) — viewer.write-gates.spec.ts only
PLAYKIT_MAIL_PROVIDER for mail specs mailpit (homelab)
MAILPIT_BASE_URL for mail specs http://10.0.10.45:8025
MAILPIT_USER / MAILPIT_PASSWORD for mail specs Mailpit basic auth

Secrets: Infisical LevkinOpsdev/playkit/punimtag + Gitea Actions. See ansible docs/hardening/SECRETS.md.

Playkit pin: @levkin/playkit@v0.3.1 (git tag). Zod schema option, storageState helpers, playkitFailureArtifacts, Mailpit, and network interception (interceptNetworkCall, startNetworkErrorMonitor — see playkit docs/NETWORK.md) are all in this pin — see network-errors.spec.ts for the silent-4xx/5xx monitors on gallery/search/upload/manage-users. Timing → Pushgateway is supported by the kit but not enabled in this CI job yet.

What we assert

  1. Login page loads on the public hostname
  2. Sign-out stays on that hostname (never 10.x)
  3. FastAPI /health{ "status": "ok" }
  4. Forgot-password / reset-password email in Mailpit; links use public host
  5. Register → unverified block → verify email via Mailpit → login
  6. Idle logout (?e2e_idle_ms=) stays on public host
  7. Upload: submit a tiny fixture via #file-upload (uses storageState)
  8. Manage Users open/close then sign-out (Kolby #57 overlay path)
  9. API /api/v1/auth/me and /api/v1/photos return 401 without token
  10. API catalog: people/tags lists, login 422/401, users/roles 401, missing photo 404|401
  11. Viewer /api/auth/session returns email when storageState is loaded
  12. FastAPI role-permission write gates: viewer 403 vs admin 200 on users, role-permissions, bulk-delete
  13. Public gallery search (/api/search): tag filter, person filter, and combined-filter narrowing return correct subsets; UI tag-filter interaction updates URL + result count
  14. Logged-in-only filters: Favorites checkbox toggles favoritesOnly URL param; People filter UI selection updates URL + result count (uses storageState)
  15. Silent-failure network monitor (startNetworkErrorMonitor): gallery home, search+filter, upload, and Manage Users overlay all stay free of background 4xx/5xx while the UI loads/interacts
  16. interceptNetworkCall spies on the real upload POST (status + { message, photos } shape) and the filtered /api/search?tags= GET (status + Zod-validated shape) instead of brittle waitForResponse/text scraping
  17. Zod-validated API responses: FastAPI login TokenResponse + /auth/me UserResponse (api.fastapi-login.spec.ts), gallery search SearchResponse (gallery.search-filters.spec.ts)
  18. NextAuth (browser-session) write gates: viewer (hasWriteAccess=false) gets 403, admin gets past the gate, on POST /api/faces/{id}/identify (viewer.write-gates.spec.ts) — distinct from the FastAPI role-permission gates in #12
  19. PROD smoke (prod.smoke.spec.ts, opt-in via PROD_BASE_URL): /api/health + login page load on the public PROD host — dormant until the PROD LXC exists (see ROADMAP)
  20. Manage Users real CRUD + causal cross-session effect (admin.manage-users-actions.spec.ts): admin creates/edits/deletes a user through the actual panel (not just open/close), and deactivating a user via the UI immediately revokes that user's already open NextAuth session (jwt callback re-checks isActive, Kolby #57 fix) — every run creates a disposable e2e-manage-test-* account and deletes it in a finally, never touching the shared punimtagdev/MirrorMatch DB's real accounts
  21. Timing budgets (timing-budgets.ts): every timings.measure() call site is followed by expectWithinBudget() against a shared BUDGET_MS bucket — a step getting order-of-magnitude slower reds the test, not just a Pushgateway sample nobody's watching

See repo root ROADMAP.md for gaps and next steps.

Defaults for base URLs live in env-defaults.json (imported by env-defaults.ts, fixtures.ts, playwright.config.ts, and the Gitea e2e job).