import { describe, expect, it, beforeEach } from "vitest"; import type { PairedDevice } from "../shared/sync-types"; import { clearPairedDevices, findPairedDeviceById, findPairedDeviceByPubkey, getLastSyncAt, listPairedDevices, markLastSyncedAt, isDeviceRevoked, listDeviceRevocations, mergeDeviceRevocations, repairPairedRevocationOverlaps, removePairedDeviceById, renamePairedDevice, revokeDeviceById, setLastSyncAt, setRelayUrlForAllPairedDevices, upsertPairedDevice, } from "./paired-devices"; function makeStorageStub() { const sync = new Map(); const local = new Map(); function area(store: Map) { return { async get(key?: string | string[] | null) { if (key === null || key === undefined) { const all: Record = {}; for (const [k, v] of store) all[k] = v; return all; } const keys = Array.isArray(key) ? key : [key]; const out: Record = {}; for (const k of keys) if (store.has(k)) out[k] = store.get(k); return out; }, async set(values: Record) { for (const [k, v] of Object.entries(values)) store.set(k, v); }, async remove(key: string | string[]) { const keys = Array.isArray(key) ? key : [key]; for (const k of keys) store.delete(k); }, }; } (globalThis as unknown as { chrome: unknown }).chrome = { storage: { sync: area(sync), local: area(local) }, }; return { sync, local }; } const SAMPLE: PairedDevice = { deviceId: "AAA-111", deviceName: "June's Pixel 9", deviceType: "android", longTermPublicKeyHex: "0".repeat(64), pairedAt: 1700000000000, }; describe("paired-devices registry", () => { beforeEach(() => makeStorageStub()); it("upsert + list", async () => { await upsertPairedDevice(SAMPLE); expect(await listPairedDevices()).toEqual([SAMPLE]); }); it("upsert is idempotent on the same deviceId — updates in place, no duplicate", async () => { await upsertPairedDevice(SAMPLE); await upsertPairedDevice({ ...SAMPLE, deviceName: "Renamed Pixel" }); const all = await listPairedDevices(); expect(all).toHaveLength(1); expect(all[0].deviceName).toBe("Renamed Pixel"); }); it("stores a local alias without changing the peer-reported name or identity", async () => { await upsertPairedDevice(SAMPLE); await renamePairedDevice(SAMPLE.deviceId, " Travel phone "); const renamed = await findPairedDeviceById(SAMPLE.deviceId); expect(renamed).toMatchObject({ deviceName: SAMPLE.deviceName, localAlias: "Travel phone", longTermPublicKeyHex: SAMPLE.longTermPublicKeyHex, }); }); it("rejects blank, oversized, and unknown-device aliases", async () => { await upsertPairedDevice(SAMPLE); await expect(renamePairedDevice(SAMPLE.deviceId, " ")).rejects.toThrow(/empty/); await expect(renamePairedDevice(SAMPLE.deviceId, "x".repeat(65))).rejects.toThrow(/64/); await expect(renamePairedDevice("missing", "Phone")).rejects.toThrow(/not_found/); }); it("upsert with a new longTermPublicKeyHex on re-pair replaces the old key", async () => { await upsertPairedDevice(SAMPLE); const newPubkey = "f".repeat(64); await upsertPairedDevice({ ...SAMPLE, longTermPublicKeyHex: newPubkey }); const found = await findPairedDeviceById(SAMPLE.deviceId); expect(found?.longTermPublicKeyHex).toBe(newPubkey); }); it("rejects upsert with missing fields", async () => { await expect(upsertPairedDevice({ ...SAMPLE, deviceId: "" })).rejects.toThrow(/deviceId/); await expect(upsertPairedDevice({ ...SAMPLE, longTermPublicKeyHex: undefined })).rejects.toThrow(/longTermPublicKeyHex/); await expect(upsertPairedDevice({ ...SAMPLE, deviceName: undefined })).rejects.toThrow(/deviceName/); await expect(upsertPairedDevice({ ...SAMPLE, deviceType: undefined })).rejects.toThrow(/deviceType/); }); it("caps oversized peer device names at the registry seam", async () => { await upsertPairedDevice({ ...SAMPLE, deviceId: "LONG-1", deviceName: "N".repeat(100) }); const found = await findPairedDeviceById("LONG-1"); expect(found?.deviceName).toBe("N".repeat(64)); }); it("findPairedDeviceByPubkey returns the correct device", async () => { const other: PairedDevice = { ...SAMPLE, deviceId: "BBB-222", longTermPublicKeyHex: "1".repeat(64) }; await upsertPairedDevice(SAMPLE); await upsertPairedDevice(other); expect(await findPairedDeviceByPubkey("1".repeat(64))).toEqual(other); expect(await findPairedDeviceByPubkey("9".repeat(64))).toBeNull(); }); it("removePairedDeviceById drops the device", async () => { await upsertPairedDevice(SAMPLE); await removePairedDeviceById(SAMPLE.deviceId); expect(await listPairedDevices()).toEqual([]); }); it("clearPairedDevices wipes the whole registry", async () => { await upsertPairedDevice(SAMPLE); await clearPairedDevices(); expect(await listPairedDevices()).toEqual([]); }); it("markLastSyncedAt mutates only the target device", async () => { const other: PairedDevice = { ...SAMPLE, deviceId: "BBB-222", longTermPublicKeyHex: "1".repeat(64) }; await upsertPairedDevice(SAMPLE); await upsertPairedDevice(other); await markLastSyncedAt(SAMPLE.deviceId, 1700001000000); const found = await findPairedDeviceById(SAMPLE.deviceId); expect(found?.lastSyncedAt).toBe(1700001000000); const untouched = await findPairedDeviceById(other.deviceId); expect(untouched?.lastSyncedAt).toBeUndefined(); }); it("moves every existing pair to a new mesh relay", async () => { const other: PairedDevice = { ...SAMPLE, deviceId: "BBB-222", longTermPublicKeyHex: "1".repeat(64), relayUrl: "wss://old-relay.example", }; await upsertPairedDevice(SAMPLE); await upsertPairedDevice(other); await setRelayUrlForAllPairedDevices("wss://new-relay.example/base"); expect((await listPairedDevices()).map((device) => device.relayUrl)).toEqual([ "wss://new-relay.example/base", "wss://new-relay.example/base", ]); }); it("last sync timestamp accepts a legacy string value", async () => { (globalThis as unknown as { chrome: { storage: { local: { set: (v: Record) => Promise } } } }).chrome.storage.local.set({ peach_last_sync_at: "1700002000000", }); expect(await getLastSyncAt()).toBe(1700002000000); }); it("setLastSyncAt persists a numeric value", async () => { await setLastSyncAt(1700003000000); expect(await getLastSyncAt()).toBe(1700003000000); }); it("listPairedDevices ignores garbage entries silently", async () => { (globalThis as unknown as { chrome: { storage: { local: { set: (v: Record) => Promise } } } }).chrome.storage.local.set({ peach_paired_devices_v3: [ { deviceId: "real", pairedAt: 1, deviceName: "x", deviceType: "android", longTermPublicKeyHex: "0".repeat(64) }, { not: "a device" }, null, 42, ], }); const devices = await listPairedDevices(); expect(devices).toHaveLength(1); expect(devices[0].deviceId).toBe("real"); }); describe("device revocation", () => { it("revokeDeviceById records the device's key and drops it from the registry", async () => { await upsertPairedDevice(SAMPLE); const record = await revokeDeviceById(SAMPLE.deviceId, "this-device", { now: 1_700_000_000_000, }); expect(record).toMatchObject({ deviceId: SAMPLE.deviceId, longTermPublicKeyHex: SAMPLE.longTermPublicKeyHex, revokedAt: 1_700_000_000_000, revokedByDeviceId: "this-device", }); expect(await findPairedDeviceById(SAMPLE.deviceId)).toBeNull(); expect(await isDeviceRevoked(SAMPLE.deviceId)).toBe(true); }); it("revoking twice is a no-op rather than a duplicate record", async () => { await upsertPairedDevice(SAMPLE); await revokeDeviceById(SAMPLE.deviceId, "this-device"); expect(await revokeDeviceById(SAMPLE.deviceId, "this-device")).toBeNull(); expect(await listDeviceRevocations()).toHaveLength(1); }); it("an existing revocation still removes a stale re-paired row", async () => { await upsertPairedDevice(SAMPLE); await revokeDeviceById(SAMPLE.deviceId, "this-device"); // Reproduce the real corrupt overlap created by older direct-pair // handlers: the row returned but its terminal revocation remained. await upsertPairedDevice(SAMPLE); expect(await revokeDeviceById(SAMPLE.deviceId, "this-device")).toBeNull(); expect(await findPairedDeviceById(SAMPLE.deviceId)).toBeNull(); expect(await listDeviceRevocations()).toHaveLength(1); }); it("mergeDeviceRevocations reports only the ids it had not already seen", async () => { const first = await mergeDeviceRevocations([ { deviceId: "a", longTermPublicKeyHex: "a".repeat(64), revokedAt: 1, revokedByDeviceId: "x" }, ]); expect(first).toEqual(["a"]); const second = await mergeDeviceRevocations([ { deviceId: "a", longTermPublicKeyHex: "a".repeat(64), revokedAt: 2, revokedByDeviceId: "y" }, { deviceId: "b", longTermPublicKeyHex: "b".repeat(64), revokedAt: 3, revokedByDeviceId: "y" }, ]); expect(second).toEqual(["b"]); expect(await listDeviceRevocations()).toHaveLength(2); }); it("a later revokedAt cannot displace an existing record — revocation is terminal", async () => { await mergeDeviceRevocations([ { deviceId: "a", longTermPublicKeyHex: "a".repeat(64), revokedAt: 1, revokedByDeviceId: "x" }, ]); await mergeDeviceRevocations([ { deviceId: "a", longTermPublicKeyHex: "c".repeat(64), revokedAt: 9_999, revokedByDeviceId: "attacker" }, ]); const records = await listDeviceRevocations(); expect(records).toHaveLength(1); expect(records[0]).toMatchObject({ revokedAt: 1, revokedByDeviceId: "x" }); }); it("HOLE 2: a far-future revokedAt is clamped rather than stored verbatim", async () => { const farFuture = Date.now() + 10 * 365 * 24 * 60 * 60 * 1000; await mergeDeviceRevocations([ { deviceId: "a", longTermPublicKeyHex: "a".repeat(64), revokedAt: farFuture, revokedByDeviceId: "peer" }, ]); const [record] = await listDeviceRevocations(); expect(record.revokedAt).toBeLessThanOrEqual(Date.now() + 5 * 60 * 1000); }); it("startup repair keeps a newer direct pair and clears its older revocation", async () => { const now = Date.now(); await mergeDeviceRevocations([{ deviceId: SAMPLE.deviceId, longTermPublicKeyHex: SAMPLE.longTermPublicKeyHex!, revokedAt: now - 1_000, revokedByDeviceId: "this-device", }]); await upsertPairedDevice({ ...SAMPLE, pairedAt: now, trustSource: "direct", }); expect(await repairPairedRevocationOverlaps()).toEqual({ revocationsCleared: 1, pairedRowsRemoved: 0, }); expect(await findPairedDeviceById(SAMPLE.deviceId)).not.toBeNull(); expect(await isDeviceRevoked(SAMPLE.deviceId)).toBe(false); }); it("startup repair removes a direct pair older than its revocation", async () => { const now = Date.now(); await upsertPairedDevice({ ...SAMPLE, pairedAt: now - 2_000, trustSource: "direct", }); await mergeDeviceRevocations([{ deviceId: SAMPLE.deviceId, longTermPublicKeyHex: SAMPLE.longTermPublicKeyHex!, revokedAt: now - 1_000, revokedByDeviceId: "this-device", }]); expect(await repairPairedRevocationOverlaps()).toEqual({ revocationsCleared: 0, pairedRowsRemoved: 1, }); expect(await findPairedDeviceById(SAMPLE.deviceId)).toBeNull(); expect(await isDeviceRevoked(SAMPLE.deviceId)).toBe(true); }); it("startup repair never lets a newer vouched pair lift a revocation", async () => { const now = Date.now(); await mergeDeviceRevocations([{ deviceId: SAMPLE.deviceId, longTermPublicKeyHex: SAMPLE.longTermPublicKeyHex!, revokedAt: now - 1_000, revokedByDeviceId: "this-device", }]); await upsertPairedDevice({ ...SAMPLE, pairedAt: now, trustSource: "vouched", }); expect(await repairPairedRevocationOverlaps()).toEqual({ revocationsCleared: 0, pairedRowsRemoved: 1, }); expect(await findPairedDeviceById(SAMPLE.deviceId)).toBeNull(); expect(await isDeviceRevoked(SAMPLE.deviceId)).toBe(true); }); }); });