paired-devices.test.ts

5.9 KB · raw

1import { describe, expect, it, beforeEach } from "vitest";2import type { PairedDevice } from "../shared/sync-types";3import {4  clearLastKnownIp,5  clearPairedDevices,6  findPairedDeviceById,7  findPairedDeviceByPubkey,8  getLastKnownIp,9  getLastSyncAt,10  listPairedDevices,11  markLastSyncedAt,12  removePairedDeviceById,13  setLastKnownIp,14  setLastSyncAt,15  upsertPairedDevice,16} from "./paired-devices";1718function makeStorageStub() {19  const sync = new Map<string, unknown>();20  const local = new Map<string, unknown>();21  function area(store: Map<string, unknown>) {22    return {23      async get(key?: string | string[] | null) {24        if (key === null || key === undefined) {25          const all: Record<string, unknown> = {};26          for (const [k, v] of store) all[k] = v;27          return all;28        }29        const keys = Array.isArray(key) ? key : [key];30        const out: Record<string, unknown> = {};31        for (const k of keys) if (store.has(k)) out[k] = store.get(k);32        return out;33      },34      async set(values: Record<string, unknown>) {35        for (const [k, v] of Object.entries(values)) store.set(k, v);36      },37      async remove(key: string | string[]) {38        const keys = Array.isArray(key) ? key : [key];39        for (const k of keys) store.delete(k);40      },41    };42  }43  (globalThis as unknown as { chrome: unknown }).chrome = {44    storage: { sync: area(sync), local: area(local) },45  };46  return { sync, local };47}4849const SAMPLE: PairedDevice = {50  deviceId: "AAA-111",51  deviceName: "June's Pixel 9",52  deviceType: "android",53  longTermPublicKeyHex: "0".repeat(64),54  pairedAt: 1700000000000,55};5657describe("paired-devices registry", () => {58  beforeEach(() => makeStorageStub());5960  it("upsert + list", async () => {61    await upsertPairedDevice(SAMPLE);62    expect(await listPairedDevices()).toEqual([SAMPLE]);63  });6465  it("upsert is idempotent on the same deviceId — updates in place, no duplicate", async () => {66    await upsertPairedDevice(SAMPLE);67    await upsertPairedDevice({ ...SAMPLE, deviceName: "Renamed Pixel" });68    const all = await listPairedDevices();69    expect(all).toHaveLength(1);70    expect(all[0].deviceName).toBe("Renamed Pixel");71  });7273  it("upsert with a new longTermPublicKeyHex on re-pair replaces the old key", async () => {74    await upsertPairedDevice(SAMPLE);75    const newPubkey = "f".repeat(64);76    await upsertPairedDevice({ ...SAMPLE, longTermPublicKeyHex: newPubkey });77    const found = await findPairedDeviceById(SAMPLE.deviceId);78    expect(found?.longTermPublicKeyHex).toBe(newPubkey);79  });8081  it("rejects upsert with missing fields", async () => {82    await expect(upsertPairedDevice({ ...SAMPLE, deviceId: "" })).rejects.toThrow(/deviceId/);83    await expect(upsertPairedDevice({ ...SAMPLE, longTermPublicKeyHex: undefined })).rejects.toThrow(/longTermPublicKeyHex/);84    await expect(upsertPairedDevice({ ...SAMPLE, deviceName: undefined })).rejects.toThrow(/deviceName/);85    await expect(upsertPairedDevice({ ...SAMPLE, deviceType: undefined })).rejects.toThrow(/deviceType/);86  });8788  it("findPairedDeviceByPubkey returns the correct device", async () => {89    const other: PairedDevice = { ...SAMPLE, deviceId: "BBB-222", longTermPublicKeyHex: "1".repeat(64) };90    await upsertPairedDevice(SAMPLE);91    await upsertPairedDevice(other);92    expect(await findPairedDeviceByPubkey("1".repeat(64))).toEqual(other);93    expect(await findPairedDeviceByPubkey("9".repeat(64))).toBeNull();94  });9596  it("removePairedDeviceById drops the device and its IP cache", async () => {97    await upsertPairedDevice(SAMPLE);98    await setLastKnownIp(SAMPLE.longTermPublicKeyHex!, "192.168.1.42");99    await removePairedDeviceById(SAMPLE.deviceId);100    expect(await listPairedDevices()).toEqual([]);101    expect(await getLastKnownIp(SAMPLE.longTermPublicKeyHex!)).toBeNull();102  });103104  it("clearPairedDevices wipes the whole registry", async () => {105    await upsertPairedDevice(SAMPLE);106    await clearPairedDevices();107    expect(await listPairedDevices()).toEqual([]);108  });109110  it("markLastSyncedAt mutates only the target device", async () => {111    const other: PairedDevice = { ...SAMPLE, deviceId: "BBB-222", longTermPublicKeyHex: "1".repeat(64) };112    await upsertPairedDevice(SAMPLE);113    await upsertPairedDevice(other);114    await markLastSyncedAt(SAMPLE.deviceId, 1700001000000);115    const found = await findPairedDeviceById(SAMPLE.deviceId);116    expect(found?.lastSyncedAt).toBe(1700001000000);117    const untouched = await findPairedDeviceById(other.deviceId);118    expect(untouched?.lastSyncedAt).toBeUndefined();119  });120121  it("getLastKnownIp / setLastKnownIp round-trip", async () => {122    await setLastKnownIp("ab".repeat(32), "10.0.0.5");123    expect(await getLastKnownIp("ab".repeat(32))).toBe("10.0.0.5");124    await clearLastKnownIp("ab".repeat(32));125    expect(await getLastKnownIp("ab".repeat(32))).toBeNull();126  });127128  it("last sync timestamp accepts a legacy string value", async () => {129    (globalThis as unknown as { chrome: { storage: { local: { set: (v: Record<string, unknown>) => Promise<void> } } } }).chrome.storage.local.set({130      peach_last_sync_at: "1700002000000",131    });132    expect(await getLastSyncAt()).toBe(1700002000000);133  });134135  it("setLastSyncAt persists a numeric value", async () => {136    await setLastSyncAt(1700003000000);137    expect(await getLastSyncAt()).toBe(1700003000000);138  });139140  it("listPairedDevices ignores garbage entries silently", async () => {141    (globalThis as unknown as { chrome: { storage: { sync: { set: (v: Record<string, unknown>) => Promise<void> } } } }).chrome.storage.sync.set({142      peach_paired_devices_v3: [143        { deviceId: "real", pairedAt: 1, deviceName: "x", deviceType: "android", longTermPublicKeyHex: "0".repeat(64) },144        { not: "a device" },145        null,146        42,147      ],148    });149    const devices = await listPairedDevices();150    expect(devices).toHaveLength(1);151    expect(devices[0].deviceId).toBe("real");152  });153});