AdoptVaultView.test.tsx

7.1 KB · raw

1// @vitest-environment jsdom23import type { JSX } from "solid-js";4import { afterEach, describe, expect, it, vi } from "vitest";5import { Onboarding } from "./Onboarding";6import { AdoptVaultView } from "./AdoptVaultView";7import { flushView, inputText, renderView } from "./view-test-utils";8import { encodePairingCode } from "../../shared/pairing-code";910const PAIRING_CODE = encodePairingCode("192.168.4.77", 51555, Uint8Array.of(1, 2, 3, 4, 5, 6, 7, 8));1112vi.mock("@zxcvbn-ts/core", () => ({13  zxcvbn: (value: string) => ({ score: value.length >= 16 ? 4 : 1 }),14  zxcvbnOptions: { setOptions: vi.fn() },15}));16vi.mock("@zxcvbn-ts/language-common", () => ({17  adjacencyGraphs: {},18  dictionary: {},19}));20vi.mock("@zxcvbn-ts/language-en", () => ({21  translations: {},22  dictionary: {},23}));2425function button(container: HTMLElement, text: string): HTMLButtonElement {26  const found = Array.from(container.querySelectorAll("button")).find((b) =>27    (b.textContent ?? "").includes(text),28  );29  if (!found) throw new Error(`button not found: ${text}`);30  return found as HTMLButtonElement;31}3233/** The pair phase now needs a real code before it will dial anything. */34async function enterPairingCode(container: HTMLElement) {35  inputText(container.querySelector("#adopt-pair-code") as HTMLInputElement, PAIRING_CODE);36  await flushView();37}3839async function enterStrongPassword(container: HTMLElement) {40  inputText(container.querySelector("#adopt-password") as HTMLInputElement, "9xQ!vZ2pLm74Tr#Ke8Wd5Bn");41  inputText(container.querySelector("#adopt-confirm-password") as HTMLInputElement, "9xQ!vZ2pLm74Tr#Ke8Wd5Bn");42  await flushView();43}4445describe("vault adoption onboarding", () => {46  const cleanups: Array<() => void> = [];4748  afterEach(() => {49    vi.useRealTimers();50    while (cleanups.length) cleanups.pop()!();51  });5253  function mounted(view: () => JSX.Element) {54    const rendered = renderView(view);55    cleanups.push(rendered.dispose);56    return rendered;57  }5859  it("T-A21: Onboarding offers Create vault, Restore from backup, and I already have Peach", () => {60    vi.useFakeTimers();61    const onComplete = vi.fn();62    const onRestore = vi.fn();63    const onAdopt = vi.fn();64    const view = mounted(() => <Onboarding onComplete={onComplete} onRestore={onRestore} onAdopt={onAdopt} />);6566    button(view.container, "next").click();67    vi.advanceTimersByTime(220);68    button(view.container, "next").click();69    vi.advanceTimersByTime(220);7071    expect(view.container.textContent).toContain("Create vault");72    expect(view.container.textContent).toContain("Restore from backup");73    expect(view.container.textContent).toContain("I already have Peach");74    button(view.container, "I already have Peach").click();75    expect(onAdopt).toHaveBeenCalledTimes(1);76  });7778  it("T-A22: password screen uses the same minimum-length gate as create-vault", () => {79    const view = mounted(() => <AdoptVaultView onCancel={() => {}} onDone={() => {}} startAdoption={vi.fn()} />);80    const continueButton = button(view.container, "Continue");81    expect(continueButton.disabled).toBe(true);82    inputText(view.container.querySelector("#adopt-password") as HTMLInputElement, "short");83    inputText(view.container.querySelector("#adopt-confirm-password") as HTMLInputElement, "short");84    expect(view.container.textContent).toContain("need 7 more characters");85    expect(continueButton.disabled).toBe(true);86  });8788  it("T-A26: pairing cannot start until a valid pairing code is entered", async () => {89    const startAdoption = vi.fn(async () => {});90    const view = mounted(() => <AdoptVaultView onCancel={() => {}} onDone={() => {}} startAdoption={startAdoption} />);91    await enterStrongPassword(view.container);92    button(view.container, "Continue").click();93    await flushView();9495    expect(button(view.container, "Pair a device").disabled).toBe(true);96    // Long enough to be grouped, but too short to carry a token.97    inputText(view.container.querySelector("#adopt-pair-code") as HTMLInputElement, "A74H48J3");98    await flushView();99    expect(button(view.container, "Pair a device").disabled).toBe(true);100101    await enterPairingCode(view.container);102    expect(button(view.container, "Pair a device").disabled).toBe(false);103    expect(startAdoption).not.toHaveBeenCalled();104  });105106  it("T-A27: the typed code is handed to the adoption transport", async () => {107    const startAdoption = vi.fn(async (_args: { code: string }) => {});108    const view = mounted(() => <AdoptVaultView onCancel={() => {}} onDone={() => {}} startAdoption={startAdoption} />);109    await enterStrongPassword(view.container);110    button(view.container, "Continue").click();111    await enterPairingCode(view.container);112    button(view.container, "Pair a device").click();113    await flushView();114115    expect(startAdoption).toHaveBeenCalledTimes(1);116    expect(startAdoption.mock.calls[0][0]).toMatchObject({ code: PAIRING_CODE });117  });118119  it("T-A23: adoption flow reaches the SAS screen", async () => {120    const startAdoption = vi.fn(async ({ confirmSAS }: { confirmSAS(sas: string): Promise<boolean> }) => {121      await confirmSAS("895245");122    });123    const view = mounted(() => <AdoptVaultView onCancel={() => {}} onDone={() => {}} startAdoption={startAdoption} />);124    await enterStrongPassword(view.container);125    button(view.container, "Continue").click();126    await enterPairingCode(view.container);127    button(view.container, "Pair a device").click();128    await flushView();129130    expect(view.container.textContent).toContain("Confirm this code matches");131    expect(view.container.textContent).toContain("8 9 5 2 4 5");132  });133134  it("T-A24: success leaves a vault and opens Peach", async () => {135    const onDone = vi.fn();136    const view = mounted(137      () => (138        <AdoptVaultView139          onCancel={() => {}}140          onDone={onDone}141          startAdoption={vi.fn(async () => {})}142        />143      ),144    );145    await enterStrongPassword(view.container);146    button(view.container, "Continue").click();147    await enterPairingCode(view.container);148    button(view.container, "Pair a device").click();149    await flushView();150151    expect(view.container.textContent).toContain("Joined your vault.");152    button(view.container, "Open Peach").click();153    expect(onDone).toHaveBeenCalledTimes(1);154  });155156  it("T-A25: SAS cancel keeps receiver vault-less and returns to the password step", async () => {157    const createVault = vi.fn();158    const view = mounted(159      () => (160        <AdoptVaultView161          onCancel={() => {}}162          onDone={() => {}}163          startAdoption={vi.fn(async ({ confirmSAS }) => {164            const ok = await confirmSAS("895245");165            if (ok) await createVault();166            else throw new Error("sas_rejected");167          })}168        />169      ),170    );171    await enterStrongPassword(view.container);172    button(view.container, "Continue").click();173    await enterPairingCode(view.container);174    button(view.container, "Pair a device").click();175    await flushView();176    button(view.container, "Cancel").click();177    await flushView();178179    expect(createVault).not.toHaveBeenCalled();180    expect(view.container.textContent).toContain("Set a password for this device.");181  });182});