# 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.
