1import { beforeEach, describe, expect, it, vi } from "vitest";23type MessageListener = (message: unknown) => void;4type DisconnectListener = () => void;56function installChromeMock() {7 const messageListeners: MessageListener[] = [];8 const disconnectListeners: DisconnectListener[] = [];9 const port = {10 postMessage: vi.fn(),11 disconnect: vi.fn(() => {12 for (const listener of disconnectListeners) listener();13 }),14 onMessage: {15 addListener: vi.fn((listener: MessageListener) => {16 messageListeners.push(listener);17 }),18 },19 onDisconnect: {20 addListener: vi.fn((listener: DisconnectListener) => {21 disconnectListeners.push(listener);22 }),23 },24 };25 const connectNative = vi.fn(() => port);2627 vi.stubGlobal("chrome", {28 runtime: {29 connectNative,30 },31 });3233 return {34 connectNative,35 port,36 emit(message: unknown) {37 for (const listener of messageListeners) listener(message);38 },39 disconnect() {40 for (const listener of disconnectListeners) listener();41 },42 };43}4445describe("sync native host client", () => {46 beforeEach(() => {47 vi.resetModules();48 vi.useRealTimers();49 vi.unstubAllGlobals();50 });5152 it("requests the native identity using the Rust host protocol", async () => {53 const chromeMock = installChromeMock();54 const sync = await import("./sync");5556 const promise = sync.getNativeIdentity();57 expect(chromeMock.connectNative).toHaveBeenCalledWith("com.peachpasswords.sync");58 expect(chromeMock.port.postMessage).toHaveBeenCalledWith({ type: "GET_IDENTITY" });5960 chromeMock.emit({ type: "IDENTITY", public_key_hex: "aa".repeat(32) });61 await expect(promise).resolves.toBe("aa".repeat(32));62 });6364 it("sends trusted-device control messages with snake_case fields", async () => {65 const chromeMock = installChromeMock();66 const sync = await import("./sync");6768 const promise = sync.trustDevice("bb".repeat(32), "June's iPhone");69 expect(chromeMock.port.postMessage).toHaveBeenCalledWith({70 type: "TRUST_DEVICE",71 public_key_hex: "bb".repeat(32),72 device_name: "June's iPhone",73 });7475 chromeMock.emit({ type: "TRUST_DEVICE_OK" });76 await expect(promise).resolves.toBeUndefined();77 });7879 it("maps native peer discovery events for subscribers", async () => {80 const chromeMock = installChromeMock();81 const sync = await import("./sync");82 const handler = vi.fn();8384 sync.connectSyncHost();85 sync.onPeerDiscovered(handler);86 chromeMock.emit({87 type: "PEER_DISCOVERED",88 peer: {89 host: "100.64.0.2",90 port: 49422,91 x25519_public_key: "remote-key",92 device_id: "device-id",93 name: "June's MacBook",94 pairing: true,95 },96 });9798 expect(handler).toHaveBeenCalledWith({99 host: "100.64.0.2",100 port: 49422,101 x25519PublicKey: "remote-key",102 deviceId: "device-id",103 name: "June's MacBook",104 pairing: true,105 });106 });107108 it("never sends private key material in CONNECT requests", async () => {109 const chromeMock = installChromeMock();110 const sync = await import("./sync");111112 const promise = sync.connectToPeer("100.64.0.2", 49422, "remote-key");113 expect(chromeMock.port.postMessage).toHaveBeenCalledWith({114 type: "CONNECT",115 host: "100.64.0.2",116 port: 49422,117 remote_x25519_public_key: "remote-key",118 local_x25519_private_key: "",119 });120121 chromeMock.emit({ type: "CONNECTED", connection_id: "conn-1" });122 await expect(promise).resolves.toBe("conn-1");123 });124125 it("decodes a base64url session key for the requested connection", async () => {126 const chromeMock = installChromeMock();127 const sync = await import("./sync");128129 const promise = sync.getNativeSessionKey("conn-1");130 expect(chromeMock.port.postMessage).toHaveBeenCalledWith({131 type: "GET_SESSION_KEY",132 connection_id: "conn-1",133 });134135 136 const expected = new Uint8Array(32).fill(7);137 let binary = "";138 for (const byte of expected) binary += String.fromCharCode(byte);139 const key = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");140141 chromeMock.emit({ type: "SESSION_KEY", connection_id: "conn-1", key });142 await expect(promise).resolves.toEqual(expected);143 });144145 it("rejects a session key returned for the wrong connection", async () => {146 const chromeMock = installChromeMock();147 const sync = await import("./sync");148149 const promise = sync.getNativeSessionKey("conn-1");150 chromeMock.emit({151 type: "SESSION_KEY",152 connection_id: "other",153 key: "AA".repeat(0),154 });155 await expect(promise).rejects.toThrow("unexpected connection");156 });157158 it("rejects pending requests when the native host disconnects", async () => {159 const chromeMock = installChromeMock();160 const sync = await import("./sync");161162 const promise = sync.getNativeStatus();163 chromeMock.disconnect();164165 await expect(promise).rejects.toThrow("sync native host disconnected");166 });167});