OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_interop.js

172 lines · javascript ref:
#!/usr/bin/env node
// E2E Interop Test: JS HyperDHT server ↔ C++ client via local testnet.
//
// Uses HyperDHT's own createTestnet() — the JS source of truth for testnet
// creation. This ensures the testnet nodes are full HyperDHT nodes (not raw
// dht-rpc) that handle PEER_HANDSHAKE, LOOKUP, ANNOUNCE, etc.
//
// Flow:
//   1. Create local testnet (3 HyperDHT nodes, non-ephemeral)
//   2. Create HyperDHT server on the testnet
//   3. Spawn C++ test_interop_client binary with testnet bootstrap
//   4. C++ bootstraps → find_peer → Noise IK handshake → holepunch
//   5. Verify C++ successfully connected (exit code 0)
//
// Usage: node test_interop.js
// Requires: ./test_interop_client binary 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 = 15000

// ---- Main ----

async function main () {
  console.log('=== E2E Interop Test: JS HyperDHT ↔ C++ Client ===\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().
  // These are full HyperDHT nodes (not raw dht-rpc) — they handle
  // PEER_HANDSHAKE, LOOKUP, ANNOUNCE and store peer records.
  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')

  // Create HyperDHT server on testnet.
  const keyPair = DHT.keyPair()
  console.log('\nServer public key:', keyPair.publicKey.toString('hex'))

  const dht = new DHT({ bootstrap: testnet.bootstrap })
  const server = dht.createServer()

  let connectionReceived = false
  let receivedDataStr = ''

  server.on('connection', (socket) => {
    connectionReceived = true
    console.log('\n--- JS server: incoming connection ---')
    console.log('  Remote public key:', socket.remotePublicKey.toString('hex').slice(0, 16) + '...')

    socket.on('data', (chunk) => {
      receivedDataStr += chunk.toString()
      console.log('  JS received:', chunk.length, 'bytes:', JSON.stringify(chunk.toString()))
    })

    socket.on('end', () => {
      console.log('  JS connection stream ended')
    })

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

    // Send response.
    socket.write(Buffer.from('Hello from JS'))
  })

  try {
    await server.listen(keyPair)
    console.log('Server listening on DHT')
    check(true, 'Server started')

    // Spawn C++ client with bootstrap addresses.
    const clientBin = path.join(__dirname, 'test_interop_client')
    const pkHex = keyPair.publicKey.toString('hex')

    const allBootstrap = testnet.bootstrap.map(b => b.host + ':' + b.port).join(',')

    console.log('\nSpawning C++ client...')
    console.log('  Binary:', clientBin)
    console.log('  Bootstrap:', allBootstrap)
    console.log('  Remote PK:', pkHex.slice(0, 16) + '...\n')

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

    let stdout = ''
    let stderr = ''

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

    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)
      }
    })

    // Wait for C++ process.
    const exitCode = await new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        child.kill('SIGKILL')
        reject(new Error('C++ client timed out after ' + TIMEOUT_MS + 'ms'))
      }, TIMEOUT_MS)

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

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

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

    check(exitCode === 0, 'C++ client exit code 0')
    check(stdout.includes('bootstrap'), 'C++ client bootstrapped')
    check(stdout.includes('handshake') || stdout.includes('connected'), 'C++ connection established')

    // Check JS side received a connection.
    if (connectionReceived) {
      check(true, 'JS server received connection')
    } else {
      console.log('  NOTE: No JS-side connection yet (expected if only holepunch completed)')
    }

  } catch (err) {
    console.error('Test error:', err.message)
    check(false, 'Test completed without error')
  } finally {
    await server.close()
    await dht.destroy()
    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)
})