# PunimTag e2e (Playwright + @levkin/playkit) Smoke tests against the **public** DEV URL so we catch LAN-redirect bugs (e.g. `NEXTAUTH_URL=http://:3001` → Kolby #57). ## Quick start ```bash 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.ca` — **not** `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://:8025` | | `MAILPIT_USER` / `MAILPIT_PASSWORD` | for mail specs | Mailpit basic auth | **Secrets:** Infisical `LevkinOps` → `dev` → `/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`](../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). The checked-in file holds placeholder hosts; real targets come from `PLAYKIT_BASE_URL` / `PLAYKIT_API_BASE_URL` env vars (Gitea Actions secrets in CI) or a gitignored `env-defaults.local.json` alongside it for local runs.