OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_interop_server.js

185 lines · javascript ref:
#!/usr/bin/env node
// E2E Interop Test (Reverse): JS HyperDHT client → C++ server via local testnet.
//
// This tests the REVERSE path from test_interop.js:
//   test_interop.js:     JS server → JS relay → C++ client
//   test_interop_server: JS client → JS relay → C++ server
//
// Flow:
//   1. Create local testnet (3 HyperDHT nodes, non-ephemeral)
//   2. Spawn C++ test_interop_server_bin (listens on DHT)
//   3. Wait for C++ to print PUBLIC_KEY and READY
//   4. JS client connects to C++ server's public key
//   5. Verify connection established (C++ exits 0)
//
// Usage: node test_interop_server.js
// Requires: ./test_interop_server_bin built via build_tests.sh

const DHT = require('hyperdht')
const createTestnet = require('hyperdht/testnet')
const { spawn } = require('child_process')
const path = require('path')

const TIMEOUT_MS = 20000

// ---- Main ----

async function main () {
  console.log('=== E2E Interop Test (Reverse): JS Client → C++ Server ===\n')

  let passed = 0
  let failed = 0
  const check = (cond, name) => {
    if (cond) {
      passed++
      console.log('  PASS:', name)
    } else {
      failed++
      console.log('  FAIL:', name)
    }
  }

  // Create local testnet using HyperDHT's own createTestnet().
  console.log('Creating local testnet (3 HyperDHT nodes)...')
  const testnet = await createTestnet(3, { teardown: () => {} })
  const bootstrapStr = testnet.bootstrap.map(b => b.host + ':' + b.port)
  console.log('Bootstrap:', bootstrapStr.join(', '))
  check(testnet.nodes.length === 3, 'Testnet created with 3 nodes')

  const allBootstrap = bootstrapStr.join(',')

  // Spawn C++ server binary.
  const serverBin = path.join(__dirname, 'test_interop_server_bin')
  console.log('\nSpawning C++ server...')
  console.log('  Binary:', serverBin)
  console.log('  Bootstrap:', allBootstrap, '\n')

  const child = spawn(serverBin, [allBootstrap], {
    stdio: ['pipe', 'pipe', 'pipe'],
    timeout: TIMEOUT_MS
  })

  let stdout = ''
  let stderr = ''
  let serverPk = null
  let serverReady = false

  // Promise that resolves when C++ server prints READY.
  const readyPromise = new Promise((resolve, reject) => {
    const readyTimeout = setTimeout(() => {
      reject(new Error('C++ server did not become READY within timeout'))
    }, TIMEOUT_MS)

    child.stdout.on('data', (d) => {
      const s = d.toString()
      stdout += s

      for (const line of s.split('\n').filter(Boolean)) {
        console.log('[C++]', line)

        // Parse public key.
        if (line.startsWith('PUBLIC_KEY:')) {
          serverPk = Buffer.from(line.slice('PUBLIC_KEY:'.length).trim(), 'hex')
          console.log('  → Parsed server PK:', serverPk.toString('hex').slice(0, 16) + '...')
        }

        // Server is ready to accept connections.
        if (line === 'READY') {
          serverReady = true
          clearTimeout(readyTimeout)
          resolve()
        }
      }
    })

    child.stderr.on('data', (d) => {
      const s = d.toString()
      stderr += s
      for (const line of s.split('\n').filter(Boolean)) {
        console.log('[C++ err]', line)
      }
    })

    child.on('error', (err) => {
      clearTimeout(readyTimeout)
      reject(err)
    })

    child.on('exit', (code) => {
      clearTimeout(readyTimeout)
      if (!serverReady) {
        reject(new Error('C++ server exited before READY (code=' + code + ')'))
      }
    })
  })

  try {
    // Wait for C++ server to be ready.
    await readyPromise
    check(serverReady, 'C++ server bootstrapped and listening')
    check(serverPk !== null && serverPk.length === 32, 'C++ server public key parsed (32 bytes)')

    // Connect as JS client.
    console.log('\n--- JS client connecting to C++ server ---')
    const clientDht = new DHT({ bootstrap: testnet.bootstrap })
    const socket = clientDht.connect(serverPk)

    let jsConnected = false

    socket.on('open', () => {
      jsConnected = true
      console.log('JS client: stream OPEN')
      console.log('  Remote PK:', socket.remotePublicKey.toString('hex').slice(0, 16) + '...')
    })

    socket.on('error', (err) => {
      console.log('JS client error:', err.message)
    })

    socket.on('close', () => {
      console.log('JS client: stream closed')
    })

    // Wait for C++ process to exit (it exits 0 on successful incoming connection).
    const exitCode = await new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        child.kill('SIGKILL')
        reject(new Error('C++ server timed out after ' + TIMEOUT_MS + 'ms'))
      }, TIMEOUT_MS)

      child.on('exit', (code) => {
        clearTimeout(timer)
        resolve(code)
      })
    })

    console.log('\n--- Results ---')
    console.log('C++ exit code:', exitCode)

    check(exitCode === 0, 'C++ server exit code 0 (incoming connection succeeded)')
    check(stdout.includes('HANDSHAKE REQUEST'), 'C++ received handshake request')
    check(stdout.includes('HANDSHAKE COMPLETE'), 'C++ completed handshake')
    check(stdout.includes('CONNECTED'), 'C++ reported connection established')

    // Cleanup JS client.
    socket.destroy()
    await clientDht.destroy()

  } catch (err) {
    console.error('Test error:', err.message)
    check(false, 'Test completed without error')
    // Kill C++ process if still running.
    try { child.kill('SIGKILL') } catch (_) {}
  } finally {
    await testnet.destroy()
  }

  console.log('\n=== Summary:', passed + '/' + (passed + failed), 'passed ===')
  process.exit(failed > 0 ? 1 : 0)
}

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