import { beforeEach, describe, expect, it, vi } from "vitest"; import { computeVaultId } from "../shared/sync-identity"; import type { StoredVault } from "../shared/types"; type StorageShape = Record; function makeChromeStorageMock(): { storage: StorageShape; chrome: typeof chrome } { const storage: StorageShape = {}; 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: StorageShape) => { Object.assign(storage, next); }), remove: vi.fn(async (keys: string | string[]) => { for (const k of Array.isArray(keys) ? keys : [keys]) delete storage[k]; }), }, session: { get: vi.fn(async () => ({})), set: vi.fn(), remove: vi.fn() }, }, alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() } }, idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() } }, } as unknown as typeof chrome; return { storage, chrome: chromeMock }; } describe("vault adoption: createVaultFromRootKey", () => { const SOURCE_PASSWORD = "source-adoption-passphrase-123"; const RECEIVER_PASSWORD = "receiver-local-passphrase-456"; beforeEach(() => { vi.resetModules(); const mock = makeChromeStorageMock(); vi.stubGlobal("chrome", mock.chrome); vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("no network"); })); }); it("T-A01: round-trips an existing root key under a different local password", async () => { const sourceVault = await import("./vault"); await sourceVault.createVault(SOURCE_PASSWORD); const rootKeyHex = sourceVault.getRootKeyHex(); expect(rootKeyHex).toMatch(/^[0-9a-f]{64}$/); const sourceVaultId = await computeVaultId(new Uint8Array(Buffer.from(rootKeyHex!, "hex"))); vi.resetModules(); const mock = makeChromeStorageMock(); vi.stubGlobal("chrome", mock.chrome); const receiverVault = await import("./vault"); const result = await receiverVault.createVaultFromRootKey(RECEIVER_PASSWORD, rootKeyHex!); expect(result.vaultId).toBe(sourceVaultId); receiverVault.lock(); expect(await receiverVault.unlock(RECEIVER_PASSWORD)).toBe(true); expect(receiverVault.getRootKeyHex()).toBe(rootKeyHex); }); it("T-A02: recovery words match the source vault because they derive from the same root key prefix", async () => { const sourceVault = await import("./vault"); await sourceVault.createVault(SOURCE_PASSWORD); const rootKeyHex = sourceVault.getRootKeyHex()!; const sourceWords = await sourceVault.getRecoveryWords(); vi.resetModules(); const mock = makeChromeStorageMock(); vi.stubGlobal("chrome", mock.chrome); const receiverVault = await import("./vault"); await receiverVault.createVaultFromRootKey(RECEIVER_PASSWORD, rootKeyHex); expect(await receiverVault.getRecoveryWords()).toEqual(sourceWords); }); it.each([ ["63 chars", "a".repeat(63)], ["65 chars", "a".repeat(65)], ["uppercase", "A".repeat(64)], ["non-hex", "g".repeat(64)], ["empty", ""], ])("T-A03: rejects invalid root key input: %s", async (_label, rootKeyHex) => { const vault = await import("./vault"); await expect(vault.createVaultFromRootKey(RECEIVER_PASSWORD, rootKeyHex)) .rejects.toThrow(/ADOPT_INVALID_ROOT_KEY/); }); it("T-A04: zeroes the decoded root key buffer on success and failure", async () => { const fillSpy = vi.spyOn(Uint8Array.prototype, "fill"); const vault = await import("./vault"); await vault.createVaultFromRootKey(RECEIVER_PASSWORD, "11".repeat(32)); expect(fillSpy).toHaveBeenCalledWith(0); vi.resetModules(); const mock = makeChromeStorageMock(); const storageSet = mock.chrome.storage.local.set as unknown as ReturnType; storageSet.mockRejectedValueOnce(new Error("quota")); vi.stubGlobal("chrome", mock.chrome); const failingVault = await import("./vault"); fillSpy.mockClear(); await expect(failingVault.createVaultFromRootKey(RECEIVER_PASSWORD, "22".repeat(32))).rejects.toThrow(); expect(fillSpy).toHaveBeenCalledWith(0); }); it("T-A05: adopted vault supports normal entry writes and keeps the original vault id", async () => { const vault = await import("./vault"); const rootKeyHex = "33".repeat(32); const { vaultId } = await vault.createVaultFromRootKey(RECEIVER_PASSWORD, rootKeyHex); const entry = await vault.addEntry({ type: "login", title: "Example", url: "https://example.com", username: "june", password: "secret", }); expect(vault.getEntries()).toContainEqual(expect.objectContaining({ id: entry.id, title: "Example" })); expect(vaultId).toBe(await computeVaultId(new Uint8Array(Buffer.from(rootKeyHex, "hex")))); }); it("T-A06: refuses to overwrite an existing local vault", async () => { const vault = await import("./vault"); await vault.createVault(RECEIVER_PASSWORD); const originalRoot = vault.getRootKeyHex(); await vault.addEntry({ type: "login", title: "Must survive", username: "june", password: "local-only", }); await expect( vault.createVaultFromRootKey(RECEIVER_PASSWORD, "44".repeat(32)), ).rejects.toThrow("ADOPT_VAULT_ALREADY_EXISTS"); expect(vault.getRootKeyHex()).toBe(originalRoot); expect(vault.getEntries()).toContainEqual( expect.objectContaining({ title: "Must survive" }), ); }); it("T-A30: storage failure leaves the receiver vault-less with no partial slot", async () => { const mock = makeChromeStorageMock(); const storageSet = mock.chrome.storage.local.set as unknown as ReturnType; storageSet.mockRejectedValueOnce(new Error("quota")); vi.stubGlobal("chrome", mock.chrome); const vault = await import("./vault"); await expect(vault.createVaultFromRootKey(RECEIVER_PASSWORD, "77".repeat(32))) .rejects.toThrow(/STORAGE_QUOTA_EXCEEDED|quota/); expect(mock.storage.peach_vault_a).toBeUndefined(); expect(mock.storage.peach_vault_b).toBeUndefined(); expect(await vault.hasVault()).toBe(false); expect(vault.isUnlocked()).toBe(false); }); it("T-A30: commits slot A, slot B, and timestamp together", async () => { const mock = makeChromeStorageMock(); vi.stubGlobal("chrome", mock.chrome); const vault = await import("./vault"); await vault.createVaultFromRootKey(RECEIVER_PASSWORD, "88".repeat(32)); const storageSet = mock.chrome.storage.local.set as unknown as ReturnType; const commitCall = storageSet.mock.calls.find(([payload]) => ( Boolean((payload as StorageShape).peach_vault_a) && Boolean((payload as StorageShape).peach_vault_b) )); expect(commitCall).toBeDefined(); expect((commitCall![0] as StorageShape).peach_vault_a).toMatchObject({ version: 1 } satisfies Partial); expect((commitCall![0] as StorageShape).peach_vault_b).toMatchObject({ version: 1 } satisfies Partial); expect((commitCall![0] as StorageShape).peach_last_modified_at).toEqual(expect.any(String)); }); it("rolls back both stored slots and clears the unlocked adopted vault", async () => { const mock = makeChromeStorageMock(); vi.stubGlobal("chrome", mock.chrome); const vault = await import("./vault"); await vault.createVaultFromRootKey(RECEIVER_PASSWORD, "99".repeat(32)); expect(vault.isUnlocked()).toBe(true); expect(mock.storage.peach_vault_a).toBeDefined(); expect(mock.storage.peach_vault_b).toBeDefined(); expect(mock.storage.peach_last_modified_at).toBeDefined(); await vault.rollbackAdoptedVault(); expect(vault.isUnlocked()).toBe(false); expect(vault.getRootKeyHex()).toBeNull(); expect(() => vault.getEntries()).toThrow("vault_locked"); expect(await vault.hasVault()).toBe(false); expect(mock.storage.peach_vault_a).toBeUndefined(); expect(mock.storage.peach_vault_b).toBeUndefined(); expect(mock.storage.peach_last_modified_at).toBeUndefined(); }); });