OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / api-management-snapshots.test.js

379 lines · javascript ref:
import test from 'brittle'
import { AVAILABLE_MODES } from '../../packages/core/core/relay-node/api-mode-transport.js'
import {
  MAX_MANAGEMENT_SERVICE_ERROR_BYTES,
  MAX_MANAGEMENT_SERVICE_METHODS,
  MAX_MANAGEMENT_SERVICE_SNAPSHOT_SERVICES,
  MAX_MANAGEMENT_SERVICE_STATS_ARRAY,
  MAX_MANAGEMENT_SERVICE_STATS_KEYS,
  MAX_MANAGEMENT_SERVICE_STATS_STRING_BYTES,
  buildManagementSnapshotRoutePayload,
  buildDeviceStatusPayload,
  buildManagementConfigPayload,
  buildManagementServicesPayload,
  buildModeCatalogPayload,
  buildPairingStatusPayload,
  buildServiceRegistrySnapshot,
  buildTransportStatusPayload,
  resolveManagementSnapshotRoute
} from '../../packages/core/core/relay-node/api-management-snapshots.js'

test('api management snapshots: route helper maps exact GET snapshot reads', (t) => {
  t.alike(resolveManagementSnapshotRoute('GET', '/api/manage/config'), { kind: 'config' })
  t.alike(resolveManagementSnapshotRoute('GET', '/api/manage/ai/models'), { kind: 'ai-models' })
  t.alike(resolveManagementSnapshotRoute('GET', '/api/manage/services/available'), { kind: 'service-config' })
  t.alike(resolveManagementSnapshotRoute('GET', '/api/manage/services'), { kind: 'services' })
  t.alike(resolveManagementSnapshotRoute('GET', '/api/manage/transports'), { kind: 'transports' })
  t.alike(resolveManagementSnapshotRoute('GET', '/api/manage/devices'), { kind: 'devices' })
  t.alike(resolveManagementSnapshotRoute('GET', '/api/manage/pairing'), { kind: 'pairing' })
  t.alike(resolveManagementSnapshotRoute('GET', '/api/manage/modes'), { kind: 'modes' })

  t.is(resolveManagementSnapshotRoute('POST', '/api/manage/config'), null)
  t.is(resolveManagementSnapshotRoute('GET', '/api/manage/service'), null)
  t.is(resolveManagementSnapshotRoute('GET', '/api/manage/services/available/extra'), null)
  t.is(resolveManagementSnapshotRoute('GET', '/api/manage'), null)
})

test('api management snapshots: service registry payload tolerates missing registry', (t) => {
  t.alike(buildServiceRegistrySnapshot(null), { services: [], count: 0 })
  t.alike(buildServiceRegistrySnapshot({ services: null }), { services: [], count: 0 })
})

test('api management snapshots: config payload uses safe config fields only', (t) => {
  const payload = buildManagementConfigPayload({
    _operatingMode: 'gateway',
    config: {
      name: 'relay',
      storage: '/tmp/hiverelay',
      apiKey: 'do-not-leak',
      connectionKey: 'do-not-leak-either',
      plugins: ['poker'],
      pairing: { enabled: true, token: 'hidden' },
      subsidy: { enabled: true, payoutDestination: 'addr' }
    }
  })

  t.is(payload.mode, 'gateway')
  t.is(payload.config.name, 'relay')
  t.is(payload.config.mode, 'gateway')
  t.alike(payload.config.plugins, ['poker'])
  t.absent(Object.prototype.hasOwnProperty.call(payload.config, 'apiKey'))
  t.absent(Object.prototype.hasOwnProperty.call(payload.config, 'connectionKey'))
})

