#!/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)
})