Turn "accept-both-outcomes" documentation tests — which pass whether the app is secure or vulnerable — into assertions that pin the secure behavior, and replace fake-UUID authz tests that 404'd before ever reaching the ownership guard with real cross-user resources. file-upload-attacks: - SVG-with-<script> → pin 400 (infer returns None for text → stored-XSS defense); was [201,400], which accepted the vulnerable outcome. - zero-byte → pin 400; path-traversal → pin 201 with UUID-derived storage and a no-path-echo check (both were [201,400]). - Fix the application/octet-stream rationale (no "application bypass" exists — the handler ignores the declared type and keys off magic bytes). authorization-deep (IDOR): - B deleting A's comment: seed a real upload+comment as A, assert 403, assert the comment survives, and assert the owner (A) still gets 204 — proving the 403 is about identity, not a broken route. (Was a DELETE on the all-zeros UUID → 404 before the user_id guard, so authorization was never exercised.) - Add B-deletes-A's-upload (403, countUploads unchanged) and B-edits-A's-caption (403, caption intact) — the find_by_id_and_event + ownership guards. export: pin the ZIP-download 404 (was [404,200] — a 200 is a data-exposure regression) and split into the two real branches: not-yet-ready, and ready-but-file-missing (new db.setExportZipReady helper). All 21 assertions verified green against the live secure backend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
84 lines
3.5 KiB
TypeScript
84 lines
3.5 KiB
TypeScript
/**
|
|
* 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<string> {
|
|
const res = await fetch(base + '/api/v1/export/ticket', {
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
});
|
|
return (await res.json()).ticket;
|
|
}
|
|
|
|
test('ZIP download 404s when the export is not yet marked ready', async ({ guest, db }) => {
|
|
const g = await guest('NotReady');
|
|
// Released flag set, but export_zip_ready is still false → must refuse, never serve.
|
|
await db.setExportReleased(SLUG, true);
|
|
await db.fakeExportJob(SLUG, 'zip', 'done');
|
|
const ticket = await mintTicket(g.jwt);
|
|
|
|
const res = await fetch(base + '/api/v1/export/zip?ticket=' + encodeURIComponent(ticket));
|
|
// Pinned to 404 (not [404,200]): a 200 here would mean serving an export that was
|
|
// never released for download — a data-exposure regression. This hits the
|
|
// `!export_zip_ready` guard.
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
test('ZIP download 404s when marked ready but the file is missing on disk', async ({ guest, db }) => {
|
|
const g = await guest('ReadyNoFile');
|
|
// Released AND ready, but no Gallery.zip on disk (we never ran a real export) →
|
|
// the handler must 404 on the missing-file check, not 200/500 or serve a stale file.
|
|
await db.setExportReleased(SLUG, true);
|
|
await db.setExportZipReady(SLUG, true);
|
|
await db.fakeExportJob(SLUG, 'zip', 'done');
|
|
const ticket = await mintTicket(g.jwt);
|
|
|
|
const res = await fetch(base + '/api/v1/export/zip?ticket=' + encodeURIComponent(ticket));
|
|
expect(res.status).toBe(404);
|
|
});
|
|
});
|