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("routes back to unlock when the background is unavailable during export", async () => {106    const onRequiresUnlock = vi.fn();107    sendMessageMock.mockImplementation(async (msg: unknown) => {108      const type = (msg as { type?: string }).type;109      if (type === "GET_ENTRIES") {110        return { ok: false, error: "SERVICE_WORKER_UNAVAILABLE" };111      }112      return { ok: true, data: {} };113    });114    const mounted = renderView(115      <CJKExportView onBack={vi.fn()} onRequiresUnlock={onRequiresUnlock} />116    );117    disposers.push(mounted.dispose);118    const container = mounted.container;119120    await unlockExport(container);121    findButton(container, "Create backup phrase →").click();122    await flushView();123    await flushView();124125    expect(exportToCJKMock).not.toHaveBeenCalled();126    expect(onRequiresUnlock).toHaveBeenCalled();127  });128129  it("exports only live entries and shows the returned counts", async () => {130    sendMessageMock.mockImplementation(async (msg: unknown) => {131      const type = (msg as { type?: string }).type;132      if (type === "GET_ENTRIES") {133        return {134          ok: true,135          data: [136            sampleEntry(),137            sampleEntry({138              id: "note-1",139              type: "note",140              title: "Recovery codes",141              notes: "a1b2-c3d4",142              password: undefined,143              username: undefined,144              url: undefined,145            }),146            sampleEntry({147              id: "deleted-1",148              title: "Old account",149              deletedAt: 2,150            }),151          ],152        };153      }154      if (type === "GET_SETTINGS") {155        return { ok: true, data: { autoLockMinutes: 30 } };156      }157      return { ok: true };158    });159    exportToCJKMock.mockResolvedValue({160      phrase: "peach:v3:entries=2:zstd:aes256gcm:argon2id\n漢字漢字",161      entryCount: 2,162      cjkCharCount: 4,163      loginCount: 1,164      noteCount: 1,165      passkeyCount: 0,166      binary: new Uint8Array([1, 2, 3]),167    });168    const container = mount();169170    await unlockExport(container);171    findButton(container, "Create backup phrase →").click();172    await flushView();173    await flushView();174    await flushView();175176    expect(exportToCJKMock).toHaveBeenCalledWith({177      entries: [178        expect.objectContaining({ id: "entry-1", title: "GitHub" }),179        expect.objectContaining({ id: "note-1", title: "Recovery codes" }),180      ],181      settings: { autoLockMinutes: 30 },182      passphrase: "correct horse battery staple",183      argon2: expect.objectContaining({ memoryKiB: 65536 }),184    });185    expect(container.textContent).toContain("2 entries");186    expect(container.textContent).toContain("1 login");187    expect(container.textContent).toContain("1 note");188  });189});