pairing-code.test.ts

4.1 KB · raw

1import { describe, it, expect } from "vitest";2import {3  PAIRING_TOKEN_BYTES,4  encodePairingCode,5  formatPairingCode,6  parsePairingCode,7} from "./pairing-code";89const token4 = Uint8Array.of(0xde, 0xad, 0xbe, 0xef);1011describe("pairing-code", () => {12  // GOLDEN VECTOR — Android's PairingCode.kt is pinned against this exact13  // output. Changing it is a coordinated cross-repo change, never a local edit.14  it("golden vector: 192.168.1.42:49142 + deadbeef -> R2M0-2ANZ-YVFA-VFQF", () => {15    const code = encodePairingCode("192.168.1.42", 49142, token4);16    expect(code).toBe("R2M0-2ANZ-YVFA-VFQF");17    expect(parsePairingCode(code)).toEqual({18      ip: "192.168.1.42",19      port: 49142,20      tokenHex: "deadbeef",21    });22  });2324  it("is exactly 16 characters in 4 groups — the whole point of a 4-byte token", () => {25    const code = encodePairingCode("192.168.1.42", 49142, token4);26    expect(code.replace(/-/g, "")).toHaveLength(16);27    expect(code).toMatch(/^[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}$/);28    expect(PAIRING_TOKEN_BYTES).toBe(4);29  });3031  it("refuses to encode a token of any other size", () => {32    // An 8-byte token is what produced the 23-character, 6-group codes; Base3233    // gives 5 bits per character, so there is no encoding fix — only this gate.34    for (const n of [3, 5, 6, 8, 10]) {35      expect(() => encodePairingCode("192.168.1.42", 49142, new Uint8Array(n))).toThrow(/exactly 4 bytes/);36    }37  });3839  it("still decodes an older 8-byte-token code so a stale peer can pair", () => {40    // Real code captured from an Android emulator build before the fix.41    expect(parsePairingCode("1800-43XF-HPH0-2EP9-A3PR-7P0")).toEqual({42      ip: "10.0.2.15",43      port: 44941,44      tokenHex: "a2013ac950ed83d8",45    });46  });4748  it("preserves the full port range", () => {49    for (const port of [1, 8765, 49142, 65535]) {50      const code = encodePairingCode("192.168.4.230", port, token4);51      expect(parsePairingCode(code)?.port).toBe(port);52    }53  });5455  it("folds the ambiguous glyphs I, L and O on the way in", () => {56    const code = encodePairingCode("192.168.1.42", 49142, token4);57    const ambiguous = code.replace(/0/g, "O").replace(/1/g, "I");58    expect(parsePairingCode(ambiguous)).toEqual(parsePairingCode(code));59  });6061  it("accepts a code typed without dashes, in lowercase, with stray spaces", () => {62    const code = encodePairingCode("192.168.1.42", 49142, token4);63    const sloppy = ` ${code.replace(/-/g, " ").toLowerCase()} `;64    expect(parsePairingCode(sloppy)).toEqual(parsePairingCode(code));65  });6667  it("rejects codes that are too short to carry a token", () => {68    // 4 IP bytes + 2 port bytes and nothing else — 10 base32 characters.69    expect(parsePairingCode("ABCDEFGHJK")).toBeNull();70    expect(parsePairingCode("")).toBeNull();71    expect(parsePairingCode("---")).toBeNull();72  });7374  it("rejects characters outside Crockford's alphabet", () => {75    // U is excluded by Crockford and is NOT folded, unlike I/L/O.76    expect(parsePairingCode("UUUU-UUUU-UUUU-UUUU")).toBeNull();77    expect(parsePairingCode("A74H-48J3-KI32-OEP!")).toBeNull();78  });7980  it("rejects a zero port", () => {81    // An all-zero payload is long enough but decodes to 0.0.0.0:0 — a code82    // that parsed would send the dialler at an unroutable endpoint.83    expect(parsePairingCode("0000-0000-0000-0000")).toBeNull();84  });8586  it("refuses to encode an out-of-range address, port, or short token", () => {87    expect(() => encodePairingCode("192.168.1.256", 49142, token4)).toThrow();88    expect(() => encodePairingCode("192.168.1", 49142, token4)).toThrow();89    expect(() => encodePairingCode("192.168.1.42", 0, token4)).toThrow();90    expect(() => encodePairingCode("192.168.1.42", 70000, token4)).toThrow();91    expect(() => encodePairingCode("192.168.1.42", 49142, Uint8Array.of(1, 2, 3))).toThrow();92  });9394  it("regroups partial input as the user types", () => {95    expect(formatPairingCode("a74h48")).toBe("A74H-48");96    expect(formatPairingCode("A74H-48J3-KI32-OEP4")).toBe("A74H-48J3-KI32-OEP4");97    expect(formatPairingCode("a74h 48j3")).toBe("A74H-48J3");98    expect(formatPairingCode("")).toBe("");99  });100});