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:
@@ -90,6 +90,14 @@ test.describe('Upload — storage quota enforcement', () => {
|
||||
await api.patchConfig(adminToken, {
|
||||
quota_enabled: 'true',
|
||||
storage_quota_enabled: 'true',
|
||||
// The per-user ceiling divides the disk budget by
|
||||
// `max(active_uploaders, estimated_guest_count, 1)` — the operator's expected headcount is a
|
||||
// FLOOR on the divisor, so the ceiling settles early instead of sliding down all evening as
|
||||
// guests arrive. It is seeded at 100, and `setLimitTo` below solves for a target using the
|
||||
// OBSERVED uploader count, so every limit it aimed for came out 100x too small and every
|
||||
// "within the quota" upload 413'd. Pin it to 1 so the divisor is the count the helper
|
||||
// actually controls; the floor itself is exercised by the Rust unit tests.
|
||||
estimated_guest_count: '1',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -55,13 +55,23 @@ test.describe('Upload — a rejected upload is surfaced', () => {
|
||||
await expect(queue, 'the upload queue must be rendered somewhere').toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
// Both the status chip ("Gesperrt") and the server's reason ("Du bist gesperrt.") must
|
||||
// render — the reason is the part that had no UI at all before.
|
||||
await expect(page.getByText('Gesperrt', { exact: true })).toBeVisible();
|
||||
// The server's reason ("Du bist gesperrt.") must render — it had no UI at all before.
|
||||
await expect(page.getByText('Du bist gesperrt.')).toBeVisible();
|
||||
// The chip reads "Fehler", NOT "Gesperrt", and that is the fix rather than a regression.
|
||||
// A ban used to come back as a generic `forbidden`, which purged the blob and moved the row
|
||||
// to `blocked` — a terminal state with no retry button. So an unban restored everything
|
||||
// except the photo that was actually in flight, which is the one the guest cares about.
|
||||
// It is now a distinct `user_banned` code that PARKS the row (status `error`, blob kept,
|
||||
// `parkedFor: 'unban'`) and resumes it when `user-shown` arrives.
|
||||
await expect(page.getByText('Gesperrt', { exact: true })).toHaveCount(0);
|
||||
// Positively, not just negatively: a chip that rendered empty would satisfy the line above.
|
||||
await expect(page.getByText('Fehler', { exact: true }).first()).toBeVisible();
|
||||
|
||||
// 3. The badge must not read as success. It counted only pending/uploading before, so a
|
||||
// rejected item dropped it to 0 — indistinguishable from a completed upload.
|
||||
// The row is now parked (`error` + `parkedFor: 'unban'`) rather than terminally `blocked`,
|
||||
// so it is the parked count that must be exactly one — and critically the blob must still
|
||||
// be there, since that is what an unban replays.
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
@@ -74,7 +84,10 @@ test.describe('Upload — a rejected upload is surfaced', () => {
|
||||
const all = tx.objectStore('queue').getAll();
|
||||
all.onsuccess = () =>
|
||||
resolve(
|
||||
all.result.filter((r: { status: string }) => r.status === 'blocked').length
|
||||
all.result.filter(
|
||||
(r: { status: string; parkedFor?: string; blob?: Blob }) =>
|
||||
r.status === 'error' && r.parkedFor === 'unban' && !!r.blob
|
||||
).length
|
||||
);
|
||||
all.onerror = () => reject(all.error);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user