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});