Files
EventSnap/e2e/README.md
fabi bbdfae09a0 chore(e2e): add ESLint + Prettier; fix real findings; dedupe BASE
The Playwright suite had no linter and no formatter — only tsc. Add flat-config ESLint
(typescript-eslint, type-aware) and Prettier (2-space, matching the suite's style).

Rules keep the ones that catch real TEST bugs and drop the noise:
  - no-floating-promises KEPT — an un-awaited request/assertion can let a test end before it runs,
    passing vacuously. It caught one: the SSE reader loop in sse-listener is now explicitly `void`.
  - no-unused-vars KEPT — caught three dead bindings (an unused adminToken fixture arg, an unused
    `api` arg, an unused JPEG_MAGIC import), all removed.
  - no-explicit-any OFF — all test code; `any` is the honest type for an untyped res.json() body or
    a page.evaluate() return.
  - no-empty-pattern OFF — Playwright's dependency-free fixtures are `async ({}, use) => {}`.

Refactor: `const BASE = process.env.E2E_FRONTEND_URL ?? '...'` was redeclared verbatim in 23
files — extracted to helpers/env.ts and imported, so a port/scheme change is one edit not a sweep.

Then `prettier --write`. Verified: eslint clean, tsc clean, prettier clean, desktop suite 210
passed / 1 skipped. (One mobile spec flaked once under retries:0 — a pre-existing cross-test
reflow-timing vector from the flakiness audit, not this change: the each-key edit is stable across
16 isolated runs and a clean full mobile re-run.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 20:45:59 +02:00

291 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# EventSnap E2E Suite
Playwright-driven end-to-end tests for the EventSnap stack. The suite spins
up an isolated docker-compose stack on ports `:3101` (Caddy → frontend +
backend) and `:55432` (Postgres), and exercises the SvelteKit frontend
against a real Rust backend with rate limits and quotas disabled.
**Phases 1, 2, and 3-mobile-gestures are landed**:
- **Phase 1** — happy-path coverage of every documented user journey, plus a
smoke matrix across nine browser/UA profiles to catch engine-level
divergences.
- **Phase 2** — adversarial inputs (XSS, SQL-injection, JWT forgery, MIME
spoofing, oversize, brute-force) and browser chaos (storage purge,
offline/slow-3G, multi-tab, clock skew, no-JS, quota exhaustion).
- **Phase 3 (gestures only)** — touch-target audit, safe-area structural
check, long-press → ContextSheet, double-tap → like, viewport reflow,
plus `test.fixme` stubs for planned gestures (lightbox swipe, swipe-down
dismiss, pull-to-refresh).
Phase 3 real-device compat (Android emulator + Samsung Internet via
`connectOverCDP`, BrowserStack), visual regression, and a11y audits are
sketched in the **Roadmap** at the bottom.
## Quickstart
```bash
cd e2e
npm install
npm run install:browsers # one-time: ~500 MB across chromium/firefox/webkit
# 1. Boot the test stack (rebuilds backend + frontend Docker images)
npm run stack:up
# 2. Wait ~20s for migrations + warmup, then run tests
npm run test:e2e # full Phase 1 suite on chromium-desktop
npm run test:e2e:smoke # cross-UA smoke matrix (~9 projects × 1 test)
npm run test:e2e:ui # interactive Playwright UI mode
# 3. After: tear the stack down (deletes volumes)
npm run stack:down
```
The CI workflow at `.github/workflows/e2e.yml` runs both jobs on every PR.
## What's tested
Every spec covers a journey from [`docs/USER_JOURNEYS.md`](../docs/USER_JOURNEYS.md)
or a security/chaos scenario. One folder per area:
| Folder | Phase | Journeys / Topic | Tests | Notes |
| ------------------------- | ----- | ----------------------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------- |
| `specs/01-auth/` | 1 | §1, §2, §3, §11, §15 | 13 | Join, recover, PIN lockout, admin login, leave event. |
| `specs/02-upload/` | 1 | §5, §6, §18 | 5 | Gallery picker, multi-file, rate-limit, admin toggle. |
| `specs/03-feed/` | 1 | §7, §8, §17 | 5 | Like/comment SSE, filter chips, SSE reconnect. |
| `specs/04-host/` | 1 | §9 | 5 | Event lock, ban/unban, role change. |
| `specs/05-admin/` | 1 | §11, §16 | 11 | Config validation, foundational auth guards, stats. |
| `specs/06-export/` | 1 | §12 | 3 | Status, release, download stub. |
| `specs/__smoke/` | 1 | (matrix) | 1 × 9 UAs | `@smoke`-tagged happy-path on every UA project. |
| `specs/07-adversarial/` | **2** | Input attacks, file upload boundaries, JWT forgery, brute-force, deep authorization, small DDoS | ~40 | See breakdown below. |
| `specs/08-browser-chaos/` | **2** | Storage purge, IndexedDB, offline/slow-3G, multi-tab, no-JS, clock skew, quota | ~20 | See breakdown below. |
| `specs/09-mobile/` | **3** | Touch-target audit, safe-area, long-press, double-tap, viewport reflow, fixme stubs | 23 | Runs only on `chromium-mobile` (Pixel 7 viewport). See below. |
### Phase 2 — adversarial (`specs/07-adversarial/`)
- **`xss-injection.spec.ts`** — 13 tests. Six XSS payloads × display-name path
- four SQLi patterns + length/encoding edge cases (NUL byte, RTL override,
caption overflow). Asserts `window.__xssFired` never gets set and no
`dialog` event fires.
- **`ui-rendering.spec.ts`** — 2 tests. Belt-and-braces: even when a script-
payload sits in localStorage as the user's display name, rendering through
`/account` keeps it as text.
- **`file-upload-attacks.spec.ts`** — 9 tests. ELF body claimed as JPEG,
oversize image vs `max_image_size_mb`, zero-byte, missing file field,
path-traversal filename, NUL filename, `application/*` declared category
bypass, SVG-with-script.
- **`auth-tampering.spec.ts`** — 8 tests. `alg:none` forging admin role,
signature tamper, payload tamper with original signature, logged-out
session reuse, header without `Bearer `, missing Authorization,
PIN brute-force lockout, admin password brute-force (documented finding —
no lockout today, bcrypt cost is the only defense).
- **`authorization-deep.spec.ts`** — 6 tests. Cross-user comment delete,
banned user across like/comment/feed-read, host→admin escalation attempts.
- **`ddos.spec.ts`** — 4 small-scale abuse tests. 20 parallel /join, 10 MB
comment body, 10 concurrent SSE streams, malformed JSON.
### Phase 2 — browser chaos (`specs/08-browser-chaos/`)
- **`storage-purge.spec.ts`** — 5 tests. `localStorage.clear()` mid-session,
cookies cleared (JWT in localStorage still works), sessionStorage cleared,
admin force-relogin, PIN intentionally survives clearAuth.
- **`indexeddb.spec.ts`** — 2 tests. Drop all IDB databases mid-session;
stub IDB to undefined before navigation.
- **`offline-network.spec.ts`** — 4 tests. `setOffline(true)` → reconnect,
slow-3G via `page.route` delay, intermittent 503s, 429 from server (no
infinite retry storm).
- **`multi-tab.spec.ts`** — 3 tests. Same user two tabs, two users two
contexts (storage isolated), logout in tab A doesn't sync to tab B
(documented gap).
- **`environment.spec.ts`** — 5 tests. JS disabled, localStorage quota
exhausted, hostile CSS hiding nav, clock skew ±1h / -2d.
Pending tests covering features that need a Node-side multipart upload helper
are marked `test.fixme` and will activate when that helper lands.
## Browser & UA matrix
| Project | Engine | UA / Device | Why |
| --------------------- | -------- | ----------------------------------- | --------------------------------------------- |
| `chromium-desktop` | Chromium | Desktop Chrome | Baseline. Full suite runs here. |
| `chromium-pixel7` | Chromium | Pixel 7 device descriptor | Chrome Android. |
| `chromium-galaxy-s22` | Chromium | Galaxy viewport + Samsung phone UA | Chrome on Samsung hardware. |
| `samsung-internet` | Chromium | Galaxy viewport + SamsungBrowser UA | **Tier-A Samsung Internet baseline.** |
| `edge-android` | Chromium | Pixel viewport + EdgA UA | Edge Mobile (Blink-based). |
| `chrome-ios` | Chromium | iPhone viewport + CriOS UA | Chrome iOS (actually WebKit, but UA differs). |
| `webkit-iphone` | WebKit | iPhone 14 Pro | Real iOS Safari engine. |
| `firefox-android` | Firefox | Pixel viewport + Firefox Android UA | Gecko engine. |
| `firefox-desktop` | Firefox | Desktop Firefox | FF-specific quirks. |
Only the `@smoke` happy-path runs across all projects (controlled by
`grep` in `playwright.config.ts`). The full Phase 1 suite is
`chromium-desktop`-only by default to keep CI under 15 min.
### Samsung Internet — three escalation tiers
Samsung Internet ships on every Galaxy phone (~5% of mobile traffic in DE).
It's **Blink-based**, so Tier-A catches ~90% of regressions. Real Samsung
divergences (Smart Switch save-data mode, dark-mode injection, custom
autoplay, in-browser ad blocking) are only reproducible at Tier B+:
- **Tier A** _(this repo, free, in CI)_: Playwright Chromium with the
Samsung Internet user-agent + Galaxy viewport. See the `samsung-internet`
project in `playwright.config.ts`.
- **Tier B** _(free, manual, future)_: Android Studio emulator on Linux →
install Samsung Internet APK → enable `--remote-debugging-port=9222`
`chromium.connectOverCDP('http://localhost:9222')`. Setup docs live in
`docs/samsung-emulator.md` (to be written).
- **Tier C** _(paid, optional)_: BrowserStack or LambdaTest cloud devices.
Real Galaxy S22/S23 hardware via Playwright's cloud integration.
## Test isolation
Every test runs against a **freshly truncated database**:
1. `global-setup.ts` waits for `/health`, logs in admin, and disables every
rate-limit and quota toggle via `PATCH /admin/config`.
2. The auto-fixture `truncate` in `fixtures/test.ts` calls
`POST /api/v1/admin/__truncate` before every test.
3. The truncate endpoint is only registered when the backend is started
with `EVENTSNAP_TEST_MODE=1` (see `backend/src/main.rs` and
`backend/src/handlers/test_admin.rs`). Production builds return 404.
Single-worker by design (`workers: 1` in the config). Per-worker isolated
DBs are a Phase-2+ change.
## Architecture
```
e2e/
├── docker-compose.test.yml # Isolated test stack: db :55432, caddy :3101
├── Caddyfile.test # Proxies /api/* /media/* /health to backend
├── playwright.config.ts # UA matrix + smoke grep
├── global-setup.ts # admin login, rate-limit disable
├── global-teardown.ts # (no-op; use `npm run stack:down`)
├── fixtures/
│ ├── api-client.ts # Typed wrapper over /api/v1/*
│ ├── db.ts # Direct Postgres escape hatch (locked-PIN, etc.)
│ ├── test.ts # Central test.extend (guest, host, signIn fixtures)
│ └── media/ # sample.jpg, sample.mp4, not-an-image.jpg
├── helpers/
│ ├── sse-listener.ts # Async SSE iterator with waitForEvent()
│ ├── storage-helpers.ts # localStorage/sessionStorage helpers
│ └── fake-media.ts # Camera permissions (Chromium only)
├── page-objects/
│ ├── join-page.ts # /join
│ ├── recover-page.ts # /recover
│ ├── admin-login-page.ts # /admin/login
│ ├── feed-page.ts # /feed + bottom nav
│ ├── upload-sheet.ts # UploadSheet.svelte + /upload
│ ├── lightbox.ts # LightboxModal.svelte
│ ├── account-page.ts # /account
│ ├── host-dashboard.ts # /host
│ ├── admin-dashboard.ts # /admin
│ └── export-page.ts # /export
└── specs/
├── __smoke/ # @smoke cross-UA matrix (1 spec)
├── 01-auth/
├── 02-upload/
├── 03-feed/
├── 04-host/
├── 05-admin/
└── 06-export/
```
## Debugging a failure
- `npm run test:e2e:ui` — interactive UI with time-travel and selector probe.
- `npm run test:e2e:headed` — watch the browser run live.
- `npm run test:e2e:debug` — Playwright inspector with breakpoints.
- `npm run stack:logs` — tail backend + Postgres logs during a failure.
- `playwright-report/index.html` — opens the HTML report (auto-generated on every run).
- Trace files (`test-results/**/trace.zip`) drag-and-drop into `https://trace.playwright.dev`.
## Conventions
- **One assertion per `expect`**. Bundling multiple expects in one statement
loses the line-level failure context.
- **Wait on data, not time**. Use `expect.poll` for DB checks; never `waitForTimeout` in production specs.
- **`@smoke` tag** on each suite's happiest path so the matrix run stays under 2 min.
- **`test.fixme`** for features that need infrastructure not yet built (Node-side multipart upload helper, real video fixtures, etc.). Fixme tests don't fail the suite but show up in the report.
- **Page objects own selectors**. Specs never use raw locators.
- **German text in assertions** is fine — it's not going to change frequently. When it does, the page object is the only file to update.
## Roadmap
### Phase 2 — Adversarial & browser chaos ✅ landed
See the **What's tested** table above and the per-file breakdown.
Known findings surfaced (documented in tests, not silent failures):
1. `/admin/login` has no rate-limit or lockout — bcrypt cost is the only defense.
2. `localStorage` 'storage' event is not listened for, so logout in tab A
doesn't synchronously sign out tab B (the next 401 from any API call
clears it).
3. SVG uploads currently pass the magic-byte check (depends on `infer`'s
detection coverage) — consider adding `X-Content-Type-Options: nosniff`
- CSP on `/media/*` if SVGs are ever expected as user content.
### Phase 3 — Mobile gestures (`specs/09-mobile/`) ✅ landed
Runs only on the `chromium-mobile` project (Pixel 7 device descriptor with
`hasTouch` and `isMobile`). The `chromium-desktop` project explicitly
ignores this folder via `testIgnore` in [playwright.config.ts](playwright.config.ts).
- **`touch-targets.spec.ts`** — 4 tests. Audits ≥ 44×44 px on bottom nav,
FAB, join submit, admin-login submit, PIN-modal buttons. Uses
`expect.soft` so a single failure surfaces the actual bounding-box
dimensions instead of stopping the suite.
- **`safe-area.spec.ts`** — 4 tests. Asserts `env(safe-area-inset-bottom)`
is present in the inline style of every bottom-anchored UI element
(bottom nav, UploadSheet, ContextSheet), and that the nav stays flush
with the viewport bottom on a no-notch emulated device.
- **`gestures-longpress.spec.ts`** — 3 tests. A 600 ms hold on a
FeedListCard opens the ContextSheet; a 200 ms tap does not; the
click-suppression logic prevents the lightbox from also opening at
pointer-up. Driven via `page.mouse.down/up` because the `longpress`
action listens for pointer events (mouse/touch/pen unified).
- **`gestures-doubletap.spec.ts`** — 2 tests. Double-tap on a feed card
image button records a like; double-tap inside the lightbox triggers
the heart-burst animation and records a like. Assertions read the like
count back via `/api/v1/feed` so they don't couple to specific badge
markup.
- **`viewport-reflow.spec.ts`** — 5 tests. Portrait, landscape, narrow
(320×568), phablet (480×1024) — each asserts the bottom nav is
visible, the FAB stays roughly centered, and there's no horizontal
overflow on `<html>`. Plus a rotation test that confirms auth survives
a viewport resize.
- **`planned-gestures.spec.ts`** — 5 **`test.fixme`** stubs documenting
the contracts for gestures from journey §17 that aren't shipped yet
(lightbox swipe L/R, swipe-down to dismiss UploadSheet,
pull-to-refresh, long-press on a comment). Flip `test.fixme` to `test`
when wiring each gesture.
#### Driving gestures: the `helpers/touch.ts` module
- `longPress(page, locator, durationMs)` — holds the pointer down for
the duration. Default 600 ms beats the action's 500 ms threshold.
- `doubleTap(page, locator)` — two `mouse.down/up` pairs within the
`doubletap` action's 300 ms window.
- `swipe(page, from, to, steps)` — gradual mouse-driven move (used by
the fixme stubs once swipe gestures land).
- `inlineStyle(locator)` / `computedStyle(locator, prop)` — read raw
`style` attributes (where `env(...)` strings live) and computed
values.
### Phase 3 — Real-device compat & visual / a11y (not landed)
- Long-press own/other post, swipe lightbox L/R, swipe-down dismiss, pull-to-refresh, double-tap like.
- Safe-area inset visual diff on iPhone notch.
- Touch-target ≥ 44 px audit.
- Tier B Samsung Internet via `connectOverCDP` on Android Studio emulator.
- Tier C BrowserStack integration (paid, optional).
- `@axe-core/playwright` accessibility audits.
- Visual regression with screenshot diffs.
### Out of scope (handed to other tools)
- Load testing → k6 / Vegeta.
- API contract testing → backend `cargo test` integration tests.
- Static asset auditing → Lighthouse CI.