/** * USER_JOURNEYS.md §9 — ban / unban / promote / demote. Driven mostly through * the API because the host dashboard UI changes shape often; integration * coverage of the buttons lives in a separate UI-focused spec. */ import { test, expect } from '../../fixtures/test'; test.describe('Host — moderation API', () => { test('ban with hide_uploads=true sets the right flags', async ({ api, host, guest }) => { const target = await guest('Banned1'); await api.banUser(host.jwt, target.userId, true); 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(true); }); test('ban without hiding leaves uploads_hidden=false', async ({ api, host, guest }) => { 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); }); test('banned user cannot call /upload', async ({ api, host, guest }) => { const target = await guest('Banned3'); await api.banUser(host.jwt, target.userId, false); // Direct fetch — multipart body shape is just a marker; the auth middleware should reject before parsing. const res = await fetch((process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101') + '/api/v1/upload', { method: 'POST', headers: { Authorization: `Bearer ${target.jwt}` }, body: new FormData(), }); expect(res.status).toBe(403); }); test('host can promote a guest to host', async ({ api, host, guest }) => { const target = await guest('Promoted'); await api.setRole(host.jwt, target.userId, 'host'); const users = await api.listUsers(host.jwt); const row = users.find((u: any) => u.id === target.userId); expect(row?.role).toBe('host'); }); }); /** * Regression for the review's H1: role & ban used to be trusted from the JWT and * never re-checked against the DB, so a demoted/banned host kept full powers for * the life of their token (up to 30d) and a banned host could even unban * themselves. The auth extractor now re-reads the live user row, so these take * effect on the *existing* session with no re-login. */ test.describe('Host — live role/ban revocation (H1)', () => { test('a demoted host loses host powers on their existing session', async ({ api, adminToken, guest, }) => { const u = await guest('DemoteMidSession'); await api.setRole(adminToken, u.userId, 'host'); // Fresh host token (role is encoded at mint time). const { body } = await api.recover(u.displayName, u.pin); const hostJwt = body.jwt; // Sanity: the token currently has host powers. await api.listUsers(hostJwt); // Demote via admin — the host does NOT re-login. await api.setRole(adminToken, u.userId, 'guest'); // Same token is now rejected with exactly 403 (RequireHost sees the DB role, // not the JWT claim). Asserting the status guards against a spurious 500 // masquerading as "revoked". await expect(api.listUsers(hostJwt)).rejects.toThrow(/→ 403/); }); test('a banned host is locked out immediately on their existing session', async ({ api, adminToken, host, }) => { // Sanity: host token works. await api.listUsers(host.jwt); await api.banUser(adminToken, host.userId, false); // Banned users are rejected with 403 by the auth extractor before any handler runs. await expect(api.listUsers(host.jwt)).rejects.toThrow(/→ 403/); }); test('a banned host cannot unban themselves', async ({ api, adminToken, host }) => { await api.banUser(adminToken, host.userId, false); await expect( api.unbanUser(host.jwt, host.userId, { expectedStatus: [204] }) ).rejects.toThrow(/→ 403/); }); });