codex-handlers.test.ts

4.6 KB · raw

1/**2 * Tests for src/background/codex-handlers.ts.3 *4 * These exercise the gate logic and error paths without standing up a full5 * vault — license and vault modules are mocked. The one happy-path that6 * needs a real vault is exercised via the messages.test.ts harness; here we7 * focus on the boundary behaviour the handler owns.8 */910import { 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", () => ({})); // unused on this path79    // A representative slotA-shaped record; field contents don't matter for80    // the size estimate — only the JSON-serialised byte count does.81    storage.peach_vault_a = {82      version: 1,83      salt: "00".repeat(16),84      nonce: "00".repeat(12),85      // ~2 KiB of hex so the deflated stream lands in a predictable range.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    }));111    vi.doMock("../shared/native", () => ({112      isNativePlatform: () => false,113      nativePlugin: () => null,114    }));115116    const { handleRestoreFromCodex } = await import("./codex-handlers");117    const result = await handleRestoreFromCodex({118      vaultBlob: "this-is-not-valid-base64url-json!!!",119      timestamp: 0,120      verifyCode: "PX-AAAA-BBBB",121      vaultFingerprint: "0".repeat(16),122      entryCount: 0,123      masterPassphrase: "ignored",124    });125    expect(result).toEqual({ ok: false, error: "INVALID_VAULT_BLOB" });126    // Restoration must short-circuit before reaching the vault module on a127    // malformed blob — we never want to touch storage on garbage input.128    expect(unlockFromCodex).not.toHaveBeenCalled();129  });130});