123import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";4import { deflate } from "pako";56import { flushView, renderView } from "./view-test-utils";7import { buildChunkEnvelope, bytesToBase64Url, sha256Hex } from "../codex/helpers";8import { encodeWithErasure } from "../codex/erasure";9import { _resetReedSolomonCacheForTest } from "../codex/rs-loader";10import type { ChunkEnvelope, CodexPayload } from "../../shared/codex-types";111213141516const sendMessageMock = vi.fn<(msg: unknown) => Promise<unknown>>();1718vi.mock("../../shared/bridge", () => ({19 sendMessage: (msg: unknown) => sendMessageMock(msg),20}));212223vi.mock("../codex/scanner", () => ({24 startScanner: vi.fn(() => ({ stop: vi.fn() })),25}));26272829let preparedEnvelopes: ChunkEnvelope[] = [];30vi.mock("../codex/pdf-import", () => ({31 importCodexPDF: vi.fn(async () => ({32 chunksFound: preparedEnvelopes.length,33 totalExpected: preparedEnvelopes.length,34 envelopes: preparedEnvelopes,35 errors: [],36 })),37}));3839const VERIFY_CODE = "PX-TEST-CODE";4041async function makePayload(): Promise<CodexPayload> {42 const blobBytes = new Uint8Array(2000);43 let s = 0x9e3779b9;44 for (let i = 0; i < blobBytes.length; i++) {45 s = (s ^ (s << 13)) >>> 0;46 s = (s ^ (s >>> 17)) >>> 0;47 s = (s ^ (s << 5)) >>> 0;48 blobBytes[i] = s & 0xff;49 }50 const vaultBlob = bytesToBase64Url(blobBytes);51 const vaultFingerprint = (await sha256Hex(new TextEncoder().encode(vaultBlob)))52 .slice(0, 16)53 .toUpperCase();54 return {55 version: 2,56 verifyCode: VERIFY_CODE,57 vaultBlob,58 timestamp: 1_700_000_000_000,59 entryCount: 7,60 vaultFingerprint,61 generatedAt: 1_700_000_001_000,62 };63}6465async function buildEnvelopes(payload: CodexPayload): Promise<ChunkEnvelope[]> {66 const compressed = deflate(new TextEncoder().encode(JSON.stringify(payload)), { level: 9 });67 const { dataChunks, parityChunks, totalChunks } = await encodeWithErasure(compressed);68 const all = [...dataChunks, ...parityChunks];69 return Promise.all(70 all.map((bytes, i) =>71 buildChunkEnvelope(bytes, i, totalChunks, i >= dataChunks.length, payload.verifyCode),72 ),73 );74}7576describe("CodexRestoreView (PCH-01-015 untrimmed passphrase)", () => {77 let dispose: (() => void) | undefined;7879 beforeEach(async () => {80 document.body.innerHTML = "";81 sendMessageMock.mockReset();82 sendMessageMock.mockResolvedValue({ ok: true, data: { entryCount: 7, verifyCode: VERIFY_CODE } });83 _resetReedSolomonCacheForTest();84 const payload = await makePayload();85 preparedEnvelopes = await buildEnvelopes(payload);86 });8788 afterEach(() => {89 dispose?.();90 dispose = undefined;91 vi.unstubAllGlobals();92 });9394 async function mountAndScan() {95 const { CodexRestoreView } = await import("./CodexRestoreView");96 const onRestoreSuccess = vi.fn();97 const view = renderView(98 CodexRestoreView({ onBack: () => {}, onRestoreSuccess }) as unknown as Parameters<typeof renderView>[0],99 );100 dispose = view.dispose;101 await flushView();102103 104 105 106 const fileInput = view.container.querySelector<HTMLInputElement>('input[type="file"]')!;107 Object.defineProperty(fileInput, "files", {108 configurable: true,109 value: [{ type: "application/pdf", name: "codex.pdf" }],110 });111 fileInput.dispatchEvent(new Event("change", { bubbles: true }));112 await flushView();113 await flushView();114 return view;115 }116117 it("forwards the untrimmed passphrase to RESTORE_FROM_CODEX", async () => {118 const view = await mountAndScan();119120 const pwInput = view.container.querySelector<HTMLInputElement>('input[type="password"]');121 expect(pwInput, "passphrase prompt should be visible after a complete scan").not.toBeNull();122123 pwInput!.value = " hunter2 ";124 pwInput!.dispatchEvent(new Event("input", { bubbles: true }));125 await flushView();126127 const restoreBtn = Array.from(view.container.querySelectorAll("button")).find((b) =>128 b.textContent?.includes("restore vault"),129 )!;130 restoreBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));131 await flushView();132 await flushView();133134 const restoreCall = sendMessageMock.mock.calls.find(135 ([m]) => (m as { type?: string }).type === "RESTORE_FROM_CODEX",136 );137 expect(restoreCall, "RESTORE_FROM_CODEX should have been dispatched").toBeTruthy();138 const payload = (restoreCall![0] as { payload: { masterPassphrase: string } }).payload;139 expect(payload.masterPassphrase).toBe(" hunter2 ");140 });141142 it("blocks an all-whitespace passphrase without dispatching RESTORE_FROM_CODEX", async () => {143 const view = await mountAndScan();144145 const pwInput = view.container.querySelector<HTMLInputElement>('input[type="password"]')!;146 pwInput.value = " ";147 pwInput.dispatchEvent(new Event("input", { bubbles: true }));148 await flushView();149150 const restoreBtn = Array.from(view.container.querySelectorAll("button")).find((b) =>151 b.textContent?.includes("restore vault"),152 )!;153 restoreBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));154 await flushView();155156 expect(157 sendMessageMock.mock.calls.some(([m]) => (m as { type?: string }).type === "RESTORE_FROM_CODEX"),158 ).toBe(false);159 });160});