/** * Regression guard — a ban must replay in the reconnect delta so a client that missed the * ephemeral `user-hidden` SSE (most acutely the unattended diashow projector) still evicts * the banned user's already-loaded slides instead of cycling them all night. * * A ban is NOT a soft-delete (no `upload.deleted_at`), so it never appears in the delta's * `deleted_ids`. The fix: `uploads_hidden_at` stamps when a user became hidden, and * `/feed/delta` returns `hidden_user_ids` for users hidden since the client's cursor. The * client (feed + diashow) evicts all uploads from those users. */ import { test, expect } from '../../fixtures/test'; import { seedUpload } from '../../helpers/seed'; const BASE = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101'; async function feedDelta(jwt: string, since: string): Promise { const res = await fetch(`${BASE}/api/v1/feed/delta?since=${encodeURIComponent(since)}`, { headers: { Authorization: `Bearer ${jwt}` }, }); expect(res.status).toBe(200); return res.json(); } test.describe('Realtime — ban replays in the reconnect delta (H1 audit fix)', () => { test('a ban since the cursor is in hidden_user_ids; a ban before it is not', async ({ host, guest, api, }) => { const g = await guest('BanReplayGuest'); await seedUpload(g.jwt, { caption: 'to be hidden' }); // Cursor BEFORE the ban — this is the "last-seen" point a disconnected client holds. const before = await feedDelta(host.jwt, '2000-01-01T00:00:00Z'); const cursorBefore: string = before.server_time; expect(before.hidden_user_ids).not.toContain(g.userId); // not hidden yet // Ban the guest (read-only ban; content hidden everywhere). await api.banUser(host.jwt, g.userId); // A delta from the pre-ban cursor MUST surface the ban so the client can evict. const afterBan = await feedDelta(host.jwt, cursorBefore); expect(afterBan.hidden_user_ids, 'ban replayed to a client that missed the live event').toContain( g.userId ); // And a delta from a cursor AFTER the ban must NOT re-surface it (the `>= since` filter // means a client already past the ban isn't told again forever). const cursorAfter: string = afterBan.server_time; const afterCursor = await feedDelta(host.jwt, cursorAfter); expect(afterCursor.hidden_user_ids).not.toContain(g.userId); // Unban clears the timestamp, so a fresh ban later would replay again. await api.unbanUser(host.jwt, g.userId); const afterUnban = await feedDelta(host.jwt, '2000-01-01T00:00:00Z'); expect(afterUnban.hidden_user_ids).not.toContain(g.userId); }); });