paired-devices.test.ts

6.4 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  setRelayUrlForAllPairedDevices,16  upsertPairedDevice,17} from "./paired-devices";1819function makeStorageStub() {20  const sync = new Map<string, unknown>();21  const local = new Map<string, unknown>();22  function area(store: Map<string, unknown>) {23    return {24      async get(key?: string | string[] | null) {25        if (key === null || key === undefined) {26          const all: Record<string, unknown> = {};27          for (const [k, v] of store) all[k] = v;28          return all;29        }30        const keys = Array.isArray(key) ? key : [key];31        const out: Record<string, unknown> = {};32        for (const k of keys) if (store.has(k)) out[k] = store.get(k);33        return out;34      },35      async set(values: Record<string, unknown>) {36        for (const [k, v] of Object.entries(values)) store.set(k, v);37      },38      async remove(key: string | string[]) {39        const keys = Array.isArray(key) ? key : [key];40        for (const k of keys) store.delete(k);41      },42    };43  }44  (globalThis as unknown as { chrome: unknown }).chrome = {45    storage: { sync: area(sync), local: area(local) },46  };47  return { sync, local };48}4950const SAMPLE: PairedDevice = {51  deviceId: "AAA-111",52  deviceName: "June's Pixel 9",53  deviceType: "android",54  longTermPublicKeyHex: "0".repeat(64),55  pairedAt: 1700000000000,56};5758describe("paired-devices registry", () => {59  beforeEach(() => makeStorageStub());6061  it("upsert + list", async () => {62    await upsertPairedDevice(SAMPLE);63    expect(await listPairedDevices()).toEqual([SAMPLE]);64  });6566  it("upsert is idempotent on the same deviceId — updates in place, no duplicate", async () => {67    await upsertPairedDevice(SAMPLE);68    await upsertPairedDevice({ ...SAMPLE, deviceName: "Renamed Pixel" });69    const all = await listPairedDevices();70    expect(all).toHaveLength(1);71    expect(all[0].deviceName).toBe("Renamed Pixel");72  });7374  it("upsert with a new longTermPublicKeyHex on re-pair replaces the old key", async () => {75    await upsertPairedDevice(SAMPLE);76    const newPubkey = "f".repeat(64);77    await upsertPairedDevice({ ...SAMPLE, longTermPublicKeyHex: newPubkey });78    const found = await findPairedDeviceById(SAMPLE.deviceId);79    expect(found?.longTermPublicKeyHex).toBe(newPubkey);80  });8182  it("rejects upsert with missing fields", async () => {83    await expect(upsertPairedDevice({ ...SAMPLE, deviceId: "" })).rejects.toThrow(/deviceId/);84    await expect(upsertPairedDevice({ ...SAMPLE, longTermPublicKeyHex: undefined })).rejects.toThrow(/longTermPublicKeyHex/);85    await expect(upsertPairedDevice({ ...SAMPLE, deviceName: undefined })).rejects.toThrow(/deviceName/);86    await expect(upsertPairedDevice({ ...SAMPLE, deviceType: undefined })).rejects.toThrow(/deviceType/);87  });8889  it("findPairedDeviceByPubkey returns the correct device", async () => {90    const other: PairedDevice = { ...SAMPLE, deviceId: "BBB-222", longTermPublicKeyHex: "1".repeat(64) };91    await upsertPairedDevice(SAMPLE);92    await upsertPairedDevice(other);93    expect(await findPairedDeviceByPubkey("1".repeat(64))).toEqual(other);94    expect(await findPairedDeviceByPubkey("9".repeat(64))).toBeNull();95  });9697  it("removePairedDeviceById drops the device and its IP cache", async () => {98    await upsertPairedDevice(SAMPLE);99    await setLastKnownIp(SAMPLE.longTermPublicKeyHex!, "192.168.1.42");100    await removePairedDeviceById(SAMPLE.deviceId);101    expect(await listPairedDevices()).toEqual([]);102    expect(await getLastKnownIp(SAMPLE.longTermPublicKeyHex!)).toBeNull();103  });104105  it("clearPairedDevices wipes the whole registry", async () => {106    await upsertPairedDevice(SAMPLE);107    await clearPairedDevices();108    expect(await listPairedDevices()).toEqual([]);109  });110111  it("markLastSyncedAt mutates only the target device", async () => {112    const other: PairedDevice = { ...SAMPLE, deviceId: "BBB-222", longTermPublicKeyHex: "1".repeat(64) };113    await upsertPairedDevice(SAMPLE);114    await upsertPairedDevice(other);115    await markLastSyncedAt(SAMPLE.deviceId, 1700001000000);116    const found = await findPairedDeviceById(SAMPLE.deviceId);117    expect(found?.lastSyncedAt).toBe(1700001000000);118    const untouched = await findPairedDeviceById(other.deviceId);119    expect(untouched?.lastSyncedAt).toBeUndefined();120  });121122  it("moves every existing pair to a new mesh relay", async () => {123    const other: PairedDevice = {124      ...SAMPLE,125      deviceId: "BBB-222",126      longTermPublicKeyHex: "1".repeat(64),127      relayUrl: "wss://old-relay.example",128    };129    await upsertPairedDevice(SAMPLE);130    await upsertPairedDevice(other);131132    await setRelayUrlForAllPairedDevices("wss://new-relay.example/base");133134    expect((await listPairedDevices()).map((device) => device.relayUrl)).toEqual([135      "wss://new-relay.example/base",136      "wss://new-relay.example/base",137    ]);138  });139140  it("getLastKnownIp / setLastKnownIp round-trip", async () => {141    await setLastKnownIp("ab".repeat(32), "10.0.0.5");142    expect(await getLastKnownIp("ab".repeat(32))).toBe("10.0.0.5");143    await clearLastKnownIp("ab".repeat(32));144    expect(await getLastKnownIp("ab".repeat(32))).toBeNull();145  });146147  it("last sync timestamp accepts a legacy string value", async () => {148    (globalThis as unknown as { chrome: { storage: { local: { set: (v: Record<string, unknown>) => Promise<void> } } } }).chrome.storage.local.set({149      peach_last_sync_at: "1700002000000",150    });151    expect(await getLastSyncAt()).toBe(1700002000000);152  });153154  it("setLastSyncAt persists a numeric value", async () => {155    await setLastSyncAt(1700003000000);156    expect(await getLastSyncAt()).toBe(1700003000000);157  });158159  it("listPairedDevices ignores garbage entries silently", async () => {160    (globalThis as unknown as { chrome: { storage: { local: { set: (v: Record<string, unknown>) => Promise<void> } } } }).chrome.storage.local.set({161      peach_paired_devices_v3: [162        { deviceId: "real", pairedAt: 1, deviceName: "x", deviceType: "android", longTermPublicKeyHex: "0".repeat(64) },163        { not: "a device" },164        null,165        42,166      ],167    });168    const devices = await listPairedDevices();169    expect(devices).toHaveLength(1);170    expect(devices[0].deviceId).toBe("real");171  });172});