import { test, expect, Page } from '@playwright/test'; import { HS, ensureUser, hsReachable, loginUI, TestUser, uniq } from './localHs'; // [Gitea #110, #123] The device-security nudge. A fresh account has no key // backup and no cross-signing → "Set up key backup", but only after the 24 h // grace period on this device; "Not now" snoozes it. const strip = (page: Page) => page.getByRole('status').filter({ hasText: /encryption keys/ }); /** Back-date this device's first-seen record past the 24 h grace period, then reload. */ async function pastGrace(page: Page) { await page.evaluate(() => { const { deviceId } = JSON.parse(localStorage.getItem('cinny_session_v1') ?? '{}'); localStorage.setItem( `lotus-security-nudge-${deviceId}`, JSON.stringify({ firstSeen: Date.now() - 2 * 86_400_000, dismissals: {} }), ); }); await page.reload(); } /** The nudge waits until sync has settled (after "Connecting…"). */ async function settled(page: Page) { await page .getByText('Connecting...') .first() .waitFor({ timeout: 30_000 }) .catch(() => undefined); await page.getByText('Connecting...').first().waitFor({ state: 'detached', timeout: 90_000 }); await page.waitForTimeout(2000); } /** [Gitea #307] Serve a Kuma status page for `localhost` with one posted announcement. */ async function mockAnnouncement(page: Page) { await page.route(/\/config\.json(\?.*)?$/, async (route) => { const res = await route.fetch(); const cfg = await res.json(); cfg.statusPages = { localhost: { url: 'https://isitup.lotusguild.org', slug: 'matrix' } }; await route.fulfill({ response: res, json: cfg }); }); await page.route(/isitup\.lotusguild\.org\/api\/status-page\//, async (route) => { const cors = { 'access-control-allow-origin': '*' }; if (route.request().url().includes('/heartbeat/')) { await route.fulfill({ headers: cors, json: { heartbeatList: {}, uptimeList: {} } }); return; } await route.fulfill({ headers: cors, json: { config: { slug: 'matrix' }, incidents: [{ id: 7, title: 'Community call on Friday', content: '', style: 'info' }], maintenanceList: [], publicGroupList: [], }, }); }); } test.describe('security nudge (#110, #123)', () => { let user: TestUser; test.beforeAll(async () => { test.skip(!(await hsReachable()), `no local homeserver at ${HS} (set E2E_LOCAL_HS)`); }); test.beforeEach(async () => { user = await ensureUser(uniq('e2e_nudge_')); }); test('nothing during the first 24 h on a device', async ({ page }) => { test.setTimeout(150_000); await loginUI(page, user); await settled(page); // Past the point where the nudge shows once the grace period is over (~5 s). await page.waitForTimeout(8000); await expect(strip(page)).toHaveCount(0); }); test('no key backup: "Set up key backup" opens Settings → Devices', async ({ page }) => { test.setTimeout(150_000); await loginUI(page, user); await pastGrace(page); await settled(page); const nudge = strip(page); await expect(nudge).toContainText("Your encryption keys aren't backed up"); await nudge.getByRole('button', { name: 'Set up' }).click(); await expect(page.getByText('Device Verification').first()).toBeVisible(); }); test('a dismissed announcement no longer hides the nudge (#307)', async ({ page }) => { test.setTimeout(200_000); await mockAnnouncement(page); await loginUI(page, user); await pastGrace(page); await settled(page); // The announcement wins the one strip slot until it's dismissed. const announcement = page.getByRole('status').filter({ hasText: 'Community call on Friday' }); await expect(announcement).toBeVisible(); await expect(strip(page)).toHaveCount(0); await announcement.getByRole('button', { name: 'Dismiss' }).click(); await page.reload(); await settled(page); await expect(strip(page)).toContainText("Your encryption keys aren't backed up"); }); test('"Not now" snoozes it across a reload', async ({ page }) => { test.setTimeout(200_000); await loginUI(page, user); await pastGrace(page); await settled(page); await strip(page).getByRole('button', { name: 'Not now' }).click(); await expect(strip(page)).toHaveCount(0); await page.reload(); await settled(page); await expect(strip(page)).toHaveCount(0); }); });