1234import { 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