9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
260 lines
8.9 KiB
TypeScript
260 lines
8.9 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
import { npubEncode } from "nostr-tools/nip19";
|
|
|
|
import { installMockBridge } from "../helpers/bridge";
|
|
import { openSettings } from "../helpers/settings";
|
|
|
|
const CURRENT_PUBKEY = "deadbeef".repeat(8);
|
|
const DIFFERENT_PUBKEY = "c0ffee00".repeat(8);
|
|
const BACKUP_FILE = {
|
|
name: "identity.ncryptsec",
|
|
mimeType: "text/plain",
|
|
buffer: Buffer.from("ncryptsec1mockbackupmaterial"),
|
|
};
|
|
|
|
async function openIdentity(page: Page) {
|
|
const identity = page.getByTestId("profile-identity-card");
|
|
if (
|
|
!(await identity.evaluate(
|
|
(element) => element instanceof HTMLDetailsElement && element.open,
|
|
))
|
|
) {
|
|
await page.getByTestId("profile-identity-toggle").click();
|
|
}
|
|
}
|
|
|
|
async function openBackupSettings(
|
|
page: Page,
|
|
mock?: Parameters<typeof installMockBridge>[1],
|
|
) {
|
|
await installMockBridge(page, mock);
|
|
await page.goto("/");
|
|
await openSettings(page, "profile");
|
|
await openIdentity(page);
|
|
}
|
|
|
|
async function openPrivateKeyMenu(page: Page) {
|
|
const reveal = page.getByTestId("profile-private-key-toggle");
|
|
if ((await reveal.textContent())?.trim() === "Reveal") {
|
|
await reveal.click();
|
|
}
|
|
await page.getByTestId("nsec-actions").click();
|
|
await expect(page.getByTestId("private-key-create-backup")).toBeVisible();
|
|
}
|
|
|
|
async function openCreateBackup(page: Page) {
|
|
await openPrivateKeyMenu(page);
|
|
await page.getByTestId("private-key-create-backup").click();
|
|
const dialog = page.getByTestId("encrypted-backup-dialog");
|
|
await expect(dialog).toBeVisible();
|
|
return dialog;
|
|
}
|
|
|
|
async function openTestBackup(page: Page) {
|
|
await openPrivateKeyMenu(page);
|
|
await page.getByTestId("private-key-test-backup").click();
|
|
const dialog = page.getByTestId("backup-test-dialog");
|
|
await expect(dialog).toBeVisible();
|
|
return dialog;
|
|
}
|
|
|
|
async function selectBackupFile(page: Page) {
|
|
await page.getByTestId("backup-test-file-input").setInputFiles(BACKUP_FILE);
|
|
await expect(page.getByTestId("backup-test-file-accepted")).toContainText(
|
|
BACKUP_FILE.name,
|
|
);
|
|
}
|
|
|
|
async function verifyBackup(page: Page, password: string) {
|
|
await page.getByTestId("backup-test-password").fill(password);
|
|
await page.getByTestId("backup-test-verify").click();
|
|
}
|
|
|
|
async function backupSaveCallCount(page: Page) {
|
|
return page.evaluate(
|
|
() =>
|
|
window.__BUZZ_E2E_COMMANDS__?.filter(
|
|
(command) => command === "save_ncryptsec_copy",
|
|
).length ?? 0,
|
|
);
|
|
}
|
|
|
|
test("private key menu replaces the backup settings rows", async ({ page }) => {
|
|
await openBackupSettings(page);
|
|
|
|
await expect(page.getByTestId("profile-encrypted-backup-row")).toHaveCount(0);
|
|
await expect(page.getByTestId("profile-backup-test-row")).toHaveCount(0);
|
|
|
|
await openPrivateKeyMenu(page);
|
|
await expect(page.getByTestId("nsec-copy")).toContainText("Copy");
|
|
await expect(page.getByTestId("private-key-create-backup")).toHaveText(
|
|
"Create backup",
|
|
);
|
|
await expect(page.getByTestId("private-key-test-backup")).toHaveText(
|
|
"Test backup",
|
|
);
|
|
|
|
await page.getByTestId("nsec-copy").click();
|
|
await expect(page.getByText(/clipboard$/i)).toBeVisible();
|
|
await expect(page.getByTestId("private-key-create-backup")).toHaveCount(0);
|
|
|
|
await openPrivateKeyMenu(page);
|
|
await page.getByTestId("private-key-test-backup").click();
|
|
const testDialog = page.getByTestId("backup-test-dialog");
|
|
await expect(testDialog).toContainText("Test a key backup");
|
|
await expect(testDialog.getByText("Select your backup file")).toBeVisible();
|
|
await expect(testDialog).toContainText("standard NIP-49 format");
|
|
});
|
|
|
|
test("creation requires a sufficiently long password and exposes a temporary header download", async ({
|
|
page,
|
|
}) => {
|
|
await openBackupSettings(page, {
|
|
backupSavePaths: [
|
|
"/Users/test/Downloads/identity.ncryptsec",
|
|
"/Users/test/Desktop/identity-copy.ncryptsec",
|
|
],
|
|
});
|
|
const dialog = await openCreateBackup(page);
|
|
|
|
const password = dialog.getByTestId("backup-passphrase-input");
|
|
const submit = dialog.getByTestId("encrypted-backup-create");
|
|
await expect(password).toHaveAttribute(
|
|
"placeholder",
|
|
"Password (min 12 characters)",
|
|
);
|
|
await expect(submit).toBeDisabled();
|
|
await password.fill("short");
|
|
await expect(submit).toBeDisabled();
|
|
|
|
await password.fill("custom password");
|
|
await expect(submit).toBeEnabled();
|
|
await submit.click();
|
|
await expect.poll(() => backupSaveCallCount(page)).toBe(1);
|
|
await expect(dialog).toBeHidden();
|
|
|
|
const keyRow = page.getByTestId("profile-private-key-row");
|
|
const download = keyRow.getByTestId("encrypted-backup-download");
|
|
await expect(download).toBeVisible();
|
|
await expect(download).toHaveText("Download backup");
|
|
await expect(download).toHaveClass(/bg-primary/);
|
|
await expect(
|
|
download.getByTestId("encrypted-backup-availability-fill"),
|
|
).toBeVisible();
|
|
await expect(keyRow.getByTestId("profile-private-key-toggle")).toBeVisible();
|
|
|
|
await download.click();
|
|
await expect.poll(() => backupSaveCallCount(page)).toBe(2);
|
|
});
|
|
|
|
test("encryption and native save continue after closing the dialog and settings", async ({
|
|
page,
|
|
}) => {
|
|
await openBackupSettings(page, {
|
|
backupEncryptionDelayMs: 750,
|
|
backupSavePaths: [null],
|
|
});
|
|
const dialog = await openCreateBackup(page);
|
|
await dialog
|
|
.getByTestId("backup-passphrase-input")
|
|
.fill("background password");
|
|
await dialog.getByTestId("encrypted-backup-create").click();
|
|
await expect(dialog.getByTestId("encrypted-backup-progress")).toBeVisible();
|
|
|
|
await dialog.getByRole("button", { name: "Close" }).click();
|
|
await page.getByTestId("settings-back-to-app").click();
|
|
await expect(page.getByTestId("settings-back-to-app")).toHaveCount(0);
|
|
await expect(
|
|
page.getByText("Preparing backup…", { exact: true }),
|
|
).toBeVisible();
|
|
|
|
await expect.poll(() => backupSaveCallCount(page)).toBe(1);
|
|
const readyToast = page.getByText("Backup ready to download", {
|
|
exact: true,
|
|
});
|
|
await expect(readyToast).toBeVisible();
|
|
await expect(
|
|
page.getByText("Your backup will be available to download for 5 minutes.", {
|
|
exact: true,
|
|
}),
|
|
).toBeVisible();
|
|
|
|
await page.getByRole("button", { name: "Open settings" }).click();
|
|
await expect(page.getByTestId("settings-back-to-app")).toBeVisible();
|
|
await openIdentity(page);
|
|
await expect(page.getByTestId("encrypted-backup-download")).toBeVisible();
|
|
});
|
|
|
|
test("the temporary download expires after five minutes", async ({ page }) => {
|
|
await page.clock.install({ time: new Date("2026-07-29T12:00:00Z") });
|
|
await openBackupSettings(page);
|
|
const dialog = await openCreateBackup(page);
|
|
await dialog.getByTestId("backup-passphrase-input").fill("expiring password");
|
|
await dialog.getByTestId("encrypted-backup-create").click();
|
|
await page.clock.fastForward(1);
|
|
|
|
await expect.poll(() => backupSaveCallCount(page)).toBe(1);
|
|
const download = page.getByTestId("encrypted-backup-download");
|
|
await expect(download).toHaveText("Download backup");
|
|
await expect(
|
|
download.getByTestId("encrypted-backup-availability-fill"),
|
|
).toBeVisible();
|
|
await page.clock.fastForward(5 * 60 * 1000 + 1);
|
|
await expect(page.getByTestId("encrypted-backup-download")).toHaveCount(0);
|
|
});
|
|
|
|
test("wrong backup password permits a successful retry in the test modal", async ({
|
|
page,
|
|
}) => {
|
|
await openBackupSettings(page, {
|
|
backupVerificationErrors: ["Wrong password.", null],
|
|
});
|
|
const dialog = await openTestBackup(page);
|
|
await selectBackupFile(page);
|
|
|
|
await verifyBackup(page, "wrong password");
|
|
await expect(dialog.getByTestId("backup-test-error")).toHaveText(
|
|
"Wrong password.",
|
|
);
|
|
await expect(dialog.getByTestId("backup-test-password")).toHaveValue("");
|
|
await expect(dialog.getByTestId("backup-test-verify")).toBeDisabled();
|
|
|
|
await verifyBackup(page, "correct password");
|
|
await expect(dialog.getByTestId("backup-test-success")).toContainText(
|
|
"It restores your current Buzz identity.",
|
|
);
|
|
});
|
|
|
|
for (const identity of [
|
|
{
|
|
label: "current",
|
|
pubkey: CURRENT_PUBKEY,
|
|
message: "It restores your current Buzz identity.",
|
|
},
|
|
{
|
|
label: "different",
|
|
pubkey: DIFFERENT_PUBKEY,
|
|
message: "It restores a different identity than the one signed in here.",
|
|
},
|
|
]) {
|
|
test(`successful modal verification identifies the ${identity.label} identity using only its npub`, async ({
|
|
page,
|
|
}) => {
|
|
await openBackupSettings(page, {
|
|
backupVerificationPubkeys: [identity.pubkey],
|
|
});
|
|
const dialog = await openTestBackup(page);
|
|
await selectBackupFile(page);
|
|
await verifyBackup(page, "one-time password");
|
|
|
|
const success = dialog.getByTestId("backup-test-success");
|
|
await expect(success).toContainText(identity.message);
|
|
await expect(success.getByTestId("backup-test-npub")).toContainText(
|
|
npubEncode(identity.pubkey),
|
|
);
|
|
await expect(success).not.toContainText(identity.pubkey);
|
|
await expect(success).not.toContainText("one-time password");
|
|
await expect(success).not.toContainText(BACKUP_FILE.buffer.toString());
|
|
});
|
|
}
|