/** * A ticket minted against a LIVE archive must stop working the moment that archive is retired. * * This is the download-side half of the stale-keepsake guarantee, and it was left unasserted. Both * tests in `export.spec.ts` are named "ZIP download 404s…" but now assert only that the *mint* * refuses — correctly, since `/export/ticket` pre-validates and refusing there spends none of the * guest's three daily downloads. The consequence is that nothing exercised `resolve_export_file` * on the download path itself, so removing that check would not have turned anything red. * * It cannot be tested by minting against an already-dead archive (the mint refuses first), so the * order has to be: real release → mint while healthy → retire → download. Which is also exactly * what a host taking a photo down mid-download does to a ticket already in flight. */ import { test, expect } from '../../fixtures/test'; import { seedUpload } from '../../helpers/seed'; import { BASE } from '../../helpers/env'; const SLUG = 'e2e-test-event'; test.describe('Export — a retired generation cannot be downloaded', () => { test('a ticket minted before the epoch moved is refused at the download, not served stale', async ({ host, db, }) => { test.setTimeout(60_000); const bearer = { Authorization: `Bearer ${host.jwt}` }; await seedUpload(host.jwt, { caption: 'about to go stale' }); const rel = await fetch(`${BASE}/api/v1/host/gallery/release`, { method: 'POST', headers: bearer, }); expect(rel.status).toBe(204); await expect .poll( async () => { const res = await fetch(`${BASE}/api/v1/export/status`, { headers: bearer }); return (await res.json()).zip?.status; }, { timeout: 45_000, intervals: [500] } ) .toBe('done'); // Mint while everything is healthy. This must succeed, or the assertion below proves nothing — // a 404 on a ticket that was never valid would be green for the wrong reason. const mint = await fetch(`${BASE}/api/v1/export/ticket?kind=zip`, { method: 'POST', headers: bearer, }); expect(mint.status, 'the ticket must be mintable while the archive is live').toBe(200); const { ticket } = await mint.json(); expect(ticket).toBeTruthy(); // And it genuinely works right now — the positive control for the negative below. const before = await fetch(`${BASE}/api/v1/export/zip?ticket=${encodeURIComponent(ticket)}`); expect(before.status, 'the ticket must serve the archive while it is current').toBe(200); // Now retire the generation, which is what a reopen or a post-release takedown does. await db.setExportZipReady(SLUG, false); // The SAME ticket — still unexpired, still under its redemption cap, session still valid — // must now be refused. Readiness is derived at READ time from `job.epoch = event.export_epoch`, // so this is the check that stops a superseded archive being served to a guest who happened to // hold a ticket when the host moderated. const after = await fetch(`${BASE}/api/v1/export/zip?ticket=${encodeURIComponent(ticket)}`); expect( after.status, 'a retired archive must 404 on the download path, not be served from a still-valid ticket' ).toBe(404); }); });