OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / test / p2p-connectivity.js

173 lines · javascript ref:
#!/usr/bin/env bare
/**
 * P2P Connectivity Test Suite
 *
 * Run on any machine to verify Hyperswarm connectivity:
 *   bare test/p2p-connectivity.js [tabertron-server-key]
 *
 * Tests:
 *   1. Hyperswarm initialization (can we create a swarm?)
 *   2. DHT bootstrap (can we reach the DHT network?)
 *   3. Topic announcement (can we announce + discover?)
 *   4. Peer connection (can we connect to a specific server?)
 *   5. git-remote-punch (does the git transport work?)
 */

import Hyperswarm from 'hyperswarm'
import crypto from 'hypercore-crypto'
import { spawn } from 'bare-subprocess'
import os from 'bare-os'
import process from 'bare-process'

const SERVER_KEY = process.argv[2] || 'f580547b1fdba52331b0ed9483fc2cbe50fb3fb3cc76c904b580b7b7ee6f3d15'
const REPO = process.argv[3] || 'hyper-overlay'
const TIMEOUT = 15000

let passed = 0
let failed = 0

// Catch uncaught errors from streams — prevent crash
process.on('uncaughtException', (err) => {
  console.log(`  [uncaught] ${err.code || err.message}`)
})

function ok (name) { passed++; console.log(`  ✓ ${name}`) }
function fail (name, reason) { failed++; console.log(`  ✗ ${name}: ${reason}`) }

async function withTimeout (promise, ms, label) {
  return Promise.race([
    promise,
    new Promise((_, reject) => setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms))
  ])
}

console.log('P2P Connectivity Tests')
console.log('======================')
console.log(`Host: ${os.hostname()}`)
console.log(`Server key: ${SERVER_KEY.slice(0, 8)}...`)
console.log(`Test repo: ${REPO}`)
console.log('')

// Test 1: Hyperswarm initialization
console.log('1. Hyperswarm initialization')
let swarm
try {
  swarm = new Hyperswarm()
  ok('Created Hyperswarm instance')
} catch (err) {
  fail('Hyperswarm creation', err.message)
  process.exit(1)
}

// Test 2: DHT bootstrap
console.log('2. DHT bootstrap')
try {
  // The swarm's DHT bootstraps on first operation
  const topic = crypto.randomBytes(32)
  const discovery = swarm.join(topic, { server: false, client: true })
  await withTimeout(swarm.flush(), 10000, 'DHT flush')
  discovery.destroy()
  ok('DHT bootstrap + flush succeeded')
} catch (err) {
  fail('DHT bootstrap', err.message)
}

// Test 3: Self-connection test (announce + discover same topic)
console.log('3. Self-announce test')
try {
  const swarm2 = new Hyperswarm()
  const topic = crypto.randomBytes(32)

  const serverReady = new Promise((resolve) => {
    swarm2.on('connection', (conn) => {
      conn.destroy()
      resolve()
    })
  })

  swarm2.join(topic, { server: true, client: false })
  await swarm2.flush()

  swarm.join(topic, { server: false, client: true })

  await withTimeout(serverReady, TIMEOUT, 'self-connection')
  ok('Self-connection works (announce + discover)')

  await swarm2.destroy()
} catch (err) {
  fail('Self-connection', err.message)
}

// Test 4: Connect to remote server topic
console.log('4. Remote server connection')
try {
  const keyBuf = Buffer.from(SERVER_KEY, 'hex')
  const topic = crypto.hash(Buffer.concat([keyBuf, Buffer.from(REPO, 'utf-8')])).subarray(0, 32)

  const swarm3 = new Hyperswarm()
  swarm3.on('error', () => {}) // prevent uncaught

  swarm3.join(topic, { server: false, client: true })

  const result = await withTimeout(
    new Promise((resolve, reject) => {
      swarm3.on('connection', (conn, info) => {
        const peerKey = info.publicKey.toString('hex')
        conn.on('error', () => {}) // prevent uncaught ECONNRESET
        resolve({ conn, peerKey })
      })
    }),
    TIMEOUT,
    'remote connection'
  )

  const shortKey = result.peerKey.slice(0, 12)
  const isSelf = result.peerKey === swarm3.keyPair.publicKey.toString('hex')
  if (isSelf) {
    fail('Remote connection', `connected to SELF (${shortKey}...) — local pear-git serving same topic?`)
  } else {
    ok(`Connected to peer ${shortKey}... (not self)`)
  }
  result.conn.on('error', () => {})
  result.conn.destroy()
  await swarm3.destroy()
} catch (err) {
  fail('Remote connection', err.message)
}

// Test 5: git ls-remote via punch://
console.log('5. git ls-remote via punch://')
try {
  const url = `punch://${SERVER_KEY}/${REPO}`
  const result = await withTimeout(
    new Promise((resolve, reject) => {
      const child = spawn('git', ['ls-remote', url, 'refs/heads/release'], {
        stdio: ['ignore', 'pipe', 'pipe']
      })
      let stdout = ''
      let stderr = ''
      child.stdout.on('data', (d) => { stdout += d.toString() })
      child.stderr.on('data', (d) => { stderr += d.toString() })
      child.on('exit', (code) => {
        if (code === 0) resolve(stdout.trim())
        else reject(new Error(`exit ${code}: ${stderr.trim().split('\n').pop()}`))
      })
      child.on('error', reject)
    }),
    30000,
    'git ls-remote'
  )
  const ref = result.split('\t')[0]
  ok(`git ls-remote returned: ${ref.slice(0, 12)}...`)
} catch (err) {
  fail('git ls-remote', err.message)
}

// Cleanup
await swarm.destroy()

// Summary
console.log('')
console.log(`Results: ${passed} passed, ${failed} failed`)
process.exit(failed > 0 ? 1 : 0)