12345678910import { beforeEach, describe, expect, it, vi } from "vitest";1112type StorageShape = Record<string, unknown>;1314describe("codex handlers", () => {15 let storage: StorageShape;1617 beforeEach(() => {18 vi.resetModules();19 vi.doUnmock("./vault");20 vi.doUnmock("./license");21 storage = {};2223 const chromeMock = {24 storage: {25 local: {26 get: vi.fn(async (key?: string | string[]) => {27 if (!key) return storage;28 if (typeof key === "string") return { [key]: storage[key] };29 return key.reduce<Record<string, unknown>>((acc, k) => {30 acc[k] = storage[k];31 return acc;32 }, {});33 }),34 set: vi.fn(async (next: Record<string, unknown>) => {35 Object.assign(storage, next);36 }),37 remove: vi.fn(async (keys: string | string[]) => {38 const list = Array.isArray(keys) ? keys : [keys];39 for (const k of list) delete storage[k];40 }),41 },42 },43 } as unknown as typeof chrome;44 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;45 });4647 it("handleGetCodexVaultData rejects free users with pro_required", async () => {48 vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) }));49 vi.doMock("./vault", () => ({50 isUnlocked: () => true,51 getEntries: () => [],52 getRecoveryWords: async () => null,53 }));54 const { handleGetCodexVaultData } = await import("./codex-handlers");55 const result = await handleGetCodexVaultData();56 expect(result).toEqual({ ok: false, error: "pro_required" });57 });5859 it("handleGetCodexVaultData returns VAULT_LOCKED for locked vault", async () => {60 vi.doMock("./license", () => ({ isProUser: vi.fn(async () => true) }));61 vi.doMock("./vault", () => ({62 isUnlocked: () => false,63 getEntries: () => {64 throw new Error("vault locked");65 },66 getRecoveryWords: async () => {67 throw new Error("vault locked");68 },69 }));70 const { handleGetCodexVaultData } = await import("./codex-handlers");71 const result = await handleGetCodexVaultData();72 expect(result.ok).toBe(false);73 expect(result.error).toMatch(/^VAULT_LOCKED/);74 });7576 it("handleEstimateCodexPages returns >=1 page for a real slotA blob", async () => {77 vi.doMock("./license", () => ({ isProUser: vi.fn(async () => true) }));78 vi.doMock("./vault", () => ({})); 79 80 81 storage.peach_vault_a = {82 version: 1,83 salt: "00".repeat(16),84 nonce: "00".repeat(12),85 86 ciphertext: "ab".repeat(1024),87 tag: "00".repeat(16),88 };89 storage.peach_vault_b = {90 version: 1,91 salt: "11".repeat(16),92 nonce: "11".repeat(12),93 ciphertext: "cd".repeat(1024),94 tag: "11".repeat(16),95 };96 const { handleEstimateCodexPages } = await import("./codex-handlers");97 const result = await handleEstimateCodexPages();98 expect(result.ok).toBe(true);99 expect(typeof result.pages).toBe("number");100 expect(result.pages!).toBeGreaterThanOrEqual(1);101 });102103 it("handleRestoreFromCodex rejects an invalid blob with INVALID_VAULT_BLOB", async () => {104 vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) }));105 const unlockFromCodex = vi.fn();106 vi.doMock("./vault", () => ({107 unlockFromCodex,108 isUnlocked: () => false,109 getEntries: () => [],110 }));111112 const { handleRestoreFromCodex } = await import("./codex-handlers");113 const result = await handleRestoreFromCodex({114 vaultBlob: "this-is-not-valid-base64url-json!!!",115 timestamp: 0,116 verifyCode: "PX-AAAA-BBBB",117 vaultFingerprint: "0".repeat(16),118 entryCount: 0,119 masterPassphrase: "ignored",120 });121 expect(result).toEqual({ ok: false, error: "INVALID_VAULT_BLOB" });122 123 124 expect(unlockFromCodex).not.toHaveBeenCalled();125 });126127 128 it("restore_from_codex_rejects_malformed_verify_code", async () => {129 vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) }));130 const unlockFromCodex = vi.fn();131 vi.doMock("./vault", () => ({ unlockFromCodex, isUnlocked: () => false, getEntries: () => [] }));132133 const { handleRestoreFromCodex } = await import("./codex-handlers");134 const result = await handleRestoreFromCodex({135 vaultBlob: "anything",136 timestamp: 1_700_000_000_000,137 verifyCode: "not-a-code",138 vaultFingerprint: "0".repeat(16),139 entryCount: 0,140 masterPassphrase: "ignored",141 });142 expect(result).toEqual({ ok: false, error: "INVALID_VERIFY_CODE" });143 expect(unlockFromCodex).not.toHaveBeenCalled();144 });145146 it("restore_from_codex_rejects_nan_timestamp_and_oversized_blob", async () => {147 vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) }));148 const unlockFromCodex = vi.fn();149 vi.doMock("./vault", () => ({ unlockFromCodex, isUnlocked: () => false, getEntries: () => [] }));150151 const { handleRestoreFromCodex } = await import("./codex-handlers");152 const base = {153 verifyCode: "PX-AAAA-BBBB",154 vaultFingerprint: "0".repeat(16),155 entryCount: 0,156 masterPassphrase: "ignored",157 };158159 const nanResult = await handleRestoreFromCodex({ ...base, vaultBlob: "ok", timestamp: NaN });160 expect(nanResult).toEqual({ ok: false, error: "INVALID_VAULT_BLOB" });161162 const bigResult = await handleRestoreFromCodex({163 ...base,164 vaultBlob: "A".repeat(10 * 1024 * 1024 + 1),165 timestamp: 1_700_000_000_000,166 });167 expect(bigResult).toEqual({ ok: false, error: "INVALID_VAULT_BLOB" });168 expect(unlockFromCodex).not.toHaveBeenCalled();169 });170171 it("handleRestoreFromCodex passes through stringified slot payloads without double-encoding", async () => {172 vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) }));173 const unlockFromCodex = vi.fn(async () => ({ ok: true as const, entryCount: 1 }));174 vi.doMock("./vault", () => ({175 unlockFromCodex,176 isUnlocked: () => false,177 getEntries: () => [],178 }));179180 const payload = {181 vaultBlob: Buffer.from(182 JSON.stringify({183 a: "{\"version\":1,\"salt\":\"deadbeef\",\"nonce\":\"cafebabe\",\"ciphertext\":\"00\",\"tag\":\"11\"}",184 b: null,185 }),186 "utf8",187 ).toString("base64url"),188 timestamp: 1,189 verifyCode: "PX-AAAA-BBBB",190 vaultFingerprint: "0123456789ABCDEF0123456789ABCDEF",191 entryCount: 1,192 masterPassphrase: "correct horse battery staple",193 };194195 const { handleRestoreFromCodex } = await import("./codex-handlers");196 const result = await handleRestoreFromCodex(payload);197198 expect(result).toEqual({199 ok: true,200 data: { entryCount: 1, verifyCode: "PX-AAAA-BBBB" },201 });202 expect(unlockFromCodex).toHaveBeenCalledWith({203 slotA:204 "{\"version\":1,\"salt\":\"deadbeef\",\"nonce\":\"cafebabe\",\"ciphertext\":\"00\",\"tag\":\"11\"}",205 slotB: null,206 timestamp: 1,207 masterPassphrase: "correct horse battery staple",208 });209 });210});