Commit Graph

12 Commits

Author SHA1 Message Date
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
fabi
d643256f36 fix(rereview): close residual export finalize↔flip double-race at reopen
Adversarial re-review of df275bb found the two-guard ready-flip fix still left a
narrow double-race open, plus test-hygiene drift from the banUser param removal.

MED — residual stale-keepsake race the `export_released_at` guard alone missed
  The flip relied on two guards defeating two clearers: the `release_seq` EXISTS
  check (vs a re-release bump) and `export_released_at IS NOT NULL` (vs open_event's
  clear). But release_gallery re-arms `export_released_at = NOW()` and bumps
  `release_seq` as SEPARATE statements, so a stale worker's flip landing in the gap
  between them satisfies BOTH guards (released re-armed, seq not yet bumped) and
  resurrects a pre-reopen keepsake — the next re-release then skips regeneration and
  serves an archive missing the reopen-window uploads.

  Root-cause fix: `open_event` now bumps every `export_job.release_seq`, making a
  reopen a supersession point symmetric with a re-release. A worker that captured the
  pre-reopen seq can never match its `release_seq`-guarded finalize/flip again,
  regardless of when the re-release re-arms `export_released_at`. The released-anchor
  guard stays as defense-in-depth. Added a deterministic e2e asserting the reopen
  bumps the seq (the invariant that closes the race without depending on
  sub-millisecond worker timing).

LOW
  - Gate the `export-progress: 100` SSE + `prune_stale_export_files` on the ready-flip
    actually flipping (rows_affected > 0). A superseded worker no longer advertises a
    misleading 100% or prunes on a fresh generation's behalf.
  - moderation.spec.ts: the two ban tests had become byte-identical after the
    hide_uploads param removal; drop the one whose "hide_uploads=true" title no longer
    matched what it exercised, keep the accurate "always hides" test.
  - host-dashboard page-object: drop the dead `banUser(hideUploads)` param + its
    checkbox branch (the UI no longer renders that checkbox — a latent hang trap).
  - sse-eviction.spec.ts: rename the test whose title referenced the removed flag.

Verified: backend 40 tests, e2e 156 passed / 1 skipped on chromium-desktop
(incl. the new reopen-supersession regression).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 22:05:06 +02:00
fabi
df275bbefa fix(rereview): close export-flip race + queue-dedup reload regression
Adversarial re-review of the persona-audit + audit-followup rounds (6411747..)
found one HIGH and one MED regression plus LOW gaps. All fixed with coverage.

HIGH — export stale-keepsake resurrected by an open_event race
  A reopen landing in the window between a *current* export worker's finalize_job
  and its ready-flag flip cleared export_released_at + the ready flags but left the
  export_job row `done` at the same release_seq. The seq-guarded flip then still
  matched and re-set export_{zip,html}_ready=TRUE on a pre-reopen snapshot; the next
  re-release read that stale TRUE and skipped regeneration (`if ready { continue }`),
  serving a keepsake missing every upload from the reopen window — the exact data
  loss migration 012 exists to prevent. Both ready-flip UPDATEs are now additionally
  anchored on `export_released_at IS NOT NULL`, so a landed reopen makes the flip a
  no-op and the re-release regenerates cleanly.

MED — queue dedup broke for reloaded items
  loadQueue rebuilt QueueItems from IndexedDB without copying lastModified, which the
  new addToQueue dedup keys on. A file re-selected after a page reload / PWA relaunch
  missed the duplicate check and uploaded twice. Rehydration now carries lastModified
  (extracted to a pure, tested entryToQueueItem helper).

LOW
  - diashow: clear the upload-processed debounce timer in onDestroy (no stray
    post-unmount /feed fetch).
  - USER_JOURNEYS §9.5: document the reconnect-delta ban replay (hidden_user_ids /
    uploads_hidden_at, migration 013), not just the live user-hidden SSE.
  - e2e api-client: drop the misleading hide_uploads param from banUser — the backend
    takes no body and always hides; strip the dead boolean at all call sites.

