/** * 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(); // POSITIVE anchor first. An absence-only assertion ("no download button") is green on a // blank page, a 404, or an unhydrated shell — i.e. it would pass even with the buttons // rendered, if the locator were wrong. Pin the empty state we actually expect. await expect(exportPage.notAvailableBanner).toBeVisible({ timeout: 10_000 }); // And only THEN the absence: no download affordance exists before release. This uses the // real button label ("Download"), so rendering a download button here turns it red. await expect(exportPage.downloadButtons).toHaveCount(0); }); test('/export shows enabled download buttons once released and the jobs are done', async ({ page, guest, signIn, db, }) => { const g = await guest('PostRelease'); await db.setExportReleased(SLUG, true); await db.fakeExportJob(SLUG, 'zip', 'done'); await db.fakeExportJob(SLUG, 'html', 'done'); await signIn(page, g); const exportPage = new ExportPage(page); await exportPage.goto(); // The mirror of the test above: this is what proves the "before release" locators can // actually SEE a download button when one exists. Without this, a typo'd locator makes // the pre-release test unfalsifiable. await expect(exportPage.notAvailableBanner).toHaveCount(0); await expect(exportPage.zipDownloadButton).toBeVisible({ timeout: 10_000 }); await expect(exportPage.zipDownloadButton).toBeEnabled(); await expect(exportPage.htmlDownloadButton).toBeVisible(); await expect(exportPage.htmlDownloadButton).toBeEnabled(); }); 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); }); });