chirp-pair.adopt-source.test.ts

4.6 KB · raw

1import { describe, expect, it, vi } from "vitest";2import { computeVaultId } from "../shared/sync-identity";3import { routeSourcePostSASFrame } from "./chirp-pair";4import type { LocalIdentity, PairingTransport } from "../shared/chirp-handshake";56const localIdentity: LocalIdentity = {7  deviceId: "source-device",8  deviceName: "Source",9  deviceType: "extension",10  longTermPublicKeyHex: "ab".repeat(32),11  signingPublicKeyHex: "cd".repeat(32),12};1314function channel(): PairingTransport & { sent: Uint8Array[]; close: ReturnType<typeof vi.fn> } {15  const sent: Uint8Array[] = [];16  return {17    sent,18    close: vi.fn(async () => {}),19    async send(msg) { sent.push(msg); },20    async receive() {21      return new TextEncoder().encode(JSON.stringify({22        type: "ADOPT_IDENTITY",23        device_id: "receiver-device",24        device_name: "Receiver",25        device_type: "extension",26        long_term_public_key_hex: "ef".repeat(32),27      }));28    },29  };30}3132describe("source-side ADOPT_REQUEST routing", () => {33  it("T-A26: routes ADOPT_REQUEST to grantAdoption after SAS confirmation and consent", async () => {34    const rootKeyHex = "77".repeat(32);35    const ch = channel();36    const completePairing = vi.fn();37    const result = await routeSourcePostSASFrame(38      {39        sas: "895245",40        frame: {41          type: "ADOPT_REQUEST",42          device_id: "receiver-ephemeral",43          device_name: "Receiver",44          device_type: "extension",45        },46        channel: ch,47      },48      localIdentity,49      {50        confirmSAS: vi.fn(async () => true),51        completePairing,52        grantAdoptionConsent: vi.fn(async () => true),53        fetchAdoptionMaterial: vi.fn(async () => ({54          rootKeyHex,55          vaultId: await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex"))),56          license: "PPK-test",57        })),58        onProgress: vi.fn(),59      },60    );6162    const grant = JSON.parse(new TextDecoder().decode(ch.sent[0]));63    expect(result).toEqual(expect.objectContaining({ deviceId: "receiver-device" }));64    expect(grant).toMatchObject({65      type: "ADOPT_GRANT",66      root_key_hex: rootKeyHex,67      vault_id: await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex"))),68      license: "PPK-test",69    });70    expect(completePairing).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "receiver-device" }));71  });7273  it("T-A27: routes PAIR_IDENTITY to the existing pairing path", async () => {74    const ch = channel();75    const completePairing = vi.fn();76    const result = await routeSourcePostSASFrame(77      {78        sas: "895245",79        frame: {80          type: "PAIR_IDENTITY",81          device_id: "phone",82          device_name: "Phone",83          device_type: "android",84          long_term_public_key_hex: "aa".repeat(32),85        },86        channel: ch,87      },88      localIdentity,89      {90        confirmSAS: vi.fn(async () => true),91        completePairing,92        onProgress: vi.fn(),93      },94    );9596    const response = JSON.parse(new TextDecoder().decode(ch.sent[0]));97    expect(response.type).toBe("PAIR_IDENTITY");98    expect(result).toEqual(expect.objectContaining({ deviceId: "phone" }));99    expect(completePairing).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "phone" }));100  });101102  it("T-A28: unknown frame closes the channel gracefully", async () => {103    const ch = channel();104    await expect(routeSourcePostSASFrame(105      { sas: "895245", frame: { type: "WHAT_IS_THIS" }, channel: ch },106      localIdentity,107      {108        confirmSAS: vi.fn(async () => true),109        completePairing: vi.fn(),110        onProgress: vi.fn(),111      },112    )).rejects.toThrow("unexpected_pairing_frame");113114    expect(ch.close).toHaveBeenCalledTimes(1);115  });116117  it("T-A29: source root key bytes are zeroed after grantAdoption regardless of outcome", async () => {118    const fillSpy = vi.spyOn(Uint8Array.prototype, "fill");119    await routeSourcePostSASFrame(120      {121        sas: "895245",122        frame: {123          type: "ADOPT_REQUEST",124          device_id: "receiver-ephemeral",125          device_name: "Receiver",126          device_type: "extension",127        },128        channel: channel(),129      },130      localIdentity,131      {132        confirmSAS: vi.fn(async () => true),133        completePairing: vi.fn(),134        grantAdoptionConsent: vi.fn(async () => true),135        fetchAdoptionMaterial: vi.fn(async () => ({136          rootKeyHex: "88".repeat(32),137          vaultId: await computeVaultId(Uint8Array.from(Buffer.from("88".repeat(32), "hex"))),138        })),139        onProgress: vi.fn(),140      },141    );142143    expect(fillSpy).toHaveBeenCalledWith(0);144  });145});