1import { beforeEach, describe, expect, it, vi } from "vitest";23import {4 dialPairingChannel,5 dialRelayPairingChannel,6 type PairingSocket,7} from "./pairing-dial";8import { encodePairingCode } from "./pairing-code";9import { createPairingInvitation } from "./pairing-invitation";1011const enc = (o: unknown) => new TextEncoder().encode(JSON.stringify(o));12const dec = (b: Uint8Array) => JSON.parse(new TextDecoder().decode(b));1314const LISTENER_EPHEMERAL_HEX = "a".repeat(64);15const CODE = encodePairingCode("192.168.4.77", 51555, Uint8Array.of(0xde, 0xad, 0xbe, 0xef));16const RELAY_TOKEN = "AAECAwQFBgcICQoLDA0ODw";17const RELAY_INVITATION = createPairingInvitation(18 "wss://relay.example.test/base",19 RELAY_TOKEN,20);2122const FAKE_HANDSHAKE_HASH = Uint8Array.from([0x0f, 0xba, 0xb8, 0xcd, ...new Array(28).fill(0)]);23242526vi.mock("./noise-js", async (orig) => {27 const actual = await orig<typeof import("./noise-js")>();28 return {29 ...actual,30 noiseNKInitiator: vi.fn(async () => ({31 encrypt: (pt: Uint8Array) => pt,32 decrypt: (ct: Uint8Array) => ct,33 handshakeHash: FAKE_HANDSHAKE_HASH,34 close: vi.fn(),35 })),36 };37});383940function fakeSocket(inbound: Array<Uint8Array | null>) {41 const sent: Uint8Array[] = [];42 let closed = false;43 let lastWasClose = false;44 const queue = [...inbound];45 const sock: PairingSocket = {46 send(bytes) { sent.push(bytes); },47 async receive() {48 const next = queue.shift();49 if (next === undefined || next === null) {50 closed = true;51 lastWasClose = true;52 throw new Error("socket closed");53 }54 lastWasClose = false;55 return next;56 },57 close() { closed = true; },58 closedBeforeFrame() { return lastWasClose; },59 };60 return { sock, sent, isClosed: () => closed };61}6263function ackThen(...rest: Array<Uint8Array | null>) {64 return fakeSocket([65 enc({ type: "PEACH_HELLO_ACK", ephemeralPublicKeyHex: LISTENER_EPHEMERAL_HEX }),66 ...rest,67 ]);68}6970beforeEach(() => { vi.clearAllMocks(); });7172describe("dialPairingChannel", () => {73 it("dials the coded endpoint", async () => {74 const openSocket = vi.fn(async () => ackThen().sock);7576 const result = await dialPairingChannel(CODE, { openSocket });7778 expect(result.ok).toBe(true);79 expect(openSocket).toHaveBeenCalledWith("192.168.4.77", 51555);80 });8182 it("presents the token as hex and returns the shared SAS", async () => {83 const s = ackThen();84 const result = await dialPairingChannel(CODE, { openSocket: async () => s.sock });8586 expect(dec(s.sent[0])).toEqual({ type: "PEACH_HELLO", token: "deadbeef" });87 expect(result.ok && result.dialled.sas).toBe("44907520");88 expect(result.ok && result.dialled.host).toBe("192.168.4.77");89 expect(result.ok && result.dialled.rendezvousSecretHex).toBe(90 "a8977cd86e944e72e9cb569dbba00a7b0abbf7f456c745299aa8f66157859143",91 );92 });9394 it("marks LAN adoption before Noise so the source can skip device-pair auth", async () => {95 const s = ackThen();96 const result = await dialPairingChannel(97 CODE,98 { openSocket: async () => s.sock },99 { flow: "adoption" },100 );101102 expect(result.ok).toBe(true);103 expect(dec(s.sent[0])).toEqual({104 type: "PEACH_HELLO",105 token: "deadbeef",106 flow: "adoption",107 });108 });109110 it("rejects an addressless legacy code before opening a socket", async () => {111 const openSocket = vi.fn(async () => ackThen().sock);112113 const result = await dialPairingChannel("123456", { openSocket });114115 expect(result).toEqual({ ok: false, error: "invalid_code" });116 expect(openSocket).not.toHaveBeenCalled();117 });118119 it("reads a silent close as invalid_code, not a transport failure", async () => {120 121 const s = fakeSocket([null]);122 const result = await dialPairingChannel(CODE, { openSocket: async () => s.sock });123124 expect(result).toEqual({ ok: false, error: "invalid_code" });125 expect(s.isClosed()).toBe(true);126 });127128 it("treats a non-ACK first frame exactly like a wrong token", async () => {129 const s = fakeSocket([enc({ type: "SOMETHING_ELSE" })]);130 const result = await dialPairingChannel(CODE, { openSocket: async () => s.sock });131132 expect(result).toEqual({ ok: false, error: "invalid_code" });133 });134135 it("rejects an ACK whose ephemeral key is not 32-byte hex", async () => {136 const s = fakeSocket([enc({ type: "PEACH_HELLO_ACK", ephemeralPublicKeyHex: "nope" })]);137 const result = await dialPairingChannel(CODE, { openSocket: async () => s.sock });138139 expect(result).toEqual({ ok: false, error: "handshake_failed" });140 expect(s.isClosed()).toBe(true);141 });142143 it("reports no_host_found when the endpoint refuses the connection", async () => {144 const result = await dialPairingChannel(CODE, {145 openSocket: async () => { throw new Error("ECONNREFUSED"); },146 });147148 expect(result).toEqual({ ok: false, error: "no_host_found" });149 });150151 it("leaves the socket open on success so the caller owns its lifetime", async () => {152 const s = ackThen(enc({ hello: "world" }));153 const result = await dialPairingChannel(CODE, { openSocket: async () => s.sock });154 if (!result.ok) throw new Error("expected a dialled channel");155156 expect(s.isClosed()).toBe(false);157 await result.dialled.channel.send(enc({ type: "ANYTHING" }));158 159 expect(dec(s.sent[1])).toEqual({ type: "ANYTHING" });160 expect(dec(await result.dialled.channel.receive())).toEqual({ hello: "world" });161162 result.dialled.socket.close();163 expect(s.isClosed()).toBe(true);164 });165});166167describe("dialRelayPairingChannel", () => {168 it("occupies the invited relay room and runs the identical hello/NK path", async () => {169 const s = ackThen();170 const openSocket = vi.fn(async () => s.sock);171172 const result = await dialRelayPairingChannel(RELAY_INVITATION, { openSocket });173174 expect(result.ok).toBe(true);175 expect(openSocket).toHaveBeenCalledWith(176 `wss://relay.example.test/base/r/${RELAY_TOKEN}`,177 );178 expect(dec(s.sent[0])).toEqual({ type: "PEACH_HELLO", token: RELAY_TOKEN });179 expect(result.ok && result.dialled.relayUrl).toBe(180 "wss://relay.example.test/base",181 );182 expect(result.ok && result.dialled.host).toBe("relay.example.test");183 });184185 it("rejects a malformed invitation before opening a socket", async () => {186 const openSocket = vi.fn(async () => ackThen().sock);187 const result = await dialRelayPairingChannel("peach-pair-v1:not-valid", {188 openSocket,189 });190 expect(result).toEqual({ ok: false, error: "invalid_code" });191 expect(openSocket).not.toHaveBeenCalled();192 });193});