fix(user-flow): close offline-queue, export, session, ban & realtime gaps
Some checks failed
E2E / Playwright E2E (chromium-desktop) (push) Failing after 6m52s
E2E / Cross-UA smoke matrix (push) Failing after 3m50s

Comprehensive user-flow review across guest/host/admin roles, then fixes with
e2e regression guards. Highlights:

- Offline upload queue auto-resumes on reconnect: network errors keep items
  pending (not error), 4xx are terminal (no infinite retry), quota exhaustion
  returns a distinct 413; queue cap + dedup.
- Export lifecycle: release atomically locks uploads; reopen invalidates and
  re-release regenerates the keepsake; workers claim their job atomically so a
  reopen->re-release can't corrupt the ZIP; startup re-spawns interrupted
  exports.
- Sessions slide on activity (no 30-day cliff); JWT expiry deferred to the
  revocable session row; sign-out-everywhere + revoke-on-PIN-reset.
- Ban always hides content (v_feed / find_visible_media / export filter
  is_banned) but stays a read-only ban per USER_JOURNEYS §10 — sessions are
  not revoked, read access + keepsake download preserved.
- Realtime: server-clock SSE delta cursor; event-closed/opened drive the UI
  live; feed_delta rate-limited; like returns {liked, like_count} to fix
  multi-device drift; lightbox live comments; diashow delta backfill.
- Forgotten-PIN in-app request flow; simultaneous same-name join returns 409;
  quota increment is transactional; operator floor.

Adds e2e/specs/10-flow-review/ (offline resume, export integrity, deterministic
anti-race guard) and updates existing specs for the new contracts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-10 23:05:37 +02:00
parent 16d1f356be
commit 641174717c
38 changed files with 1400 additions and 153 deletions

View File

@@ -61,7 +61,7 @@ test.describe('Host — event lock', () => {
method: 'POST',
headers: { Authorization: `Bearer ${g.jwt}` },
});
expect(likeRes.status).toBe(204);
expect(likeRes.status).toBe(200);
// Comments stay open on a locked event.
const commentRes = await fetch(`${BASE}/api/v1/upload/${id}/comments`, {

View File

@@ -16,13 +16,15 @@ test.describe('Host — moderation API', () => {
expect(row?.uploads_hidden).toBe(true);
});
test('ban without hiding leaves uploads_hidden=false', async ({ api, host, guest }) => {
test('ban always hides — the hide_uploads flag is ignored (USER_JOURNEYS §10.5)', async ({ api, host, guest }) => {
// Ban is now unconditionally a hide: even asking NOT to hide still hides, because a
// banned user's content is "gone" everywhere. The legacy hide_uploads arg is ignored.
const target = await guest('Banned2');
await api.banUser(host.jwt, target.userId, false);
const users = await api.listUsers(host.jwt);
const row = users.find((u: any) => u.id === target.userId);
expect(row?.is_banned).toBe(true);
expect(row?.uploads_hidden).toBe(false);
expect(row?.uploads_hidden).toBe(true);
});
test('banned user cannot call /upload', async ({ api, host, guest }) => {
@@ -102,7 +104,7 @@ test.describe('Host — live role/ban revocation (H1)', () => {
// H1 / USER_JOURNEYS §10: a banned user keeps *read* access but every write is
// rejected with 403. The ban is enforced on write handlers + Require{Host,Admin},
// NOT in the base extractor (which would wrongly block reads too).
test('a banned user keeps read access but is blocked from writes', async ({ api, host, guest }) => {
test('a banned user keeps read access but is blocked from writes', async ({ api, host, guest, db }) => {
const base = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101';
const target = await guest('BannedRW');
const auth = (jwt: string) => ({ Authorization: `Bearer ${jwt}` });
@@ -147,10 +149,11 @@ test.describe('Host — live role/ban revocation (H1)', () => {
});
expect(delComment.status).toBe(403);
// The upload survived every blocked write (still fetchable via the feed).
const feed = await fetch(base + '/api/v1/feed', { headers: auth(host.jwt) });
const body = await feed.json();
const rows = body.uploads ?? body.items ?? body;
expect(Array.isArray(rows) && rows.some((u: any) => u.id === ownUpload)).toBe(true);
// The upload survived every blocked write — the delete was rejected, so the row still
// exists (deleted_at IS NULL). We check the DB directly rather than the feed: ban now
// ALWAYS hides, so a banned user's own upload is (correctly) filtered out of every feed
// even though the row is intact. Read access to *other* content is proven by the 200 on
// /me/context above.
expect(await db.countUploadsForUser(target.userId)).toBe(1);
});
});