pairing-code.test.ts

3.3 KB · raw

1import { describe, it, expect } from "vitest";2import { encodePairingCode, formatPairingCode, parsePairingCode } from "./pairing-code";34const token4 = Uint8Array.of(0xde, 0xad, 0xbe, 0xef);5const token8 = Uint8Array.of(0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef);67describe("pairing-code", () => {8  it("round-trips a 4-byte token as exactly 16 characters", () => {9    const code = encodePairingCode("192.168.1.42", 49142, token4);10    expect(code.replace(/-/g, "")).toHaveLength(16);11    expect(code).toMatch(/^[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}$/);12    expect(parsePairingCode(code)).toEqual({13      ip: "192.168.1.42",14      port: 49142,15      tokenHex: "deadbeef",16    });17  });1819  it("round-trips an 8-byte token without a coordinated length change", () => {20    const code = encodePairingCode("10.0.0.1", 8765, token8);21    expect(parsePairingCode(code)).toEqual({22      ip: "10.0.0.1",23      port: 8765,24      tokenHex: "0123456789abcdef",25    });26  });2728  it("preserves the full port range", () => {29    for (const port of [1, 8765, 49142, 65535]) {30      const code = encodePairingCode("192.168.4.230", port, token4);31      expect(parsePairingCode(code)?.port).toBe(port);32    }33  });3435  it("folds the ambiguous glyphs I, L and O on the way in", () => {36    const code = encodePairingCode("192.168.1.42", 49142, token4);37    const ambiguous = code.replace(/0/g, "O").replace(/1/g, "I");38    expect(parsePairingCode(ambiguous)).toEqual(parsePairingCode(code));39  });4041  it("accepts a code typed without dashes, in lowercase, with stray spaces", () => {42    const code = encodePairingCode("192.168.1.42", 49142, token4);43    const sloppy = ` ${code.replace(/-/g, " ").toLowerCase()} `;44    expect(parsePairingCode(sloppy)).toEqual(parsePairingCode(code));45  });4647  it("rejects codes that are too short to carry a token", () => {48    // 4 IP bytes + 2 port bytes and nothing else — 10 base32 characters.49    expect(parsePairingCode("ABCDEFGHJK")).toBeNull();50    expect(parsePairingCode("")).toBeNull();51    expect(parsePairingCode("---")).toBeNull();52  });5354  it("rejects characters outside Crockford's alphabet", () => {55    // U is excluded by Crockford and is NOT folded, unlike I/L/O.56    expect(parsePairingCode("UUUU-UUUU-UUUU-UUUU")).toBeNull();57    expect(parsePairingCode("A74H-48J3-KI32-OEP!")).toBeNull();58  });5960  it("rejects a zero port", () => {61    // An all-zero payload is long enough but decodes to 0.0.0.0:0 — a code62    // that parsed would send the dialler at an unroutable endpoint.63    expect(parsePairingCode("0000-0000-0000-0000")).toBeNull();64  });6566  it("refuses to encode an out-of-range address, port, or short token", () => {67    expect(() => encodePairingCode("192.168.1.256", 49142, token4)).toThrow();68    expect(() => encodePairingCode("192.168.1", 49142, token4)).toThrow();69    expect(() => encodePairingCode("192.168.1.42", 0, token4)).toThrow();70    expect(() => encodePairingCode("192.168.1.42", 70000, token4)).toThrow();71    expect(() => encodePairingCode("192.168.1.42", 49142, Uint8Array.of(1, 2, 3))).toThrow();72  });7374  it("regroups partial input as the user types", () => {75    expect(formatPairingCode("a74h48")).toBe("A74H-48");76    expect(formatPairingCode("A74H-48J3-KI32-OEP4")).toBe("A74H-48J3-KI32-OEP4");77    expect(formatPairingCode("a74h 48j3")).toBe("A74H-48J3");78    expect(formatPairingCode("")).toBe("");79  });80});