device-identity.test.ts

3.0 KB · raw

1import { beforeEach, describe, expect, it, vi } from "vitest";23// PCH-01-004: deviceId must live in chrome.storage.local (device-local), never4// in chrome.storage.sync (which roams across profiles and collides identities).56type StorageShape = Record<string, unknown>;7const DEVICE_ID_KEY = "peach_device_id";89describe("device-identity getOrCreateDeviceId (PCH-01-004)", () => {10  let local: StorageShape;11  let sync: StorageShape;12  let localSet: ReturnType<typeof vi.fn>;13  let syncSet: ReturnType<typeof vi.fn>;14  let syncRemove: ReturnType<typeof vi.fn>;1516  function makeArea(store: StorageShape, set: ReturnType<typeof vi.fn>, remove?: ReturnType<typeof vi.fn>) {17    return {18      get: vi.fn(async (key?: string | string[]) => {19        if (!key) return store;20        if (typeof key === "string") return { [key]: store[key] };21        return key.reduce<Record<string, unknown>>((r, k) => {22          r[k] = store[k];23          return r;24        }, {});25      }),26      set,27      remove: remove ?? vi.fn(async () => undefined),28    };29  }3031  beforeEach(() => {32    vi.resetModules();33    local = {};34    sync = {};35    localSet = vi.fn(async (next: Record<string, unknown>) => {36      Object.assign(local, next);37    });38    syncSet = vi.fn(async (next: Record<string, unknown>) => {39      Object.assign(sync, next);40    });41    syncRemove = vi.fn(async (key: string) => {42      delete sync[key];43    });4445    const chromeMock = {46      storage: {47        local: makeArea(local, localSet),48        sync: makeArea(sync, syncSet, syncRemove),49      },50    } as unknown as typeof chrome;51    (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;52  });5354  it("fresh_install_writes_id_to_local_only", async () => {55    const vault = await import("./vault");56    await vault.createVault("identity-test-password");57    const { getOrCreateDeviceId } = await import("./device-identity");5859    const id = await getOrCreateDeviceId();6061    expect(typeof id).toBe("string");62    expect(id.length).toBeGreaterThan(0);63    expect(local[DEVICE_ID_KEY]).toBeUndefined();64    expect(local["peach_local_identity_v2"]).toBeDefined();65    expect(syncSet).not.toHaveBeenCalled();66  });6768  it("returns_a_stable_id_across_calls", async () => {69    const vault = await import("./vault");70    await vault.createVault("identity-test-password");71    const { getOrCreateDeviceId } = await import("./device-identity");7273    const first = await getOrCreateDeviceId();74    const second = await getOrCreateDeviceId();7576    expect(second).toBe(first);77  });7879  it("replaces_a_copied_local_id_when_adoption_reports_a_collision", async () => {80    const vault = await import("./vault");81    await vault.createVault("identity-test-password");82    const { getOrCreateDeviceId, rotateDeviceId } = await import("./device-identity");8384    const original = await getOrCreateDeviceId();85    const fresh = await rotateDeviceId();8687    expect(fresh).not.toBe(original);88    expect(local["peach_local_identity_v2"]).toBeDefined();89    expect(await getOrCreateDeviceId()).toBe(fresh);90    expect(syncSet).not.toHaveBeenCalled();91  });92});