cjk-export.test.tsx

5.4 KB · raw

1// @vitest-environment jsdom23import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";45import type { VaultEntry } from "../../shared/types";6import { CJKExportView } from "./CJKExport";7import { flushView, inputText, renderView } from "./view-test-utils";89const sendMessageMock = vi.fn<(msg: unknown) => Promise<unknown>>();10const exportToCJKMock = vi.fn<(input: unknown) => Promise<unknown>>();1112vi.mock("../App", () => ({13  sendMessage: (msg: unknown) => sendMessageMock(msg),14}));1516vi.mock("../../crypto/cjkExport", () => ({17  ARGON2_DEFAULT: {18    memoryKiB: 65536,19    iterations: 3,20    parallelism: 4,21  },22  exportToCJK: (input: unknown) => exportToCJKMock(input),23}));2425vi.mock("../../crypto/cjkPassphrase", () => ({26  MIN_PASSPHRASE_LENGTH: 16,27  evaluateStrength: () => ({28    tier: "excellent",29    score: 4,30    feedback: "strong",31  }),32  validateForExport: ({33    passphrase,34    confirm,35  }: {36    passphrase: string;37    confirm: string;38  }) =>39    passphrase.length >= 16 && passphrase === confirm40      ? { ok: true }41      : { ok: false, error: "PASSPHRASE_TOO_SHORT" },42  checkPassphraseBreached: vi.fn(async () => ({43    breached: false,44    count: 0,45    apiReachable: true,46  })),47  generateDicewarePassphrase: () => "correct horse battery staple drift waves",48}));4950describe("CJKExportView", () => {51  const disposers: Array<() => void> = [];5253  beforeEach(() => {54    document.body.innerHTML = "";55    sendMessageMock.mockReset();56    exportToCJKMock.mockReset();57  });5859  afterEach(() => {60    while (disposers.length > 0) {61      disposers.pop()?.();62    }63  });6465  function mount() {66    const mounted = renderView(<CJKExportView onBack={vi.fn()} />);67    disposers.push(mounted.dispose);68    return mounted.container;69  }7071  async function unlockExport(container: HTMLElement) {72    const passphrase = container.querySelector(73      'input[placeholder="passphrase"]'74    ) as HTMLInputElement;75    inputText(passphrase, "correct horse battery staple");76    await flushView();7778    const confirm = container.querySelector(79      'input[placeholder="confirm passphrase"]'80    ) as HTMLInputElement;81    inputText(confirm, "correct horse battery staple");82    await flushView();83  }8485  function findButton(container: HTMLElement, text: string): HTMLButtonElement {86    return Array.from(container.querySelectorAll("button")).find(87      (candidate) => candidate.textContent?.trim() === text88    ) as HTMLButtonElement;89  }9091  function sampleEntry(overrides: Partial<VaultEntry> = {}): VaultEntry {92    return {93      id: "entry-1",94      type: "login",95      title: "GitHub",96      username: "june",97      password: "secret123",98      url: "https://github.com",99      createdAt: 1,100      modifiedAt: 1,101      ...overrides,102    };103  }104105  it("shows a worker error instead of generating a fake 0-entry phrase", async () => {106    sendMessageMock.mockImplementation(async (msg: unknown) => {107      const type = (msg as { type?: string }).type;108      if (type === "GET_ENTRIES") {109        return { ok: false, error: "SERVICE_WORKER_UNAVAILABLE" };110      }111      return { ok: true, data: {} };112    });113    const container = mount();114115    await unlockExport(container);116    findButton(container, "Create backup phrase →").click();117    await flushView();118    await flushView();119120    expect(exportToCJKMock).not.toHaveBeenCalled();121    expect(container.textContent).toContain(122      "Peach couldn't reach its background worker. Reopen the popup and try again."123    );124    expect(container.textContent).not.toContain("0 entries");125  });126127  it("exports only live entries and shows the returned counts", async () => {128    sendMessageMock.mockImplementation(async (msg: unknown) => {129      const type = (msg as { type?: string }).type;130      if (type === "GET_ENTRIES") {131        return {132          ok: true,133          data: [134            sampleEntry(),135            sampleEntry({136              id: "note-1",137              type: "note",138              title: "Recovery codes",139              notes: "a1b2-c3d4",140              password: undefined,141              username: undefined,142              url: undefined,143            }),144            sampleEntry({145              id: "deleted-1",146              title: "Old account",147              deletedAt: 2,148            }),149          ],150        };151      }152      if (type === "GET_SETTINGS") {153        return { ok: true, data: { autoLockMinutes: 30 } };154      }155      return { ok: true };156    });157    exportToCJKMock.mockResolvedValue({158      phrase: "peach:v3:entries=2:zstd:aes256gcm:argon2id\n漢字漢字",159      entryCount: 2,160      cjkCharCount: 4,161      loginCount: 1,162      noteCount: 1,163      passkeyCount: 0,164      binary: new Uint8Array([1, 2, 3]),165    });166    const container = mount();167168    await unlockExport(container);169    findButton(container, "Create backup phrase →").click();170    await flushView();171    await flushView();172    await flushView();173174    expect(exportToCJKMock).toHaveBeenCalledWith({175      entries: [176        expect.objectContaining({ id: "entry-1", title: "GitHub" }),177        expect.objectContaining({ id: "note-1", title: "Recovery codes" }),178      ],179      settings: { autoLockMinutes: 30 },180      passphrase: "correct horse battery staple",181      argon2: expect.objectContaining({ memoryKiB: 65536 }),182    });183    expect(container.textContent).toContain("2 entries");184    expect(container.textContent).toContain("1 login");185    expect(container.textContent).toContain("1 note");186  });187});