CI / Build & Quality Checks (pull_request) Successful in 3m18s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 13s
CI / Playwright smoke (e2e) (pull_request) Successful in 16m21s
SecurityBanner asked pickBanner whether another strip would show without the dismissed set, so a dismissed Kuma announcement (often pinned for weeks) still counted and suppressed the verify / backup nudge on every device that had dismissed it. The dismissed set is now a shared jotai atom (still persisted to localStorage) used by both banners. Adds a Playwright regression. Fixes #307 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
117 lines
4.4 KiB
TypeScript
117 lines
4.4 KiB
TypeScript
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);
|
|
});
|
|
});
|