sync.test.ts

5.0 KB · raw

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    // 32 bytes of 0x07 → URL-safe base64 with no padding.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});