/** * Regression guard β€” the role must follow the identity, not the tab. * * The `role` store is a module-level singleton seeded ONCE at import. `goto()` is a * client-side navigation, so leaving and re-joining in the same tab re-imports nothing and * re-runs no `onMount` β€” the previous user's role simply stayed. A host who left and a * guest who then joined kept `isStaff === true` and were offered "🚫 Beitrag entfernen" on * other people's photos. The backend 403s the delete, so it was a false affordance rather * than a privilege escalation, but `/feed` never fetched `/me/context`, so it never * self-corrected either β€” it survived until a hard reload. * * The mirror case matters just as much and is easier to forget: a guest who recovers into a * host account must GAIN the affordance without a reload. */ import { test, expect } from '../../fixtures/test'; import { seedUpload } from '../../helpers/seed'; import { JoinPage } from '../../page-objects'; const REMOVE = /beitrag entfernen/i; test.describe('Role β€” follows the identity across a same-tab switch', () => { test('a guest joining after a host leaves does NOT inherit host actions', async ({ page, host, guest, signIn, }) => { // Someone else's photo β€” the only kind the removal action is offered on. const author = await guest('RoleAuthor'); await seedUpload(author.jwt); // 1. Host is signed in and DOES see the moderation action. Establishing this first is // what makes the negative assertion below meaningful. await signIn(page, host); await page.goto('/feed'); const card = page.locator('article').filter({ hasText: author.displayName }).first(); await expect(card).toBeVisible({ timeout: 15_000 }); await card.getByRole('button', { name: 'Mehr Aktionen' }).click(); await expect(page.getByRole('button', { name: REMOVE })).toBeVisible(); await page.keyboard.press('Escape'); // 2. Host leaves, in-app β€” no reload. This is the path "Event verlassen" takes. await page.goto('/account'); await page.getByRole('button', { name: /event verlassen/i }).click(); const confirm = page.getByTestId('confirm-sheet-confirm'); if (await confirm.isVisible().catch(() => false)) await confirm.click(); await page.waitForURL('**/join', { timeout: 10_000 }); // 3. A brand-new guest joins in the same tab β€” the real flow, PIN modal and all. const join = new JoinPage(page); await join.joinAs(`Nachzuegler${Date.now() % 100000}`); await join.continueToFeed(); await expect(page).toHaveURL(/\/feed$/, { timeout: 15_000 }); // 4. They must NOT be offered moderation on someone else's photo. const card2 = page.locator('article').filter({ hasText: author.displayName }).first(); await expect(card2).toBeVisible({ timeout: 15_000 }); await card2.getByRole('button', { name: 'Mehr Aktionen' }).click(); await expect( page.getByRole('button', { name: REMOVE }), 'a fresh guest must not inherit the previous user’s role' ).toHaveCount(0); }); test('a guest who recovers into a host account GAINS host actions without a reload', async ({ page, api, adminToken, guest, signIn, }) => { // The mirror. If the fix only cleared the role it would pass the test above and still // leave a real host with no moderation until they reloaded. const author = await guest('RoleAuthor2'); await seedUpload(author.jwt); const futureHost = await guest('WillBeHost'); await signIn(page, futureHost); await page.goto('/feed'); const card = page.locator('article').filter({ hasText: author.displayName }).first(); await expect(card).toBeVisible({ timeout: 15_000 }); await card.getByRole('button', { name: 'Mehr Aktionen' }).click(); await expect(page.getByRole('button', { name: REMOVE })).toHaveCount(0); await page.keyboard.press('Escape'); // Promote them server-side. Their resident JWT still claims `role: guest`. await api.setRole(adminToken, futureHost.userId, 'host'); const claim = JSON.parse(Buffer.from(futureHost.jwt.split('.')[1], 'base64').toString()); expect(claim.role, 'the token must still be stale for this to prove anything').toBe('guest'); // A plain in-app navigation back to the feed must pick up the live role. await page.goto('/account'); await page.goto('/feed'); const card2 = page.locator('article').filter({ hasText: author.displayName }).first(); await expect(card2).toBeVisible({ timeout: 15_000 }); await card2.getByRole('button', { name: 'Mehr Aktionen' }).click(); await expect( page.getByRole('button', { name: REMOVE }), 'the live role from /me/context must reach the feed' ).toBeVisible({ timeout: 10_000 }); }); });