import { describe, it, expect } from 'vitest' import { createCpClient, CpClientError } from '../src/cp-client.js' function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } }) } describe('createCpClient.listHosts', () => { it('maps the CP host records to a curated view and strips agentPubkey/enrollFpr', async () => { const captured: { url?: string; init?: RequestInit | undefined } = {} const fetchFn = (async (url: string | URL | Request, init?: RequestInit) => { captured.url = String(url) captured.init = init return jsonResponse([ { hostId: 'h1', accountId: 'acct-1', subdomain: 'alpha', agentPubkey: 'BASE64PUBKEY', enrollFpr: 'fpr', status: 'online', lastSeen: '2026-01-02T00:00:00.000Z', createdAt: '2026-01-01T00:00:00.000Z', revokedAt: null, }, ]) }) as typeof fetch const client = createCpClient({ cpUrl: 'http://127.0.0.1:8080', fetchFn }) const hosts = await client.listHosts('acct-1', 'TOKEN123') expect(captured.url).toBe('http://127.0.0.1:8080/accounts/acct-1/hosts') expect((captured.init?.headers as Record).authorization).toBe('Bearer TOKEN123') expect(hosts).toHaveLength(1) expect(hosts[0]).toMatchObject({ hostId: 'h1', subdomain: 'alpha', status: 'online', lastSeen: '2026-01-02T00:00:00.000Z' }) expect(hosts[0]).not.toHaveProperty('agentPubkey') expect(hosts[0]).not.toHaveProperty('enrollFpr') }) it('throws CpClientError carrying the upstream status on non-2xx', async () => { const fetchFn = (async () => new Response('nope', { status: 403 })) as typeof fetch const client = createCpClient({ cpUrl: 'http://127.0.0.1:8080', fetchFn }) await expect(client.listHosts('acct-1', 't')).rejects.toMatchObject({ name: 'CpClientError', status: 403 }) }) it('maps a network failure to a 502 CpClientError', async () => { const fetchFn = (async () => { throw new Error('ECONNREFUSED') }) as typeof fetch const client = createCpClient({ cpUrl: 'http://127.0.0.1:8080', fetchFn }) await expect(client.listHosts('acct-1', 't')).rejects.toMatchObject({ status: 502 }) }) }) describe('createCpClient.createPairingCode', () => { it('POSTs and maps { code, expiresAt }', async () => { const captured: { init?: RequestInit | undefined } = {} const fetchFn = (async (_url: string | URL | Request, init?: RequestInit) => { captured.init = init return jsonResponse({ code: 'ABCD-EFGH', expiresAt: '2026-02-01T00:00:00.000Z' }, 201) }) as typeof fetch const client = createCpClient({ cpUrl: 'http://127.0.0.1:8080', fetchFn }) const issued = await client.createPairingCode('acct-1', 'TOK') expect(captured.init?.method).toBe('POST') expect(issued).toEqual({ code: 'ABCD-EFGH', expiresAt: '2026-02-01T00:00:00.000Z' }) }) it('rejects a malformed CP pairing response', async () => { const fetchFn = (async () => jsonResponse({ nope: true }, 201)) as typeof fetch const client = createCpClient({ cpUrl: 'http://127.0.0.1:8080', fetchFn }) await expect(client.createPairingCode('acct-1', 't')).rejects.toBeInstanceOf(Error) }) }) describe('createCpClient.deleteHost', () => { it('DELETEs and resolves on 204', async () => { const captured: { url?: string; init?: RequestInit | undefined } = {} const fetchFn = (async (url: string | URL | Request, init?: RequestInit) => { captured.url = String(url) captured.init = init return new Response(null, { status: 204 }) }) as typeof fetch const client = createCpClient({ cpUrl: 'http://127.0.0.1:8080', fetchFn }) await client.deleteHost('host-42', 'TOK') expect(captured.url).toBe('http://127.0.0.1:8080/hosts/host-42') expect(captured.init?.method).toBe('DELETE') }) it('throws CpClientError on a non-2xx delete', async () => { const fetchFn = (async () => new Response('no', { status: 404 })) as typeof fetch const client = createCpClient({ cpUrl: 'http://127.0.0.1:8080', fetchFn }) await expect(client.deleteHost('h', 't')).rejects.toMatchObject({ status: 404 }) }) })