pairing-dial.test.ts

5.6 KB · raw

1import { beforeEach, describe, expect, it, vi } from "vitest";23import { dialPairingChannel, PAIRING_PORT, type PairingSocket } from "./pairing-dial";4import { encodePairingCode } from "./pairing-code";56const enc = (o: unknown) => new TextEncoder().encode(JSON.stringify(o));7const dec = (b: Uint8Array) => JSON.parse(new TextDecoder().decode(b));89const LISTENER_EPHEMERAL_HEX = "a".repeat(64);10const CODE = encodePairingCode("192.168.4.77", 51555, Uint8Array.of(0xde, 0xad, 0xbe, 0xef));1112const FAKE_HANDSHAKE_HASH = Uint8Array.from([0x0f, 0xba, 0xb8, 0xcd, ...new Array(28).fill(0)]);1314// Skip the real handshake messages; encrypt/decrypt are identity transforms so15// the channel's framing can be asserted directly.16vi.mock("./noise-js", async (orig) => {17  const actual = await orig<typeof import("./noise-js")>();18  return {19    ...actual,20    noiseNKInitiator: vi.fn(async () => ({21      encrypt: (pt: Uint8Array) => pt,22      decrypt: (ct: Uint8Array) => ct,23      handshakeHash: FAKE_HANDSHAKE_HASH,24    })),25  };26});2728/** `null` in the queue means "the socket closes here with no data". */29function fakeSocket(inbound: Array<Uint8Array | null>) {30  const sent: Uint8Array[] = [];31  let closed = false;32  let lastWasClose = false;33  const queue = [...inbound];34  const sock: PairingSocket = {35    send(bytes) { sent.push(bytes); },36    async receive() {37      const next = queue.shift();38      if (next === undefined || next === null) {39        closed = true;40        lastWasClose = true;41        throw new Error("socket closed");42      }43      lastWasClose = false;44      return next;45    },46    close() { closed = true; },47    closedBeforeFrame() { return lastWasClose; },48  };49  return { sock, sent, isClosed: () => closed };50}5152function ackThen(...rest: Array<Uint8Array | null>) {53  return fakeSocket([54    enc({ type: "PEACH_HELLO_ACK", ephemeralPublicKeyHex: LISTENER_EPHEMERAL_HEX }),55    ...rest,56  ]);57}5859beforeEach(() => { vi.clearAllMocks(); });6061describe("dialPairingChannel", () => {62  it("dials the coded endpoint and never probes for a host", async () => {63    const openSocket = vi.fn(async () => ackThen().sock);64    const findHost = vi.fn(async () => "10.0.0.9");6566    const result = await dialPairingChannel(CODE, { findHost, openSocket });6768    expect(result.ok).toBe(true);69    expect(findHost).not.toHaveBeenCalled();70    expect(openSocket).toHaveBeenCalledWith("192.168.4.77", 51555);71  });7273  it("presents the token as hex and returns the shared SAS", async () => {74    const s = ackThen();75    const result = await dialPairingChannel(CODE, { openSocket: async () => s.sock });7677    expect(dec(s.sent[0])).toEqual({ type: "PEACH_HELLO", token: "deadbeef" });78    expect(result.ok && result.dialled.sas).toBe("895245");79    expect(result.ok && result.dialled.host).toBe("192.168.4.77");80  });8182  it("probes for a host and sends a padded code for a legacy 6-digit code", async () => {83    const s = ackThen();84    const findHost = vi.fn(async () => "10.0.0.9");85    const openSocket = vi.fn(async () => s.sock);8687    await dialPairingChannel("123", { findHost, openSocket });8889    expect(findHost).toHaveBeenCalledWith(PAIRING_PORT);90    expect(openSocket).toHaveBeenCalledWith("10.0.0.9", PAIRING_PORT);91    expect(dec(s.sent[0])).toEqual({ type: "PEACH_HELLO", code: "000123" });92  });9394  it("has nowhere to go for an addressless code when no probe is supplied", async () => {95    const openSocket = vi.fn(async () => ackThen().sock);9697    const result = await dialPairingChannel("123456", { openSocket });9899    expect(result).toEqual({ ok: false, error: "no_host_found" });100    expect(openSocket).not.toHaveBeenCalled();101  });102103  it("reads a silent close as invalid_code, not a transport failure", async () => {104    // A wrong or expired token gets no error frame — the listener just closes.105    const s = fakeSocket([null]);106    const result = await dialPairingChannel(CODE, { openSocket: async () => s.sock });107108    expect(result).toEqual({ ok: false, error: "invalid_code" });109    expect(s.isClosed()).toBe(true);110  });111112  it("treats a non-ACK first frame exactly like a wrong token", async () => {113    const s = fakeSocket([enc({ type: "SOMETHING_ELSE" })]);114    const result = await dialPairingChannel(CODE, { openSocket: async () => s.sock });115116    expect(result).toEqual({ ok: false, error: "invalid_code" });117  });118119  it("rejects an ACK whose ephemeral key is not 32-byte hex", async () => {120    const s = fakeSocket([enc({ type: "PEACH_HELLO_ACK", ephemeralPublicKeyHex: "nope" })]);121    const result = await dialPairingChannel(CODE, { openSocket: async () => s.sock });122123    expect(result).toEqual({ ok: false, error: "handshake_failed" });124    expect(s.isClosed()).toBe(true);125  });126127  it("reports no_host_found when the endpoint refuses the connection", async () => {128    const result = await dialPairingChannel(CODE, {129      openSocket: async () => { throw new Error("ECONNREFUSED"); },130    });131132    expect(result).toEqual({ ok: false, error: "no_host_found" });133  });134135  it("leaves the socket open on success so the caller owns its lifetime", async () => {136    const s = ackThen(enc({ hello: "world" }));137    const result = await dialPairingChannel(CODE, { openSocket: async () => s.sock });138    if (!result.ok) throw new Error("expected a dialled channel");139140    expect(s.isClosed()).toBe(false);141    await result.dialled.channel.send(enc({ type: "ANYTHING" }));142    // sent[0] is the plaintext hello; sent[1] went through the Noise channel.143    expect(dec(s.sent[1])).toEqual({ type: "ANYTHING" });144    expect(dec(await result.dialled.channel.receive())).toEqual({ hello: "world" });145146    result.dialled.socket.close();147    expect(s.isClosed()).toBe(true);148  });149});