vault-createVaultFromRootKey.test.ts

4.7 KB · raw

1// DRAFT DESTINATION: src/background/vault-createVaultFromRootKey.test.ts2// T-A01..T-A05: createVaultFromRootKey adoption invariants.34import { beforeEach, describe, expect, it, vi } from "vitest";5import { computeVaultId } from "../shared/sync-identity";67type StorageShape = Record<string, unknown>;89function makeChromeStorageMock(): { storage: StorageShape; chrome: typeof chrome } {10  const storage: StorageShape = {};11  const chrome = {12    storage: {13      local: {14        get: vi.fn(async (key?: string | string[]) => {15          if (!key) return { ...storage };16          if (typeof key === "string") return { [key]: storage[key] };17          return key.reduce<StorageShape>((acc, k) => { acc[k] = storage[k]; return acc; }, {});18        }),19        set: vi.fn(async (next: StorageShape) => { Object.assign(storage, next); }),20        remove: vi.fn(async (keys: string | string[]) => {21          for (const k of Array.isArray(keys) ? keys : [keys]) delete storage[k];22        }),23      },24      session: { get: vi.fn(async () => ({})), set: vi.fn(), remove: vi.fn() },25    },26    alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() } },27    idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() } },28  } as unknown as typeof chrome;29  return { storage, chrome };30}3132describe("vault adoption: createVaultFromRootKey", () => {33  const SOURCE_PASSWORD = "source-adoption-passphrase-123";34  const RECEIVER_PASSWORD = "receiver-local-passphrase-456";3536  beforeEach(() => {37    vi.resetModules();38    const mock = makeChromeStorageMock();39    vi.stubGlobal("chrome", mock.chrome);40    vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("no network"); }));41  });4243  it("T-A01: round-trips an existing root key under a different local password", async () => {44    const sourceVault = await import("./vault");45    await sourceVault.createVault(SOURCE_PASSWORD);46    const rootKeyHex = sourceVault.getRootKeyHex();47    expect(rootKeyHex).toMatch(/^[0-9a-f]{64}$/);48    const sourceVaultId = await computeVaultId(new Uint8Array(Buffer.from(rootKeyHex!, "hex")));4950    vi.resetModules();51    const mock = makeChromeStorageMock();52    vi.stubGlobal("chrome", mock.chrome);53    const receiverVault = await import("./vault");54    const result = await receiverVault.createVaultFromRootKey(RECEIVER_PASSWORD, rootKeyHex!);5556    expect(result.vaultId).toBe(sourceVaultId);57    receiverVault.lock();58    expect(await receiverVault.unlock(RECEIVER_PASSWORD)).toBe(true);59    expect(receiverVault.getRootKeyHex()).toBe(rootKeyHex);60  });6162  it("T-A02: recovery words match the source vault because they derive from the same root key prefix", async () => {63    const sourceVault = await import("./vault");64    await sourceVault.createVault(SOURCE_PASSWORD);65    const rootKeyHex = sourceVault.getRootKeyHex()!;66    const sourceWords = await sourceVault.getRecoveryWords();6768    vi.resetModules();69    const mock = makeChromeStorageMock();70    vi.stubGlobal("chrome", mock.chrome);71    const receiverVault = await import("./vault");72    await receiverVault.createVaultFromRootKey(RECEIVER_PASSWORD, rootKeyHex);7374    expect(await receiverVault.getRecoveryWords()).toEqual(sourceWords);75  });7677  it.each([78    ["63 chars", "a".repeat(63)],79    ["65 chars", "a".repeat(65)],80    ["uppercase", "A".repeat(64)],81    ["non-hex", "g".repeat(64)],82    ["empty", ""],83  ])("T-A03: rejects invalid root key input: %s", async (_label, rootKeyHex) => {84    const vault = await import("./vault");85    await expect(vault.createVaultFromRootKey(RECEIVER_PASSWORD, rootKeyHex))86      .rejects.toThrow(/ADOPT_INVALID_ROOT_KEY/);87  });8889  it("T-A04: zeroes the decoded root key buffer on success and failure", async () => {90    const fillSpy = vi.spyOn(Uint8Array.prototype, "fill");91    const vault = await import("./vault");92    await vault.createVaultFromRootKey(RECEIVER_PASSWORD, "11".repeat(32));93    expect(fillSpy).toHaveBeenCalledWith(0);9495    fillSpy.mockClear();96    await expect(vault.createVaultFromRootKey("", "22".repeat(32))).rejects.toThrow();97    expect(fillSpy).toHaveBeenCalledWith(0);98  });99100  it("T-A05: adopted vault supports normal entry writes and keeps the original vault id", async () => {101    const vault = await import("./vault");102    const rootKeyHex = "33".repeat(32);103    const { vaultId } = await vault.createVaultFromRootKey(RECEIVER_PASSWORD, rootKeyHex);104    const entry = await vault.addEntry({105      type: "login",106      title: "Example",107      url: "https://example.com",108      username: "june",109      password: "secret",110    });111112    expect(vault.getEntries()).toContainEqual(expect.objectContaining({ id: entry.id, title: "Example" }));113    expect(vaultId).toBe(await computeVaultId(new Uint8Array(Buffer.from(rootKeyHex, "hex"))));114  });115});116