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