Tests
  - Extract isReversibleLock (the terminal-403 KEEP-vs-PURGE-blob discriminator) into a
    pure exported helper + unit tests, so the data-loss-critical branch is covered
    without an XHR harness.
  - entryToQueueItem unit tests lock the lastModified-carry regression.
  - Document the export flip-race guard in the reopen/re-release spec (the sub-ms
    finalize↔flip interleave isn't deterministically forceable with fast fixtures;
    covered by the SQL guard + the end-to-end completeness test).

Verified: backend 40 tests, frontend 44 unit tests, svelte-check 0 errors,
e2e 156 passed / 1 skipped on chromium-desktop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 21:47:44 +02:00
MechaCat02
641174717c fix(user-flow): close offline-queue, export, session, ban & realtime gaps
Some checks failed
E2E / Playwright E2E (chromium-desktop) (push) Failing after 6m52s
E2E / Cross-UA smoke matrix (push) Failing after 3m50s
Comprehensive user-flow review across guest/host/admin roles, then fixes with
e2e regression guards. Highlights:

- Offline upload queue auto-resumes on reconnect: network errors keep items
  pending (not error), 4xx are terminal (no infinite retry), quota exhaustion
  returns a distinct 413; queue cap + dedup.
- Export lifecycle: release atomically locks uploads; reopen invalidates and
  re-release regenerates the keepsake; workers claim their job atomically so a
  reopen->re-release can't corrupt the ZIP; startup re-spawns interrupted
  exports.
- Sessions slide on activity (no 30-day cliff); JWT expiry deferred to the
  revocable session row; sign-out-everywhere + revoke-on-PIN-reset.
- Ban always hides content (v_feed / find_visible_media / export filter
  is_banned) but stays a read-only ban per USER_JOURNEYS §10 — sessions are
  not revoked, read access + keepsake download preserved.
- Realtime: server-clock SSE delta cursor; event-closed/opened drive the UI
  live; feed_delta rate-limited; like returns {liked, like_count} to fix
  multi-device drift; lightbox live comments; diashow delta backfill.
- Forgotten-PIN in-app request flow; simultaneous same-name join returns 409;
  quota increment is transactional; operator floor.

Adds e2e/specs/10-flow-review/ (offline resume, export integrity, deterministic
anti-race guard) and updates existing specs for the new contracts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:05:37 +02:00
fabi
faf7a2504a fix(audit): restore broken upload pipeline + role-based E2E audit fixes
A comprehensive role-based E2E audit (guest/host/admin, across browser
sessions) surfaced one critical and several smaller issues; this addresses
them and hardens the tests that missed them.

Critical
- The client upload pipeline was fully broken: the IndexedDB v1->v2 upgrade
  opened a *new* transaction inside the upgrade callback, which throws during
  a version-change transaction and aborted the whole upgrade, leaving the
  queue object store uncreated -- so no UI upload ever fired. Reuse the
  version-change transaction the callback provides, and bump the DB to v3 with
  a contains() guard so installs already corrupted by the shipped bug
  self-heal on next load. Re-enabled the previously-fixme'd UI upload E2E test.

High / Medium
- Event lock is uploads-only again: likes, comments and browsing stay open
  while the event is locked (USER_JOURNEYS 9.3 / FEATURES) -- it was wrongly
  freezing social interaction. Updated the event-lock spec accordingly.
