codex-handlers.test.ts

6.1 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  });130131  it("handleRestoreFromCodex passes through stringified slot payloads without double-encoding", async () => {132    vi.doMock("./license", () => ({ isProUser: vi.fn(async () => false) }));133    const unlockFromCodex = vi.fn(async () => ({ ok: true as const, entryCount: 1 }));134    vi.doMock("./vault", () => ({135      unlockFromCodex,136      isUnlocked: () => false,137      getEntries: () => [],138    }));139    vi.doMock("../shared/native", () => ({140      isNativePlatform: () => false,141      nativePlugin: () => null,142    }));143144    const payload = {145      vaultBlob: Buffer.from(146        JSON.stringify({147          a: "{\"version\":1,\"salt\":\"deadbeef\",\"nonce\":\"cafebabe\",\"ciphertext\":\"00\",\"tag\":\"11\"}",148          b: null,149        }),150        "utf8",151      ).toString("base64url"),152      timestamp: 1,153      verifyCode: "PX-AAAA-BBBB",154      vaultFingerprint: "0123456789ABCDEF0123456789ABCDEF",155      entryCount: 1,156      masterPassphrase: "correct horse battery staple",157    };158159    const { handleRestoreFromCodex } = await import("./codex-handlers");160    const result = await handleRestoreFromCodex(payload);161162    expect(result).toEqual({163      ok: true,164      data: { entryCount: 1, verifyCode: "PX-AAAA-BBBB" },165    });166    expect(unlockFromCodex).toHaveBeenCalledWith({167      slotA:168        "{\"version\":1,\"salt\":\"deadbeef\",\"nonce\":\"cafebabe\",\"ciphertext\":\"00\",\"tag\":\"11\"}",169      slotB: null,170      timestamp: 1,171      masterPassphrase: "correct horse battery staple",172    });173  });174});