OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / test-p2p-services.js

289 lines · javascript ref:
#!/usr/bin/env node

/**
 * P2P Service Protocol Integration Test Suite
 *
 * Tests client.callService() over the P2P service channel against live relays:
 *   1. Service channel opens on connect
 *   2. Service catalog received from relay
 *   3. callService('identity', 'whoami') returns relay pubkey
 *   4. callService('schema', 'list') returns schema list
 *   5. callService('sla', 'list') returns SLA list
 *   6. callService with unknown service returns error
 *   7. callService with unknown method returns error
 *   8. Multiple concurrent callService calls
 *   9. App catalog received from relay
 *  10. callService across multiple relays
 *
 * Usage: node scripts/test-p2p-services.js
 */

import { HiveRelayClient } from '../client/index.js'

let passed = 0; let failed = 0
const results = []
function log (icon, msg) { console.log(`  ${icon} ${msg}`) }
async function test (name, fn) {
  const start = Date.now()
  try { await fn(); log('✅', `${name} (${Date.now() - start}ms)`); passed++; results.push({ name, status: 'pass' }) } catch (err) { log('❌', `${name} — ${err.message}`); failed++; results.push({ name, status: 'fail', error: err.message }) }
}
function assert (cond, msg) { if (!cond) throw new Error(msg) }

