import { test, expect, Page } from '@playwright/test'; import { HS, PASSWORD, api, ensureUser, hsReachable, loginUI, uniq } from './localHs'; // [Gitea #113] Sessions unused for 90+ days: a nudge (after the device-security // one), and Review opens Settings → Devices with them ticked for sign-out. // Synapse won't backdate last-seen, so the session list is served with one // real extra session made 120 days old. const DAY = 86_400_000; const strip = (page: Page, text: RegExp) => page.getByRole('status').filter({ hasText: text }); 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(); } 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 }); } test.describe('stale sessions (#113)', () => { test.beforeAll(async () => { test.skip(!(await hsReachable()), 'local homeserver not running'); }); test('nudge after the security one, Review ticks the old session, sign-out works', async ({ page, }) => { test.setTimeout(180_000); const user = await ensureUser(uniq('ss')); // Two extra sessions: one we'll make look 4 months old, one recent. const login = (device: string) => api<{ device_id: string }>('POST', '/_matrix/client/v3/login', undefined, { type: 'm.login.password', identifier: { type: 'm.id.user', user: user.localpart }, password: PASSWORD, device_id: device, initial_device_display_name: device, }); await login('OLDLAPTOP'); await login('RECENTPHONE'); await page.route(/\/_matrix\/client\/v3\/devices(\?.*)?$/, async (route) => { const res = await route.fetch(); const body = await res.json(); for (const d of body.devices ?? []) { if (d.device_id === 'OLDLAPTOP') d.last_seen_ts = Date.now() - 120 * DAY; else d.last_seen_ts = Date.now() - DAY; } await route.fulfill({ response: res, json: body }); }); await loginUI(page, user); await pastGrace(page); await settled(page); // A fresh account has no key backup: that nudge comes first. const security = strip(page, /encryption keys/); await expect(security).toBeVisible({ timeout: 30_000 }); await security.getByRole('button', { name: 'Not now' }).click(); const stale = strip(page, /a session you haven't used in 3 months/); await expect(stale).toBeVisible({ timeout: 30_000 }); await stale.getByRole('button', { name: 'Review' }).click(); const dialog = page.getByRole('dialog').first(); await expect(dialog.getByText('Inactive for 3+ months')).toHaveCount(1); await expect(dialog.getByText(/Logout from selected devices\. \(1 selected\)/)).toBeVisible(); // The sticky "Logout from selected devices" bar comes last. await dialog.getByRole('button', { name: 'Logout', exact: true }).last().click(); await page.locator('input[name="passwordInput"]').fill(PASSWORD); await page.getByRole('button', { name: 'Continue' }).click(); await expect(dialog.getByText(/Logout from selected devices/)).toHaveCount(0, { timeout: 30_000, }); const { devices } = await api<{ devices: { device_id: string }[] }>( 'GET', '/_matrix/client/v3/devices', user.token, ); const ids = devices.map((d) => d.device_id); expect(ids).not.toContain('OLDLAPTOP'); expect(ids).toContain('RECENTPHONE'); expect(HS).toBeTruthy(); }); });