9b9092926943d256a86f35ec84f7aab6f84353bc
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9b90929269 |
test(e2e): re-point the PIN lockout specs at the property that matters
Three specs asserted the OLD policy — that three wrong PINs lock an account — which is exactly the behaviour the previous commit removed, because that threshold sat below the per-(IP, name) throttle ceiling and so let any single IP lock any guest whose display name is readable off the feed. Rewritten to assert the distinction the fix introduces, which a status code alone cannot show: both tiers answer 429, but only the account lock costs the VICTIM. The new specs read the row via db.isPinLocked rather than the response, so: - one IP hammering /recover is throttled and the account stays UNLOCKED; - a distributed guesser (counter preloaded via db.setFailedPinAttempts, since no single source can reach the threshold any more) still trips the lock, and it holds even against the correct PIN; - concurrent wrong PINs are all counted — the atomicity property the old parallel test was really about, now asserted on the counter instead of inferred from a 429 that the throttle could equally have produced. The UI spec asserts the user-visible half: after four wrong PINs Dave can still get into his own account. It also now types the PIN digit by digit rather than filling and clicking, because the 4th digit auto-submits (pin-auto-submit.spec.ts) and doing both raced the button's disabled state. The adversarial spec enables rate_limits_enabled for its own run — it is off by default in this environment, so without that the throttle tier would silently not be exercised — and restores it in afterEach so it cannot leak into other specs sharing the stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
d2ad560df2 |
test(security): prove admin-login rate limit; make its toggle settable
The admin-login IP rate limit already existed (auth/handlers.rs: 5/min/IP, gated by
`rate_limits_enabled` + `admin_login_rate_enabled`, both default true in prod) — the earlier
"no rate-limit or lockout" note was wrong. What was missing was any TEST of it: the e2e
reseed sets `rate_limits_enabled=false`, so the old test observed all-401 under a disabled
limiter and "documented" the opposite of reality.
Replace that test with three that enable the limiter and prove it, mutation-verified
(deleting the check in the handler fails the first two):
- a burst of wrong passwords from one IP starts returning 429 (first is a normal 401, so
the password path ran and the limiter had budget; none is ever 200)
- once throttled, even the CORRECT password is refused — isolates the RATE LIMIT from the
password check (this is the assertion that dies if the limiter is removed)
- with `admin_login_rate_enabled=false` the same burst is NOT limited (the toggle gates it)
Two fixes this surfaced:
- `admin_login_rate_enabled` and `recover_rate_enabled` are read by their handlers but
were missing from patch_config's BOOL_KEYS allowlist — the switches existed in code and
could never be flipped. Added both.
- Test isolation: exhausting the admin-login limiter locks out the NEXT test's truncate
fixture, which must itself call admin_login before it can reset anything. An afterEach
disables the toggle via patchConfig (JWT-based, not rate-limited) so the next login is
clear; truncate then wipes the counter map. Runs on failure too.
Full desktop suite 201 passed / 1 skipped; backend 56 tests; clippy clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
02971f3186 |
test(e2e): de-vacuum security tests; add quota, authz-sweep, keepsake-regen coverage
An audit found tests that pass on broken code. The dominant pattern: fire a security
assertion at the all-zeros UUID and accept [403, 404] — the 404 comes from the resource
LOOKUP, not the guard, so the guard can be deleted and the test still passes. Repaired to
use real resources and demand exactly 403:
- banned-user cannot like / comment (the only coverage of those ban invariants)
- host cannot promote a real guest (or self) to admin — asserts nobody's role changed
- IDOR comment-delete already used a real resource; kept
Also inverted recovery.spec's "unknown name → nicht gefunden" test: that asserted the exact
account-enumeration oracle the F4 fix removed, so restoring the vuln would have made it
pass. Now: an unknown name must be byte-identical to a wrong PIN.
New coverage for paths that ran in production but in zero tests:
- quota.spec.ts: storage quota enforcement (413 over-limit, atomic increment under two
uploads held mid-body so both carry a stale total=0 — the real race; a naive Promise.all
version was itself vacuous and is documented as such). Proven to fail without the guard.
- authz-sweep.spec.ts: table-driven guest→403 / host→403 over ALL 19 privileged routes +
anonymous + __truncate. No live hole found; the whole surface is now locked.
- ban / unban / host-comment-delete AFTER release regenerate the keepsake (data-loss
paths that were dead under test); comment-delete mid-build doesn't strand the ZIP.
Lower-severity de-vacuuming: 10 MB comment test hit Caddy's 502 before the real 500-char
cap (now seeds a real upload, 501→400 / 500→201, mutation-verified); XSS name payloads
shortened under the 50-char cap so they actually store+render; ui-rendering XSS test now
proves the payload rendered before asserting no <b>; export page-object locators fixed to
the real "Download" label with a positive empty-state anchor; avatar palette spread test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
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>
|