/** * Tests for src/background/codex-handlers.ts. * * These exercise the gate logic and error paths without standing up a full * vault — license and vault modules are mocked. The one happy-path that * needs a real vault is exercised via the messages.test.ts harness; here we * focus on the boundary behaviour the handler owns. */ import { beforeEach, describe, expect, it, vi } from "vitest"; type StorageShape = Record; describe("codex handlers", () => { let storage: StorageShape; beforeEach(() => { vi.resetModules(); vi.doUnmock("./vault"); vi.doUnmock("./license"); storage = {}; const chromeMock = { storage: { local: { get: vi.fn(async (key?: string | string[]) => { if (!key) return storage; if (typeof key === "string") return { [key]: storage[key] }; return key.reduce>((acc, k) => { acc[k] = storage[k]; return acc; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(storage, next); }), remove: vi.fn(async (keys: string | string[]) => { const list = Array.isArray(keys) ? keys : [keys]; for (const k of list) delete storage[k]; }), }, }, } as unknown as typeof chrome; (globalThis as { chrome?: typeof chrome }).chrome = chromeMock; }); it("handleGetCodexVaultData rejects free users with pro_required", async () => { vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) })); vi.doMock("./vault", () => ({ isUnlocked: () => true, getEntries: () => [], getRecoveryWords: async () => null, })); const { handleGetCodexVaultData } = await import("./codex-handlers"); const result = await handleGetCodexVaultData(); expect(result).toEqual({ ok: false, error: "pro_required" }); }); it("handleGetCodexVaultData returns VAULT_LOCKED for locked vault", async () => { vi.doMock("./license", () => ({ isProUser: vi.fn(async () => true) })); vi.doMock("./vault", () => ({ isUnlocked: () => false, getEntries: () => { throw new Error("vault locked"); }, getRecoveryWords: async () => { throw new Error("vault locked"); }, })); const { handleGetCodexVaultData } = await import("./codex-handlers"); const result = await handleGetCodexVaultData(); expect(result.ok).toBe(false); expect(result.error).toMatch(/^VAULT_LOCKED/); }); it("handleEstimateCodexPages returns >=1 page for a real slotA blob", async () => { vi.doMock("./license", () => ({ isProUser: vi.fn(async () => true) })); vi.doMock("./vault", () => ({})); // unused on this path // A representative slotA-shaped record; field contents don't matter for // the size estimate — only the JSON-serialised byte count does. storage.peach_vault_a = { version: 1, salt: "00".repeat(16), nonce: "00".repeat(12), // ~2 KiB of hex so the deflated stream lands in a predictable range. ciphertext: "ab".repeat(1024), tag: "00".repeat(16), }; storage.peach_vault_b = { version: 1, salt: "11".repeat(16), nonce: "11".repeat(12), ciphertext: "cd".repeat(1024), tag: "11".repeat(16), }; const { handleEstimateCodexPages } = await import("./codex-handlers"); const result = await handleEstimateCodexPages(); expect(result.ok).toBe(true); expect(typeof result.pages).toBe("number"); expect(result.pages!).toBeGreaterThanOrEqual(1); }); it("handleRestoreFromCodex rejects an invalid blob with INVALID_VAULT_BLOB", async () => { vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) })); const unlockFromCodex = vi.fn(); vi.doMock("./vault", () => ({ unlockFromCodex, isUnlocked: () => false, getEntries: () => [], })); const { handleRestoreFromCodex } = await import("./codex-handlers"); const result = await handleRestoreFromCodex({ vaultBlob: "this-is-not-valid-base64url-json!!!", timestamp: 0, verifyCode: "PX-AAAA-BBBB", vaultFingerprint: "0".repeat(16), entryCount: 0, masterPassphrase: "ignored", }); expect(result).toEqual({ ok: false, error: "INVALID_VAULT_BLOB" }); // Restoration must short-circuit before reaching the vault module on a // malformed blob — we never want to touch storage on garbage input. expect(unlockFromCodex).not.toHaveBeenCalled(); }); // PCH-01-017: validate attacker-supplied metadata before it reaches storage. it("restore_from_codex_rejects_malformed_verify_code", async () => { vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) })); const unlockFromCodex = vi.fn(); vi.doMock("./vault", () => ({ unlockFromCodex, isUnlocked: () => false, getEntries: () => [] })); const { handleRestoreFromCodex } = await import("./codex-handlers"); const result = await handleRestoreFromCodex({ vaultBlob: "anything", timestamp: 1_700_000_000_000, verifyCode: "not-a-code", vaultFingerprint: "0".repeat(16), entryCount: 0, masterPassphrase: "ignored", }); expect(result).toEqual({ ok: false, error: "INVALID_VERIFY_CODE" }); expect(unlockFromCodex).not.toHaveBeenCalled(); }); it("restore_from_codex_rejects_nan_timestamp_and_oversized_blob", async () => { vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) })); const unlockFromCodex = vi.fn(); vi.doMock("./vault", () => ({ unlockFromCodex, isUnlocked: () => false, getEntries: () => [] })); const { handleRestoreFromCodex } = await import("./codex-handlers"); const base = { verifyCode: "PX-AAAA-BBBB", vaultFingerprint: "0".repeat(16), entryCount: 0, masterPassphrase: "ignored", }; const nanResult = await handleRestoreFromCodex({ ...base, vaultBlob: "ok", timestamp: NaN }); expect(nanResult).toEqual({ ok: false, error: "INVALID_VAULT_BLOB" }); const bigResult = await handleRestoreFromCodex({ ...base, vaultBlob: "A".repeat(10 * 1024 * 1024 + 1), timestamp: 1_700_000_000_000, }); expect(bigResult).toEqual({ ok: false, error: "INVALID_VAULT_BLOB" }); expect(unlockFromCodex).not.toHaveBeenCalled(); }); it("handleRestoreFromCodex passes through stringified slot payloads without double-encoding", async () => { vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) })); const unlockFromCodex = vi.fn(async () => ({ ok: true as const, entryCount: 1 })); vi.doMock("./vault", () => ({ unlockFromCodex, isUnlocked: () => false, getEntries: () => [], })); const payload = { vaultBlob: Buffer.from( JSON.stringify({ a: "{\"version\":1,\"salt\":\"deadbeef\",\"nonce\":\"cafebabe\",\"ciphertext\":\"00\",\"tag\":\"11\"}", b: null, }), "utf8", ).toString("base64url"), timestamp: 1, verifyCode: "PX-AAAA-BBBB", vaultFingerprint: "0123456789ABCDEF0123456789ABCDEF", entryCount: 1, masterPassphrase: "correct horse battery staple", }; const { handleRestoreFromCodex } = await import("./codex-handlers"); const result = await handleRestoreFromCodex(payload); expect(result).toEqual({ ok: true, data: { entryCount: 1, verifyCode: "PX-AAAA-BBBB" }, }); expect(unlockFromCodex).toHaveBeenCalledWith({ slotA: "{\"version\":1,\"salt\":\"deadbeef\",\"nonce\":\"cafebabe\",\"ciphertext\":\"00\",\"tag\":\"11\"}", slotB: null, timestamp: 1, masterPassphrase: "correct horse battery staple", }); }); });