test(e2e): cover the untested security lifecycles (PIN reset, logout-all, ban gating, caption)
Closes the coverage gaps the audit flagged and I verified were real:
pin-reset-lifecycle.spec.ts (new): the whole "I forgot my PIN" journey (§4) had only its
403 gate tested. Now: the always-204 non-enumeration contract (unknown name looks
identical to known); dedup (ON CONFLICT); admins are EXCLUDED from the queue; the
3-per-15-min throttle; and a host reset REVOKES the target's sessions (the pre-reset JWT
401s afterward) and clears the request. Sessions are validated per-request against the
DB, so the revoke is observable.
logout-everywhere.spec.ts (new): DELETE /sessions ("sign out everywhere") had ZERO tests.
Proves it revokes ALL of the caller's sessions across two devices (both tokens 401 after),
not just the current one, and doesn't touch another user's sessions.
media-gating: the file claimed delete AND ban-hide both revoke preview access, but only
delete was exercised. Add the ban case — a banned uploader's gated preview 404s, same as
a takedown. (Thumbnail shares the identical find_by_id_visible gate; the seed fixture
produces no thumbnail derivative, so preview is the honest thing to assert.)
export caption test: an edit after release regenerates the viewer (epoch bumps) while the
ZIP is carried forward, not rebuilt — guards the fix in the previous commit.
Helpers: api.listPinResetRequests; db.countSessionsForUser / countPinResetRequestsForUser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -518,6 +518,43 @@ test.describe('Flow re-review — reopen→re-release export integrity + stale-k
|
||||
expect(await zipJobEpoch()).toBe(await eventEpoch());
|
||||
});
|
||||
|
||||
test('editing a caption AFTER release regenerates the viewer but carries the ZIP forward', async ({
|
||||
host,
|
||||
}) => {
|
||||
// edit_upload used to update the caption and NOTHING else — no keepsake invalidation. A caption
|
||||
// lives in the HTML viewer (the ZIP holds media only), so after release the downloadable viewer
|
||||
// kept showing the OLD caption forever while the live feed showed the new one. Editing is
|
||||
// intentionally allowed post-release (like comments), so the fix is to regenerate, not forbid:
|
||||
// invalidate_and_arm(ViewerOnly) — rebuild the viewer, carry the finished ZIP forward untouched.
|
||||
const uploadId = await seedUpload(host.jwt, { caption: 'tippfehlr' });
|
||||
expect((await post('/api/v1/host/gallery/release', host.jwt)).status).toBe(204);
|
||||
await waitExportDone(host.jwt);
|
||||
|
||||
const zipEpochBefore = await zipJobEpoch();
|
||||
const eventEpochBefore = await eventEpoch();
|
||||
expect(zipEpochBefore).toBe(eventEpochBefore);
|
||||
|
||||
// The edit is allowed (not blocked by the release lock) ...
|
||||
const res = await fetch(BASE + `/api/v1/upload/${uploadId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { Authorization: `Bearer ${host.jwt}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ caption: 'korrigiert' }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// ... and it bumped the epoch (the viewer must rebuild) ...
|
||||
await expect.poll(async () => await eventEpoch(), { timeout: 10_000 }).toBeGreaterThan(eventEpochBefore);
|
||||
|
||||
// ... while the ZIP was CARRIED FORWARD, not rebuilt: its row rides the new epoch (the media
|
||||
// didn't change, so a full ZIP rebuild would be wasted work).
|
||||
await waitExportDone(host.jwt);
|
||||
expect(await zipJobEpoch(), 'the ZIP must be carried forward to the new epoch').toBe(
|
||||
await eventEpoch()
|
||||
);
|
||||
// The keepsake is still downloadable with its single photo intact.
|
||||
expect(await downloadZipEntries(host.jwt)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('a comment deleted while the ZIP is still BUILDING does not strand the ZIP', async ({
|
||||
host,
|
||||
guest,
|
||||
|
||||
Reference in New Issue
Block a user