test(e2e): make nine red specs assert the contracts the code actually implements

The e2e suite had never been run during this audit. It failed 9 of 256; seven of those
predated the audit's changes, established by building a stack from a clean HEAD worktree
and running the same specs against it rather than guessing.

Most were stale assertions rather than product defects:

- quota.spec solved for a target limit using the observed uploader count, but the divisor is
  max(active, estimated_guest_count, 1) and that config seeds at 100 — so every limit it
  aimed for came out 100x small and every "within quota" upload 413'd.
- rate-limit-shared-nat destructured `ticket` from a 429 body and fetched with
  `ticket=undefined`, turning the 429 under test into an unrelated 401. It also faked a
  release with no archive on disk, so the mint's pre-check 404'd and the per-day limiter was
  never reached; it now does a real release and asserts 200 rather than "not 429".
- ddos allowed only [200,429] from ten concurrent streams, so it failed on the very defence
  it exercises: four tickets per session survive and the rest correctly 401. Now asserts
  exactly four, which a tightened cap or an inverted eviction order would catch.
- auth-tampering asserted a throttled IP is refused EVEN with the correct password. That
  contract was deliberately removed — it let any phone on the venue NAT lock the operator
  out of their own admin panel, with a circular escape hatch. Inverted, plus a new check
  that a success does not refill an attacker's bucket.
- moderation-ui assumed a ban leaves a comment "stuck on screen"; `list_for_upload` filters
  banned authors, so it is hidden from everyone including the host. Now pins the pair that
  matters — the ban hides it, and the host's permanent removal survives an unban — and the
  UI leg it used to own is restored as a separate test on a reachable comment.

The export specs mint with `?kind=` now that a download ticket is bound to one archive, and
four of them assert the mint's 404 rather than the download's: with the kind always known,
the pre-check refuses up front instead of after charging a daily download for an archive
that cannot be served.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-08-11 22:44:48 +02:00
parent a53729a704
commit 32dfe6874a
19 changed files with 351 additions and 88 deletions

View File

@@ -101,10 +101,26 @@ test.describe('Adversarial — small-scale abuse', () => {
})
);
const responses = await Promise.all(requests);
// All accepted (or some rate-limited — both fine).
for (const r of responses) {
expect([200, 429]).toContain(r.status);
// One session may hold only MAX_TICKETS_PER_SESSION (4) live tickets — enough for a guest with
// a couple of tabs open, deliberately not enough for a reconnect loop to accumulate. Minting a
// 5th evicts the oldest, so most of these ten tickets are already dead when their stream opens
// and the server answers 401. That IS the cap working; this used to allow only [200, 429] and
// so failed on the very defence it was written to exercise.
//
// What must hold is that the server sheds the flood deliberately rather than falling over: no
// 5xx, and the surviving tickets still get their stream.
const statuses = responses.map((r) => r.status);
for (const s of statuses) {
expect([200, 401, 429], `unexpected status from the stream flood: ${statuses}`).toContain(s);
}
// EXACTLY four, not merely "at least one". The cap is a known constant, so asserting a
// bound this loose would still pass if it were tightened to 1 (a guest with two tabs loses
// their live feed) or if eviction kept the OLDEST ticket instead of the newest (every
// reconnect throws away the ticket it just minted — a permanently dead feed for that guest).
expect(
statuses.filter((s) => s === 200).length,
`exactly MAX_TICKETS_PER_SESSION streams should survive, got: ${statuses}`
).toBe(4);
// Tear them all down so the next test doesn't see leaked connections.
controllers.forEach((c) => c.abort());