OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / test-bootstrap-cache.js

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

/**
 * test-bootstrap-cache.js
 *
 * Validates the BootstrapCache fix:
 *   1. Reads the live bootstrap-cache.json on disk
 *   2. Tests merge() with null, undefined, [], and real peer lists
 *   3. Tests load()/save() round-trip via a temp directory
 *   4. Tests that merge(null) lets Hyperswarm reach the real DHT
 */

import { BootstrapCache } from '../core/bootstrap-cache.js'
import { readFile, mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import Hyperswarm from 'hyperswarm'

const LIVE_CACHE = join(process.env.HOME, '.hiverelay/storage/bootstrap-cache.json')

let passed = 0
let failed = 0

function assert (condition, label) {
  if (condition) {
    console.log(`  PASS  ${label}`)
    passed++
  } else {
    console.log(`  FAIL  ${label}`)
    failed++
  }
}

// ── 1. Read the live cache file ─────────────────────────────────────────────
async function testReadLiveCache () {
  console.log('\n── 1. Read live bootstrap-cache.json ──')
  try {
    const raw = await readFile(LIVE_CACHE, 'utf8')
    const data = JSON.parse(raw)
    assert(data && Array.isArray(data.peers), 'cache file has peers array')
    assert(data.peers.length > 0, `cache contains ${data.peers.length} peer(s)`)
    assert(typeof data.updatedAt === 'number', 'updatedAt is a number')
    console.log(`  (peers: ${data.peers.map(p => p.host + ':' + p.port).join(', ')})`)
  } catch (err) {
    assert(false, 'read live cache: ' + err.message)
  }
}

// ── 2 & 3. merge() behaviour ────────────────────────────────────────────────
async function testMerge () {
  console.log('\n── 2. BootstrapCache.merge() tests ──')

  // Create a cache with some pre-loaded peers so _collectPeers() returns data
  const tmp = await mkdtemp(join(tmpdir(), 'hive-merge-'))
  const cache = new BootstrapCache(tmp)
  // Manually seed _seenPeers to simulate cached peers
  cache._seenPeers.set('10.0.0.1:9000', { host: '10.0.0.1', port: 9000, lastSeen: Date.now() })
  cache._seenPeers.set('10.0.0.2:9001', { host: '10.0.0.2', port: 9001, lastSeen: Date.now() })

  // 2a. merge(null) — should return undefined (use Hyperswarm defaults)
  const r1 = cache.merge(null)
  assert(r1 === undefined, 'merge(null) returns undefined')

  // 2b. merge(undefined) — should return undefined
  const r2 = cache.merge(undefined)
  assert(r2 === undefined, 'merge(undefined) returns undefined')

  // 2c. merge([]) — should return undefined
  const r3 = cache.merge([])
  assert(r3 === undefined, 'merge([]) returns undefined')

  // 2d. merge(configured) WITH cached peers — returns merged array
  const configured = [{ host: '1.2.3.4', port: 49737 }]
  const r4 = cache.merge(configured)
  assert(Array.isArray(r4), 'merge(configured) with cached peers returns an array')
  assert(r4.length === 3, `merged array has 3 entries (1 configured + 2 cached), got ${r4 && r4.length}`)
  assert(r4[0].host === '1.2.3.4' && r4[0].port === 49737, 'configured node is first in merged list')
  const hosts = r4.map(p => p.host)
  assert(hosts.includes('10.0.0.1') && hosts.includes('10.0.0.2'), 'cached peers are in merged list')

  // 2e. merge(configured) WITHOUT cached peers — returns configured list
  const emptyCache = new BootstrapCache(tmp)
  // _seenPeers is empty, so _collectPeers() returns []
  const r5 = emptyCache.merge(configured)
  // When no cached peers, configuredBootstrap is returned as-is (truthy passthrough)
  assert(r5 === configured || (Array.isArray(r5) && r5.length === 1 && r5[0].host === '1.2.3.4'),
    'merge(configured) without cached peers returns the configured list')

  await rm(tmp, { recursive: true, force: true })
}

// ── 4. load()/save() round-trip ─────────────────────────────────────────────
async function testLoadSaveCycle () {
  console.log('\n── 3. load()/save() round-trip ──')

  const tmp = await mkdtemp(join(tmpdir(), 'hive-ls-'))
  const cache1 = new BootstrapCache(tmp)
  cache1._seenPeers.set('5.5.5.5:1234', { host: '5.5.5.5', port: 1234, lastSeen: 111 })
  cache1._seenPeers.set('6.6.6.6:5678', { host: '6.6.6.6', port: 5678, lastSeen: 222 })
  await cache1.save()

  // Read the file back raw to verify format
  const raw = await readFile(join(tmp, 'bootstrap-cache.json'), 'utf8')
  const on_disk = JSON.parse(raw)
  assert(Array.isArray(on_disk.peers), 'saved file has peers array')
  assert(on_disk.peers.length === 2, `saved file has 2 peers, got ${on_disk.peers.length}`)

  // Reload into a fresh instance
  const cache2 = new BootstrapCache(tmp)
  await cache2.load()
  assert(cache2._peers.length === 2, `loaded 2 peers from disk, got ${cache2._peers.length}`)
  const loaded_hosts = cache2._peers.map(p => p.host).sort()
  assert(loaded_hosts.includes('5.5.5.5') && loaded_hosts.includes('6.6.6.6'),
    'loaded peers match saved peers')

  await rm(tmp, { recursive: true, force: true })
}

// ── 5. Hyperswarm with merge(null) connects to real DHT ─────────────────────
async function testHyperswarmDHT () {
  console.log('\n── 4. Hyperswarm with merge(null) reaches real DHT ──')

  const tmp = await mkdtemp(join(tmpdir(), 'hive-dht-'))
  const cache = new BootstrapCache(tmp)
  const bootstrap = cache.merge(null) // must be undefined → Hyperswarm uses defaults

  assert(bootstrap === undefined, 'merge(null) gives undefined bootstrap (Hyperswarm uses defaults)')

  let swarm
  try {
    swarm = new Hyperswarm({ bootstrap })

    // firewalled check — after the swarm opens a socket, dht.firewalled
    // resolves once the node has contacted bootstrap servers
    const dht = swarm.dht

    // Wait for the DHT to be ready (indicates successful bootstrap contact)
    await Promise.race([
      new Promise((resolve) => {
        if (dht.bootstrapped) return resolve()
        dht.once('ready', resolve)
      }),
      new Promise((_resolve, reject) => setTimeout(() => reject(new Error('DHT ready timeout (10s)')), 10000))
    ])
    assert(true, 'DHT bootstrapped successfully (ready event fired)')

    // Check firewalled status is available (boolean)
    const fw = dht.firewalled
    assert(typeof fw === 'boolean', `dht.firewalled is boolean (value: ${fw})`)

    // Try findNode — exercises real DHT connectivity
    const randomKey = Buffer.alloc(32)
    randomKey[0] = 0xab; randomKey[1] = 0xcd
    let findNodeOk = false
    try {
      const q = dht.findNode(randomKey)
      // Consume at least one result or finish
      await Promise.race([
        (async () => { for await (const _ of q) { findNodeOk = true; break } })(), // eslint-disable-line
        new Promise((resolve) => setTimeout(resolve, 5000))
      ])
      // Even if no results, completing without error = DHT reachable
      findNodeOk = true
    } catch (err) {
      console.log(`  (findNode error: ${err.message})`)
    }
    assert(findNodeOk, 'dht.findNode() executed without error (DHT reachable)')
  } finally {
    if (swarm) {
      await swarm.destroy()
    }
    await rm(tmp, { recursive: true, force: true })
  }
}

// ── Run all tests ───────────────────────────────────────────────────────────
async function main () {
  console.log('=== BootstrapCache Test Suite ===')

  await testReadLiveCache()
  await testMerge()
  await testLoadSaveCycle()
  await testHyperswarmDHT()

  console.log(`\n=== Results: ${passed} passed, ${failed} failed, ${passed + failed} total ===`)
  process.exit(failed > 0 ? 1 : 0)
}

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