async function runTests () {
  console.log('\n╔═══════════════════════════════════════════════════╗')
  console.log('║     HiveRelay P2P Service Protocol Tests           ║')
  console.log('╚═══════════════════════════════════════════════════╝\n')

  // ──────────────────────────────────────────
  // Setup: Create client and connect to relays
  // ──────────────────────────────────────────
  console.log('── Setup: Connecting to relay network ──')

  const client = new HiveRelayClient({
    storage: '/tmp/test-p2p-services-' + Date.now(),
    apiKey: process.env.HIVERELAY_API_KEY || 'hiverelay-secret'
  })

  // Track events
  const events = { catalogReceived: false, appCatalogReceived: false, serviceChannelOpen: false }
  let serviceCatalog = null
  let appCatalog = null

  client.on('service-channel-open', (evt) => {
    events.serviceChannelOpen = true
    log('ℹ️', `Service channel opened with relay ${evt.relay.slice(0, 16)}...`)
  })

  client.on('service-catalog', (evt) => {
    events.catalogReceived = true
    serviceCatalog = evt.services
    log('ℹ️', `Service catalog from ${evt.relay.slice(0, 16)}...: ${(evt.services || []).map(s => s.name).join(', ')}`)
  })

  client.on('app-catalog', (evt) => {
    events.appCatalogReceived = true
    appCatalog = evt.apps
    log('ℹ️', `App catalog from ${evt.relay.slice(0, 16)}...: ${(evt.apps || []).length} apps`)
  })

  await client.start()
  log('ℹ️', 'Client started, waiting for relay connections...')

  // Wait for relay connection + service channel
  let connected = false
  for (let i = 0; i < 20; i++) {
    const relays = client.getRelays()
    if (relays.length > 0 && relays.some(r => r.hasServiceProtocol)) {
      connected = true
      log('ℹ️', `Connected to ${relays.length} relay(s), ${relays.filter(r => r.hasServiceProtocol).length} with service channel`)
      break
    }
    await new Promise(resolve => setTimeout(resolve, 1000))
    process.stdout.write('.')
  }
  console.log()

  if (!connected) {
    console.log('  ❌ Could not connect to any relay with service channel')
    await client.destroy()
    process.exit(1)
  }

  // Give a moment for catalogs to arrive
  await new Promise(resolve => setTimeout(resolve, 2000))

  // ──────────────────────────────────────────
  // 1. Service Channel Opens
  // ──────────────────────────────────────────
  console.log('\n── Test Group 1: Service Channel ──')

  await test('Service channel opened on connect', async () => {
    assert(events.serviceChannelOpen, 'service-channel-open event not received')
  })

  await test('Service catalog received from relay', async () => {
    assert(events.catalogReceived, 'service-catalog event not received')
    assert(Array.isArray(serviceCatalog), 'Catalog is not an array')
    assert(serviceCatalog.length > 0, 'Catalog is empty')
    log('ℹ️', `Catalog has ${serviceCatalog.length} services: ${serviceCatalog.map(s => s.name).join(', ')}`)
  })

  await test('App catalog received from relay', async () => {
    assert(events.appCatalogReceived, 'app-catalog event not received')
    assert(Array.isArray(appCatalog), 'App catalog is not an array')
    log('ℹ️', `${appCatalog.length} apps in catalog`)
  })

  // ──────────────────────────────────────────
  // 2. Identity Service via P2P
  // ──────────────────────────────────────────
  console.log('\n── Test Group 2: Identity Service ──')

  await test('callService: identity.whoami', async () => {
    const result = await client.callService('identity', 'whoami', {}, { timeout: 10000 })
    assert(result, 'Empty result')
    assert(result.pubkey, 'Missing pubkey in whoami result')
    assert(result.pubkey.length === 64, `Invalid pubkey length: ${result.pubkey.length}`)
    log('ℹ️', `Relay identity: ${result.pubkey.slice(0, 16)}...`)
  })

  await test('callService: identity.sign rejects remote peers (security)', async () => {
    try {
      await client.callService('identity', 'sign', { message: 'test' }, { timeout: 10000 })
      assert(false, 'Should have rejected remote sign request')
    } catch (err) {
      assert(err.message.includes('UNAUTHORIZED'), `Expected UNAUTHORIZED, got: ${err.message}`)
      log('ℹ️', `Correctly rejected: ${err.message}`)
    }
  })

  // ──────────────────────────────────────────
  // 3. Schema Service via P2P
  // ──────────────────────────────────────────
  console.log('\n── Test Group 3: Schema Service ──')

  await test('callService: schema.list', async () => {
    const result = await client.callService('schema', 'list', {}, { timeout: 10000 })
    assert(result, 'Empty result')
    log('ℹ️', `Schema list: ${JSON.stringify(result).slice(0, 100)}`)
  })

  await test('callService: schema.register + schema.validate', async () => {
    const schemaId = 'test-p2p-schema-' + Date.now()
    const definition = { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }

    const regResult = await client.callService('schema', 'register', { schemaId, definition, version: '1.0.0' }, { timeout: 10000 })
    assert(regResult, 'Empty register result')
    log('ℹ️', `Registered schema: ${schemaId}`)

    const valResult = await client.callService('schema', 'validate', { schemaId, data: { name: 'test' } }, { timeout: 10000 })
    assert(valResult, 'Empty validate result')
    assert(valResult.valid === true, `Validation failed: ${JSON.stringify(valResult)}`)
    log('ℹ️', 'Schema validation passed')
  })

  // ──────────────────────────────────────────
  // 4. SLA Service via P2P
  // ──────────────────────────────────────────
  console.log('\n── Test Group 4: SLA Service ──')

  await test('callService: sla.list', async () => {
    const result = await client.callService('sla', 'list', {}, { timeout: 10000 })
    assert(result, 'Empty result')
    log('ℹ️', `SLA list: ${JSON.stringify(result).slice(0, 100)}`)
  })

  await test('callService: sla.stats', async () => {
    const result = await client.callService('sla', 'stats', {}, { timeout: 10000 })
    assert(result, 'Empty result')
    log('ℹ️', `SLA stats: ${JSON.stringify(result).slice(0, 100)}`)
  })

  // ──────────────────────────────────────────
  // 5. Error Handling
  // ──────────────────────────────────────────
  console.log('\n── Test Group 5: Error Handling ──')

  await test('callService: unknown service returns error', async () => {
    try {
      await client.callService('nonexistent', 'method', {}, { timeout: 5000 })
      assert(false, 'Should have thrown')
    } catch (err) {
      assert(err.message !== 'SERVICE_TIMEOUT', 'Got timeout instead of error — relay may not be handling unknown services')
      log('ℹ️', `Error for unknown service: ${err.message}`)
    }
  })

  await test('callService: unknown method returns error', async () => {
    try {
      await client.callService('identity', 'nonexistent-method', {}, { timeout: 5000 })
      assert(false, 'Should have thrown')
    } catch (err) {
      assert(err.message !== 'SERVICE_TIMEOUT', 'Got timeout instead of error')
      log('ℹ️', `Error for unknown method: ${err.message}`)
    }
  })

  // ──────────────────────────────────────────
  // 6. Concurrent Calls
  // ──────────────────────────────────────────
  console.log('\n── Test Group 6: Concurrent Calls ──')

  await test('5 concurrent callService calls', async () => {
    const calls = [
      client.callService('identity', 'whoami', {}, { timeout: 10000 }),
      client.callService('schema', 'list', {}, { timeout: 10000 }),
      client.callService('sla', 'list', {}, { timeout: 10000 }),
      client.callService('sla', 'stats', {}, { timeout: 10000 }),
      client.callService('identity', 'whoami', {}, { timeout: 10000 })
    ]
    const results = await Promise.allSettled(calls)
    const fulfilled = results.filter(r => r.status === 'fulfilled')
    assert(fulfilled.length >= 4, `Expected ≥4 successful, got ${fulfilled.length}/5`)
    log('ℹ️', `${fulfilled.length}/5 concurrent calls succeeded`)
  })

  // ──────────────────────────────────────────
  // 7. Relay Selection
  // ──────────────────────────────────────────
  console.log('\n── Test Group 7: Relay Selection ──')

  await test('getRelays includes service channel info', async () => {
    const relays = client.getRelays()
    const withService = relays.filter(r => r.hasServiceProtocol)
    assert(withService.length > 0, 'No relays with service channel')
    log('ℹ️', `${withService.length}/${relays.length} relays have service channel`)
  })

  await test('callService on specific relay', async () => {
    const relays = client.getRelays().filter(r => r.hasServiceProtocol)
    if (relays.length === 0) throw new Error('No relays with service channel')

    const result = await client.callService('identity', 'whoami', {}, {
      relay: relays[0].pubkey,
      timeout: 10000
    })
    assert(result?.pubkey, 'Missing pubkey')
    log('ℹ️', `Specific relay ${relays[0].pubkey.slice(0, 16)}... responded: ${result.pubkey.slice(0, 16)}...`)
  })

  // ──────────────────────────────────────────
  // 8. Latency
  // ──────────────────────────────────────────
  console.log('\n── Test Group 8: P2P Service Latency ──')

  await test('P2P callService latency benchmark', async () => {
    const times = []
    for (let i = 0; i < 5; i++) {
      const start = Date.now()
      await client.callService('identity', 'whoami', {}, { timeout: 10000 })
      times.push(Date.now() - start)
    }
    const avg = Math.round(times.reduce((a, b) => a + b, 0) / times.length)
    const min = Math.min(...times)
    const max = Math.max(...times)
    log('ℹ️', `P2P latency: avg=${avg}ms, min=${min}ms, max=${max}ms`)
    assert(avg < 5000, `Average latency ${avg}ms too high`)
  })

  // ──────────────────────────────────────────
  // Cleanup
  // ──────────────────────────────────────────
  await client.destroy()

  // ──────────────────────────────────────────
  // Summary
  // ──────────────────────────────────────────
  console.log('\n╔═══════════════════════════════════════════════════╗')
  console.log(`║  Results: ${passed} passed, ${failed} failed`)
  console.log('╚═══════════════════════════════════════════════════╝')
  if (failed > 0) {
    console.log('\nFailed tests:')
    for (const r of results.filter(r => r.status === 'fail')) console.log(`  ❌ ${r.name}: ${r.error}`)
  }
  console.log()
  process.exit(failed > 0 ? 1 : 0)
}

runTests().catch(err => { console.error('Fatal:', err); process.exit(1) })