vault.entry-id-namespace.test.ts

4.2 KB · raw

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}$/;3536/**37 * A Peach entry id is a UUID, but an imported or restored file carries whatever38 * id its author used and no reader bounds it. One oversized id would ride into39 * every future sync frame and be rejected there — a permanent, opaque failure40 * on both platforms, caused by data that was already local.41 *42 * The seam is restoreEntries, and only restoreEntries. Sync adopts a peer's id43 * verbatim, which is the only thing keeping two devices agreed on which entry44 * they are discussing, so reminting there would fork identity on every device.45 */46describe("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});