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

@@ -88,12 +88,14 @@ test.describe('Export — release and download', () => {
// Browser downloads stream to disk via a top-level navigation, so the download
// endpoint authenticates with a single-use ticket (no Bearer header).
async function mintTicket(jwt: string): Promise<string> {
const res = await fetch(base + '/api/v1/export/ticket', {
/** The raw mint response — `/export/ticket` pre-validates that the archive is actually
* servable, so an unavailable keepsake is refused HERE rather than after charging one of the
* guest's three daily downloads. */
async function mintTicketResponse(jwt: string, kind: 'zip' | 'html' = 'zip') {
return fetch(base + `/api/v1/export/ticket?kind=${kind}`, {
method: 'POST',
headers: { Authorization: `Bearer ${jwt}` },
});
return (await res.json()).ticket;
}
test('ZIP download 404s for a `done` job at a RETIRED epoch', async ({ guest, db }) => {
@@ -105,10 +107,12 @@ test.describe('Export — release and download', () => {
await db.setExportReleased(SLUG, true);
await db.fakeExportJob(SLUG, 'zip', 'done');
await db.setExportZipReady(SLUG, false); // retire the job to a dead epoch
const ticket = await mintTicket(g.jwt);
const res = await fetch(base + '/api/v1/export/zip?ticket=' + encodeURIComponent(ticket));
expect(res.status).toBe(404);
// Refused at the MINT. This used to be asserted one step later, on the download, because the
// spec did not send `kind` and so skipped the pre-check entirely — now that a ticket is bound
// to an archive the kind is always known, and the guest is told the truth before a daily
// download is spent on an archive that cannot be served.
expect((await mintTicketResponse(g.jwt)).status).toBe(404);
});
test('ZIP download 404s when the job is current but the file is missing on disk', async ({
@@ -122,9 +126,9 @@ test.describe('Export — release and download', () => {
await db.setExportReleased(SLUG, true);
await db.fakeExportJob(SLUG, 'zip', 'done');
await db.setExportZipReady(SLUG, true);
const ticket = await mintTicket(g.jwt);
const res = await fetch(base + '/api/v1/export/zip?ticket=' + encodeURIComponent(ticket));
expect(res.status).toBe(404);
// Same as above: the pre-check resolves the file on disk, so a `done` job whose archive is
// missing is refused at the mint rather than 404ing mid-download.
expect((await mintTicketResponse(g.jwt)).status).toBe(404);
});
});