test('api management snapshots: route payload builder centralizes GET snapshot responses', async (t) => {
  const node = {
    _operatingMode: 'private',
    mode: 'private',
    config: {
      name: 'relay',
      enableServices: true,
      plugins: ['ai'],
      transports: { websocket: true },
      wsPort: 9999
    },
    serviceRegistry: {
      services: new Map([
        ['ai', { status: 'running', capabilities: ['list-models'] }]
      ])
    },
    accessControl: {
      isPairing: true,
      _pairingState: {
        token: 'do-not-leak',
        expiresAt: 12345
      }
    },
    listDevices () {
      return [{ pubkey: 'a'.repeat(64), name: 'phone' }]
    }
  }
  const aiProvider = {
    ok: true,
    provider: {
      async 'list-models' () {
        return [{ modelId: 'local-model', hasEndpoint: true }]
      }
    }
  }

  const config = await buildManagementSnapshotRoutePayload({ route: { kind: 'config' }, node })
  const aiModels = await buildManagementSnapshotRoutePayload({ route: { kind: 'ai-models' }, node, aiModelProvider: aiProvider })
  const serviceConfig = await buildManagementSnapshotRoutePayload({ route: { kind: 'service-config' }, node })
  const services = await buildManagementSnapshotRoutePayload({ route: { kind: 'services' }, node })
  const transports = await buildManagementSnapshotRoutePayload({ route: { kind: 'transports' }, node })
  const devices = await buildManagementSnapshotRoutePayload({ route: { kind: 'devices' }, node })
  const pairing = await buildManagementSnapshotRoutePayload({ route: { kind: 'pairing' }, node })
  const modes = await buildManagementSnapshotRoutePayload({ route: { kind: 'modes' }, node })
  const missingAI = await buildManagementSnapshotRoutePayload({
    route: { kind: 'ai-models' },
    node,
    aiModelProvider: { ok: false, status: 503, error: 'AI service is not running' }
  })
  const unknown = await buildManagementSnapshotRoutePayload({ route: { kind: 'unknown' }, node })

  t.is(config.status, undefined)
  t.is(config.payload.mode, 'private')
  t.is(aiModels.payload.count, 1)
  t.is(aiModels.payload.models[0].modelId, 'local-model')
  t.alike(serviceConfig.payload.plugins, ['ai'])
  t.is(services.payload.count, 1)
  t.is(transports.payload.websocket.port, 9999)
  t.is(devices.payload.count, 1)
  t.is(pairing.payload.pairing.expiresAt, 12345)
  t.is(modes.payload.current, 'private')
  t.is(missingAI.status, 503)
  t.is(missingAI.payload.error, 'AI service is not running')
  t.is(unknown.status, 404)
  t.is(unknown.payload.error, 'unknown management snapshot route')
  t.absent(JSON.stringify(pairing.payload).includes('do-not-leak'))
})

test('api management snapshots: services payload preserves compatibility capabilities', (t) => {
  const payload = buildManagementServicesPayload({
    services: new Map([
      ['storage', {
        status: 'running',
        capabilities: ['put', 'get'],
        provider: { stats () { return { ok: true } } }
      }]
    ])
  })

  t.is(payload.enabled, true)
  t.is(payload.statsVerified, false)
  t.is(payload.count, 1)
  t.alike(payload.services[0].methods, ['put', 'get'])
  t.alike(payload.services[0].capabilities, ['put', 'get'])
})

test('api management snapshots: service entries prefer capabilities and isolate stats failures', (t) => {
  const registry = {
    services: new Map([
      ['ai', {
        version: '1.0.0',
        description: 'models',
        status: 'running',
        capabilities: ['infer'],
        provider: {
          methods: { ignored () {} },
          stats () { throw new Error('stats failed') }
        },
        restartCount: 2,
        lastStartedAt: 'start',
        lastStoppedAt: 'stop',
        lastError: 'previous error'
      }],
      ['poker', {
        running: true,
        provider: {
          methods: {
            listTables () {},
            joinTable () {}
          },
          stats () {
            return { tables: 3 }
          }
        }
      }]
    ])
  }

  const payload = buildServiceRegistrySnapshot(registry)
  t.is(payload.count, 2)
  t.is(payload.total, 2)
  t.absent(payload.truncated)
  t.alike(payload.services[0], {
    name: 'ai',
    version: '1.0.0',
    description: 'models',
    status: 'running',
    running: true,
    methods: ['infer'],
    stats: null,
    restartCount: 2,
    lastStartedAt: 'start',
    lastStoppedAt: 'stop',
    lastError: 'previous error'
  })
  t.alike(payload.services[1], {
    name: 'poker',
    version: null,
    description: '',
    status: 'running',
    running: true,
    methods: ['listTables', 'joinTable'],
    stats: { tables: 3 },
    restartCount: 0,
    lastStartedAt: null,
    lastStoppedAt: null,
    lastError: null
  })
})

