/** * USER_JOURNEYS.md §12 — release the export, see status, download. * * We don't drive a real export job here (the compression takes too long * for E2E timing). Instead we forge the export-job rows via the db helper * and assert the API behavior + UI banner state. */ import { test, expect } from '../../fixtures/test'; import { ExportPage } from '../../page-objects'; const SLUG = 'e2e-test-event'; test.describe('Export — release and download', () => { test('/export shows the "not yet available" state before release', async ({ page, guest, signIn }) => { const g = await guest('PreRelease'); await signIn(page, g); const exportPage = new ExportPage(page); await exportPage.goto(); // The page shouldn't show download buttons before release. await expect(page.getByRole('button', { name: /^herunterladen$/i })).not.toBeVisible(); }); test('export status API reflects released flag', async ({ guest, db }) => { const g = await guest('ReleaseQuery'); let res = await fetch((process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101') + '/api/v1/export/status', { headers: { Authorization: `Bearer ${g.jwt}` }, }); let body: any = await res.json(); expect(body.released).toBe(false); await db.setExportReleased(SLUG, true); await db.fakeExportJob(SLUG, 'zip', 'done'); await db.fakeExportJob(SLUG, 'html', 'done'); res = await fetch((process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101') + '/api/v1/export/status', { headers: { Authorization: `Bearer ${g.jwt}` }, }); body = await res.json(); expect(body.released).toBe(true); expect(body.zip.status).toBe('done'); expect(body.html.status).toBe('done'); }); const base = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101'; // 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 { const res = await fetch(base + '/api/v1/export/ticket', { 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 }) => { const g = await guest('NotReady'); // The event is released and the zip job says `done` — but the job carries a dead epoch, which // is exactly the state a reopen (or a superseded worker) leaves behind. Readiness is derived // from `job.epoch = event.export_epoch`, so this archive is NOT current and must never be // served. A 200 here would be the stale-keepsake bug leaking through the read path. 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); }); test('ZIP download 404s when the job is current but the file is missing on disk', async ({ guest, db, }) => { const g = await guest('ReadyNoFile'); // Released, and the job is `done` at the LIVE epoch — but no archive was ever written (we // never ran a real export). The handler must 404 on the missing file, not 200/500 or serve // something stale. 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); }); });