1import { beforeEach, describe, expect, it, vi } from "vitest";2import type { StoredVault } from "../shared/types";34type StorageShape = Record<string, unknown>;56function makeChromeStorageMock(storage: StorageShape) {7 return {8 storage,9 chrome: {10 storage: {11 local: {12 get: vi.fn(async (key?: string | string[]) => {13 if (!key) return { ...storage };14 if (typeof key === "string") return { [key]: storage[key] };15 return key.reduce<StorageShape>((acc, k) => {16 acc[k] = storage[k];17 return acc;18 }, {});19 }),20 set: vi.fn(async (next: StorageShape) => {21 Object.assign(storage, next);22 }),23 remove: vi.fn(async (keys: string | string[]) => {24 for (const key of Array.isArray(keys) ? keys : [keys]) delete storage[key];25 }),26 },27 },28 alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() } },29 idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() } },30 } as unknown as typeof globalThis.chrome,31 };32}3334const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;353637383940414243444546describe("vault entry-id namespace", () => {47 let storage: StorageShape;48 let vault: typeof import("./vault");49 const password = "entry-id-namespace-pass";5051 beforeEach(async () => {52 vi.resetModules();53 storage = {};54 (globalThis as Record<string, unknown>).chrome = makeChromeStorageMock(storage).chrome;55 vi.stubGlobal("fetch", vi.fn(async () => {56 throw new Error("network disabled in tests");57 }));58 vault = await import("./vault");59 await vault.createVault(password);60 });6162 function imported(id: string, title: string) {63 return {64 id,65 type: "login" as const,66 title,67 password: "pw",68 createdAt: 1,69 modifiedAt: Date.now(),70 };71 }7273 it("keeps a well-formed id the file already carried", async () => {74 const fromAnotherPeach = "b1f0c8e2-4a6d-4f1a-9c3e-7d5b2a8e1f40";75 const fromAVendor = "sj4kd9f2q7m1x8zb3nv6ct0h";76 const summary = await vault.restoreEntries(77 [imported(fromAnotherPeach, "Bitwarden"), imported(fromAVendor, "OnePassword")],78 "replace",79 );80 expect(summary.added).toBe(2);81 expect(vault.getEntries().map((e) => e.id).sort()).toEqual(82 [fromAVendor, fromAnotherPeach].sort(),83 );84 });8586 it("mints a fresh id for one that is not a Peach id, keeping the entry", async () => {87 const summary = await vault.restoreEntries(88 [89 imported("x".repeat(200), "Too long"),90 imported('a b"c\n{"injected":true}', "Not an id"),91 ],92 "replace",93 );94 expect(summary.added).toBe(2);95 const minted = vault.getEntries();96 expect(minted.map((e) => e.title).sort()).toEqual(["Not an id", "Too long"]);97 expect(new Set(minted.map((e) => e.id)).size).toBe(2);98 for (const entry of minted) expect(entry.id).toMatch(UUID_SHAPE);99 });100101 it("adopts a sync peer's id verbatim", async () => {102 const peerId = `peer-${"x".repeat(120)}`;103 const result = await vault.applyMergedRemoteEntries(104 [{ ...imported(peerId, "From peer") }],105 {},106 );107 expect(result.applied).toBe(1);108 expect(vault.getEntries().map((e) => e.id)).toEqual([peerId]);109 });110111 it("persists the naturalised ids across a lock and unlock", async () => {112 await vault.restoreEntries([imported("x".repeat(200), "Too long")], "replace");113 const minted = vault.getEntries()[0].id;114 vault.lock();115 await vault.unlock(password);116 expect(vault.getEntries().map((e) => e.id)).toEqual([minted]);117 expect(storage.peach_vault_a as StoredVault).toBeDefined();118 });119});