test('api management snapshots: service entries sanitize provider stats and noisy metadata', (t) => {
  const registry = {
    services: new Map([
      ['bad\nname', { version: '1.0.0' }],
      ['storage', {
        version: '1.0.0',
        description: 'x'.repeat(200),
        status: 'running',
        capabilities: [
          'put',
          'put',
          'bad\nmethod',
          ...Array.from({ length: MAX_MANAGEMENT_SERVICE_METHODS + 8 }, (_, i) => 'method-' + i)
        ],
        stats: {
          requests: 12,
          nested: {
            ok: true,
            apiKey: 'do-not-leak',
            token: 'also-hidden'
          },
          labels: [
            'ok',
            'bad\nlabel',
            ...Array.from({ length: MAX_MANAGEMENT_SERVICE_STATS_ARRAY + 4 }, (_, i) => 'item-' + i)
          ],
          text: 'z'.repeat(MAX_MANAGEMENT_SERVICE_STATS_STRING_BYTES + 64),
          nope: Infinity
        },
        restartCount: 1.9,
        lastStartedAt: 'start',
        lastStoppedAt: 'bad\nstop',
        lastError: 'e'.repeat(MAX_MANAGEMENT_SERVICE_ERROR_BYTES + 64)
      }]
    ])
  }
  for (let i = 0; i < MAX_MANAGEMENT_SERVICE_STATS_KEYS + 10; i++) {
    registry.services.get('storage').stats['metric-' + i] = i
  }
  for (let i = 0; i < MAX_MANAGEMENT_SERVICE_SNAPSHOT_SERVICES + 4; i++) {
    registry.services.set('extra-' + i, { version: '1.0.0' })
  }

  const payload = buildServiceRegistrySnapshot(registry)

  t.is(payload.count, MAX_MANAGEMENT_SERVICE_SNAPSHOT_SERVICES)
  t.is(payload.total, MAX_MANAGEMENT_SERVICE_SNAPSHOT_SERVICES + 6)
  t.ok(payload.truncated)

  const badName = payload.services[0]
  t.is(badName.name, 'unknown')

  const service = payload.services[1]
  t.is(service.methods.length, MAX_MANAGEMENT_SERVICE_METHODS)
  t.is(service.methods[0], 'put')
  t.absent(service.methods.includes('bad\nmethod'))
  t.is(service.restartCount, 1)
  t.is(service.lastStoppedAt, null)
  t.is(Buffer.byteLength(service.lastError, 'utf8'), MAX_MANAGEMENT_SERVICE_ERROR_BYTES)
  t.is(Buffer.byteLength(service.stats.text, 'utf8'), MAX_MANAGEMENT_SERVICE_STATS_STRING_BYTES)
  t.is(service.stats.requests, 12)
  t.is(service.stats.nested.ok, true)
  t.absent(Object.prototype.hasOwnProperty.call(service.stats.nested, 'apiKey'))
  t.absent(Object.prototype.hasOwnProperty.call(service.stats.nested, 'token'))
  t.absent(Object.prototype.hasOwnProperty.call(service.stats, 'nope'))
  t.is(service.stats.labels.length, MAX_MANAGEMENT_SERVICE_STATS_ARRAY)
  t.absent(JSON.stringify(payload).includes('do-not-leak'))
  t.absent(JSON.stringify(payload).includes('also-hidden'))
})

test('api management snapshots: transport payload reports optional runtime transports', (t) => {
  const payload = buildTransportStatusPayload({
    config: { transports: { websocket: true }, wsPort: 9999 },
    holesailTransport: { connectionKey: 'hole-key', running: true },
    torTransport: { onionAddress: 'relay.onion', running: false }
  })

  t.alike(payload, {
    udp: true,
    holesail: {
      enabled: true,
      connectionKey: 'hole-key',
      running: true
    },
    tor: {
      enabled: true,
      onionAddress: 'relay.onion',
      running: false
    },
    websocket: {
      enabled: true,
      port: 9999
    }
  })

  t.alike(buildTransportStatusPayload({ config: {} }).websocket, {
    enabled: false,
    port: 8765
  })
})

test('api management snapshots: device and pairing status avoid private state leakage', (t) => {
  t.alike(buildDeviceStatusPayload({ mode: 'public' }), {
    enabled: false,
    mode: 'public',
    devices: []
  })
  t.alike(buildPairingStatusPayload({ mode: 'public' }), {
    enabled: false,
    mode: 'public',
    pairing: null
  })

  const device = { pubkey: 'A'.repeat(64), name: 'phone', pairedAt: 1, lastSeen: 2, token: 'do-not-leak-device-token' }
  const node = {
    mode: 'private',
    accessControl: {
      isPairing: true,
      _pairingState: {
        token: 'do-not-leak',
        relayPubkey: 'b'.repeat(64),
        expiresAt: 12345
      }
    },
    listDevices () {
      return [
        device,
        { pubkey: 'not-a-key', name: 'ignored', secret: 'hidden' },
        { pubkey: 'b'.repeat(64), name: 'bad\nname', pairedAt: -1, lastSeen: Infinity }
      ]
    }
  }

  t.alike(buildDeviceStatusPayload(node), {
    enabled: true,
    mode: 'private',
    count: 2,
    total: 3,
    truncated: true,
    devices: [
      { pubkey: 'a'.repeat(64), name: 'phone', pairedAt: 1, lastSeen: 2 },
      { pubkey: 'b'.repeat(64), name: 'manual', pairedAt: null, lastSeen: null }
    ]
  })
  t.absent(JSON.stringify(buildDeviceStatusPayload(node)).includes('do-not-leak-device-token'))

  const pairing = buildPairingStatusPayload(node)
  t.alike(pairing, {
    enabled: true,
    mode: 'private',
    pairing: {
      active: true,
      expiresAt: 12345
    }
  })
  t.absent(Object.prototype.hasOwnProperty.call(pairing.pairing, 'token'))
  t.absent(JSON.stringify(pairing).includes('do-not-leak'))
})

test('api management snapshots: mode catalog stays aligned with switchable modes', (t) => {
  const payload = buildModeCatalogPayload('private')
  t.is(payload.current, 'private')
  t.alike(payload.available.map(mode => mode.id), AVAILABLE_MODES)
  t.ok(payload.available.every(mode => mode.name && mode.description))
})