CI / Build & Quality Checks (pull_request) Successful in 1m48s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 12s
CI / Playwright smoke (e2e) (pull_request) Successful in 11m38s
One "security" strip, in the same top slot and style as the status banner (#124), for this device, in priority order: - "Verify this device" — cross-signing exists but this device isn't verified (can't unlock backed-up history, untrusted to others). First, because verifying with the recovery key also connects the backup. - "Connect this device to your key backup" — a backup exists, this device isn't using it. - "Set up key backup" — no backup at all (includes accounts without cross-signing; the setup flow does both). The button opens Settings → Devices (existing flows); "Not now" snoozes. Rules: nothing in a device's first 24 h; "Not now" snoozes that nudge 7 days; 3 dismissals stop it; never on a healthy device; waits until sync has settled (never under "Connecting…"); outage/maintenance/connection strips win. Per-device record in localStorage, wiped on logout. Mounted inside the Matrix client context and an error boundary (an early version outside the context crashed the app on load — caught before pushing). Also: the status strip wraps its text on phones instead of truncating it when there's no Details button (benefits #124's strips too). Design approved on #123 (real-client screenshots there). Tests: 4 unit (the #123 state table, loading, timing, stored record); e2e: nothing during the grace period, "Set up key backup" → Settings → Devices, "Not now" holds across a reload. Unit 1319, Playwright 29 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
75 lines
2.7 KiB
TypeScript
75 lines
2.7 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);
|
|
}
|
|
|
|
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('"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);
|
|
});
|
|
});
|