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:
@@ -59,7 +59,7 @@ test.describe('Export — the archives extract to readable files', () => {
|
||||
.toBe(true);
|
||||
|
||||
for (const kind of ['zip', 'html'] as const) {
|
||||
const ticketRes = await fetch(`${BASE}/api/v1/export/ticket`, {
|
||||
const ticketRes = await fetch(`${BASE}/api/v1/export/ticket?kind=${kind}`, {
|
||||
method: 'POST',
|
||||
headers: bearer,
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ test.describe('Export — EXIF orientation in the keepsake', () => {
|
||||
)
|
||||
.toBe('done');
|
||||
|
||||
const ticketRes = await fetch(`${BASE}/api/v1/export/ticket`, {
|
||||
const ticketRes = await fetch(`${BASE}/api/v1/export/ticket?kind=html`, {
|
||||
method: 'POST',
|
||||
headers: bearer,
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ test.describe('Export — no public leak (CR2)', () => {
|
||||
|
||||
// …but IS retrievable via the gated single-use ticket endpoint. This proves the
|
||||
// 404 above means "not public", not merely "no file was produced".
|
||||
const ticketRes = await fetch(`${BASE}/api/v1/export/ticket`, {
|
||||
const ticketRes = await fetch(`${BASE}/api/v1/export/ticket?kind=zip`, {
|
||||
method: 'POST',
|
||||
headers: bearer,
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ test.describe('Export — video streaming (P4)', () => {
|
||||
.toBe('done');
|
||||
|
||||
// Download Memories.zip via the gated single-use ticket.
|
||||
const ticketRes = await fetch(`${BASE}/api/v1/export/ticket`, {
|
||||
const ticketRes = await fetch(`${BASE}/api/v1/export/ticket?kind=html`, {
|
||||
method: 'POST',
|
||||
headers: bearer,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ test.describe('Export — a caption cannot brick the keepsake viewer', () => {
|
||||
)
|
||||
.toBe('done');
|
||||
|
||||
const ticketRes = await fetch(`${BASE}/api/v1/export/ticket`, {
|
||||
const ticketRes = await fetch(`${BASE}/api/v1/export/ticket?kind=html`, {
|
||||
method: 'POST',
|
||||
headers: bearer,
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ test.describe('Export — the keepsake has no broken tiles', () => {
|
||||
)
|
||||
.toBe('done');
|
||||
|
||||
const ticketRes = await fetch(`${BASE}/api/v1/export/ticket`, {
|
||||
const ticketRes = await fetch(`${BASE}/api/v1/export/ticket?kind=html`, {
|
||||
method: 'POST',
|
||||
headers: bearer,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user