- get_original now excludes soft-deleted and ban-hidden uploads, and direct
  /media/originals/** serving is blocked, so a hidden user's originals can no
  longer be pulled by UUID (all originals go through the checked alias).
- The upload handler reads the file field with an early-abort size cap chosen
  from the declared content-type, instead of buffering the entire body before
  the size check.

Low
- unban_user mirrors the ban role guard (a host can no longer unban a
  host/admin banned by an admin).
- reset_user_pin's UPDATE is event-scoped.
- Admin login returns and stores a real identity (user_id + display name)
  instead of a blank session.
- The host user list no longer renders target-actions (ban/promote/demote/PIN)
  on the caller's own row, where the backend always rejected them.
- /diashow gains a client-side auth guard like the other protected routes.
- The join page shows the event name via a new public GET /api/v1/event.

Verified: backend cargo build clean, frontend svelte-check 0 errors, full
Playwright E2E suite 144 passed / 1 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 07:28:27 +02:00
fabi
0ed97f45cf fix(review-2): address re-review — close two blocker regressions + harden tests
The round-2 re-review (full suite green) found two real defects the passing
tests missed, both now fixed and covered:

B1 — banned users could still edit/delete their OWN content: edit_upload,
     delete_upload, delete_comment skipped the is_banned guard that upload/
     like/comment got (round-2 removed the global extractor block and missed
     these three). Added the guard (using auth.is_banned — no extra query).
     The moderation ban test now asserts 403 on DELETE upload, PATCH upload,
     and DELETE comment for a banned owner (+ upload survives) — it previously
     only exercised POST /upload, which is why the gap shipped green.

B2 — H3 live-eviction was dead code: 'user-hidden' and 'comment-deleted' were
     missing from KNOWN_EVENTS, so EventSource never listened and the handlers
     never fired. Added both. New browser-driven sse-eviction test asserts a
     hidden user's card actually evicts from an open feed with no reload — the
     backend-only SseListener checks couldn't catch this.

Minor items folded in:
- admin dashboard bounces a mid-session-demoted admin to /feed via live
  /me/context (mirrors the host page) instead of a dead error screen.
- feed "new posts" pill cleared on any full refresh (pull-to-refresh no longer
  strands it).
- corrected the stale sse.rs comment (banned users may hold a read-only stream).

Verified: backend 35 unit tests; svelte-check 0 errors; e2e 04-host + comment-ui
+ export-leak 13/13 on chromium (incl. the two new regression tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:45:57 +02:00
fabi
80179357a7 fix(review-2): high — read-only ban model, failed-compression cleanup, live SSE eviction
H1: corrected a round-1 over-reach. Bans are read-only per USER_JOURNEYS §10:
    the base AuthUser extractor no longer rejects banned users (they keep read
    access); Require{Host,Admin} and write handlers enforce the ban via
    auth.is_banned → 403 (incl. self-unban). Demoted hosts still lose powers
    immediately (live DB role) and are bounced off the dashboard. Also fixed the
    login/recover redirect regression on unauthenticated 401.

H2: failed compression now self-heals instead of leaving a permanent broken
    card — refund the quota, delete the orphaned original, soft-delete the row;
    the frontend toasts the uploader and evicts the card on upload-error.

H3: self-delete, ban-with-hide (user-hidden), and comment-deletes now broadcast
    SSE; feed, diashow, and lightbox evict the affected content live instead of
    waiting for a reload. sse-eviction.spec.ts covers it.

Riders: feed/+page.svelte also carries the medium truncated-delta pill; host.rs
also carries the low atomic release_gallery claim; admin/+page.svelte also drops
the dead compression_concurrency control (medium).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:19:43 +02:00
fabi
41ac742af8 test+docs: tighten H1 assertions, cover secret/XFF edges, document boot & SSE-ban
Re-review follow-ups (non-blocking quality items):

Tests:
- moderation H1 specs now assert exact `→ 403` instead of bare .rejects.toThrow(),
  so a spurious 500 can no longer masquerade as "revocation worked".
- config: added case-insensitivity (upper/mixed-case placeholder) and the
  len==32/31 boundary cases for validate_secrets.
- rate_limiter: added the trailing-comma empty-entry case for client_ip (must
  fall back, not return "").
  (Backend unit tests: 35 pass.)

Docs:
- README: note that with APP_ENV=production a placeholder .env makes the app
  refuse to boot and Caddy wait unhealthy — reason is in `docker compose logs app`.
- SECURITY-BACKLOG + sse.rs comment: document that a mid-session ban does not
  tear down an already-open SSE stream (new tickets are blocked; low blast radius).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 07:14:33 +02:00
fabi
91ff961850 fix(security): medium/low — event-lock, atomic config, a11y, diashow, hygiene
- social: likes/comments rejected on a closed event (e2e proven).
- admin: patch_config validates-then-writes atomically; char-based length.
- hashtag/comment models: atomic tag writes in edit + comment paths.
- Modal: inert the background (modal-inert action) for screen readers.
- sse.ts: idempotent visibilitychange listener (no duplicate reconnects).
- diashow: videos advance on `ended` (transitions + page wiring).
- docs/hygiene: README clone-case fix; removed stale committed .env.test and
  gitignored it; docker-compose.dev.yml tidy.

Left documented as accepted-risk per plan: PIN-persist-after-logout,
UUID-reachable hidden uploads, DECISION-media-auth.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:25:58 +02:00
fabi
f08d858281 fix(security): high — live authz, XFF, SSE buffering, atomicity, pagination
H1: auth extractor re-reads the live user row — trusts the DB role (not the
    JWT claim) and rejects banned users mid-session. e2e proves a demoted
    host loses powers and a banned host is locked out and can't self-unban.
H2: client_ip takes the right-most XFF hop (the one Caddy appends); spoofed
    left-most entries are ignored, restoring IP-based throttles.
H3: Caddy excludes /api/v1/stream from `encode` so SSE isn't buffered.
H4: upload — quota increment + row insert + hashtag links now one txn.
H5: feed keyset pagination tiebroken on (created_at, id) + composite index
    (migration 010); feed_delta bounded with LIMIT.
H7: LightboxModal keys its comment load off upload.id, ending the
    SSE-driven refetch storm.
H8: CSP via SvelteKit kit.csp (svelte.config.js) hardens the localStorage
    token model against injection.

Migration 010 also adds the latent comment/comment_hashtag indexes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:25:50 +02:00
fabi
498b4e256b fix(security): critical — repair PIN reset + enforce real prod secrets
C1: reset_user_pin wrote to a non-existent column (pin_failed_attempts);
    the real column is failed_pin_attempts, so every PIN reset 500'd. Fixed
    the column name; new e2e (pin-reset.spec.ts) proves a reset returns a
    usable PIN and the target can recover with it.

C2: config.rs::validate_secrets now rejects placeholder-ish secrets
    (change_me/dev_secret/placeholder), enforces len>=32 in prod, and
    requires a real ADMIN_PASSWORD_HASH. docker-compose.yml sets
    APP_ENV=production so the guard actually runs. Corrected the false
    "fixed" claim in SECURITY-BACKLOG.md. .env.example documents the rule.

Riders in these files (documented here since git can't split hunks):
- host.rs also carries the event-scoped ban_user fix and the
  close_event/open_event no-op broadcast guard (medium).
- docker-compose.yml also adds ORIGIN (H6), app/frontend healthchecks with
  Caddy waiting on health, and per-service memory limits (medium).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:25:39 +02:00
MechaCat02
e42d8a92a1 feat(e2e): Playwright suite — 134 tests across 9 spec areas + UA matrix
Adds an end-to-end Playwright test suite under e2e/ that spins up an
isolated docker-compose stack (Postgres :55432, Caddy :3101, backend with
EVENTSNAP_TEST_MODE=1, SvelteKit adapter-node frontend) and exercises the
SvelteKit app against the real Rust backend.

Phase 1 — happy paths covering every documented USER_JOURNEYS.md flow:
  01-auth/      join, recover, admin login, leave event, PIN lockout
  02-upload/    gallery picker (API path), rate-limit + admin toggle
  03-feed/      like/comment SSE, filters, SSE reconnect on visibility
  04-host/      event lock API, ban/unban, promote
  05-admin/     config validation, foundational authz guards, stats
  06-export/    /export status + download stub
  __smoke/      cross-UA happy-path (runs on every UA project)

Phase 2 — adversarial + browser chaos:
  07-adversarial/  XSS payloads (6 × display name path), SQLi shapes,
                   length / encoding / RTL override / NUL byte;
                   file-upload boundaries (ELF body claimed as JPEG,
                   oversize vs max_image_size_mb, zero-byte, NUL
                   filename, path-traversal, SVG-with-script);
                   JWT alg:none, signature/payload tamper, expired
                   session, PIN brute-force (serial + parallel),
                   admin password brute-force; deep authz (cross-user
                   delete, banned user across like/comment/feed-read,
                   host→admin escalation); small-scale DDoS (20× /join,
                   10MB comment body, 10 concurrent SSE).
  08-browser-chaos/ localStorage / sessionStorage / cookie purge,
                    IndexedDB drop mid-session, offline → reconnect,
                    slow-3G, 503 flakes, 429 with no retry storm,
                    multi-tab same/different user, no-JS, hostile CSS,
                    clock skew ±1h / -2d, localStorage quota exhausted.

Phase 3 — mobile gestures (runs only on chromium-mobile / Pixel 7):
  09-mobile/    touch-target ≥44px audit, env(safe-area-inset-bottom)
                structural check, long-press (FeedListCard → ContextSheet,
                quick-tap negation, click-suppression), double-tap
                (feed card like + lightbox heart-burst, via synthetic
                pointer events to bypass the first-tap-fires-click trap),
                viewport reflow (portrait/landscape/narrow/phablet),
                plus fixme stubs documenting planned gestures (swipe
                lightbox L/R, swipe-down dismiss, pull-to-refresh,
                long-press-comment).

Cross-UA matrix (chromium-engine projects run @smoke only):
  chromium-pixel7, chromium-galaxy-s22, samsung-internet (Samsung UA
  emulation on Galaxy viewport), edge-android, plus webkit-iphone,
  chrome-ios, firefox-android, firefox-desktop — the latter four need
  libavif16 on the host (Playwright dep) but the configs are in place.

Infrastructure:
  - fixtures/test.ts central test.extend (api, db, adminToken, guest,
    host, signIn). Per-test DB truncate via the dev-only POST
    /admin/__truncate route, gated by EVENTSNAP_TEST_MODE=1.
  - helpers/sse-listener.ts, helpers/upload-client.ts (Node-side
    multipart for adversarial file-upload tests + JPEG/PNG/ELF magic
    constants), helpers/touch.ts (longPress / doubleTap / swipe /
    inlineStyle / computedStyle).
  - 10 page objects covering every route + UploadSheet/Lightbox.
  - global-setup waits for /health, logs in admin, disables every
    rate-limit and quota toggle.
  - .github/workflows/e2e.yml: PR check runs chromium-desktop + the
    smoke matrix in parallel, uploads playwright-report/ and traces on
    failure.

Findings the suite surfaces as live `[finding]` warnings (not silenced):
  1. /admin/login has no rate-limit or lockout (bcrypt cost only).
  2. PIN-attempt counter races under parallel /recover requests.
  3. Zero-byte uploads pass /api/v1/upload.
  4. SVG-with-script can pass the magic-byte check (consider CSP +
     X-Content-Type-Options on /media/*).

Stack-internal docs live in e2e/README.md (UA tier table, Samsung
Internet escalation tiers A/B/C, debugging tips, roadmap).

Final tally: 134 passed / 0 failed / 9 skipped (test.fixme stubs for
not-yet-shipped gestures and one UI-upload-flow investigation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:37:11 +02:00