OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / integration / client.test.js

178 lines · javascript ref:
import test from 'brittle'
import createTestnet from '@hyperswarm/testnet'
import Hyperswarm from 'hyperswarm'
import { RelayNode } from 'p2p-hiverelay/core/relay-node/index.js'
import { HiveRelayClient } from 'p2p-hiverelay-client'
import b4a from 'b4a'
import path from 'path'
import { tmpdir } from 'os'
import { randomBytes } from 'crypto'
import { mkdirSync } from 'fs'

function tmpStorage () {
  const storage = path.join(tmpdir(), 'hiverelay-client-test-' + randomBytes(8).toString('hex'))
  mkdirSync(storage, { recursive: true })
  return storage
}

test('integration: client discovers relay node via DHT', async (t) => {
  const testnet = await createTestnet(3)

  // Start a relay node
  const relay = new RelayNode({
    storage: tmpStorage(),
    bootstrapNodes: testnet.bootstrap,
    enableAPI: false,
    enableMetrics: false
  })
  await relay.start()

  // Create a client swarm and client
  const clientSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
  const client = new HiveRelayClient({ swarm: clientSwarm })

  t.teardown(async () => {
    await client.destroy()
    await clientSwarm.destroy()
    await relay.stop()
    await testnet.destroy()
  })

  await client.start()

  // Flush multiple times to give DHT time to propagate
  await clientSwarm.flush()

  // Poll for connection — DHT discovery can take a few seconds
  let found = false
  for (let i = 0; i < 20; i++) {
    if (client.relays.size > 0) { found = true; break }
    await new Promise((resolve) => setTimeout(resolve, 500))
    await clientSwarm.flush()
  }

  t.ok(found, 'client discovered relay node')

  const relays = client.getRelays()
  t.ok(relays.length >= 1, 'at least one relay in list')
  if (relays.length > 0) {
    t.ok(relays[0].pubkey, 'relay has pubkey')
  }
})

test('integration: client seed request reaches relay', async (t) => {
  const testnet = await createTestnet(3)

  const relay = new RelayNode({
    storage: tmpStorage(),
    bootstrapNodes: testnet.bootstrap,
    enableAPI: false,
    enableMetrics: false
  })
  await relay.start()

  const clientSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
  const client = new HiveRelayClient({ swarm: clientSwarm })

  t.teardown(async () => {
    await client.destroy()
    await clientSwarm.destroy()
    await relay.stop()
    await testnet.destroy()
  })

  await client.start()
  await clientSwarm.flush()

  // Wait for relay to be discovered
  for (let i = 0; i < 20 && client.relays.size === 0; i++) {
    await new Promise((resolve) => setTimeout(resolve, 500))
    await clientSwarm.flush()
  }

  // Create a fake app key and request seeding
  const appKey = randomBytes(32)
  const appKeyHex = b4a.toString(appKey, 'hex')

  // Seed request will broadcast; we verify it was published
  let published = false
  client.on('seed-request-published', ({ appKey: key }) => {
    if (key === appKeyHex) published = true
  })

  // Short timeout since we don't expect relay acceptance in test
  const acceptances = await client.seed(appKey, { replicas: 1, timeout: 3000 })

  t.is(published, true, 'seed request was published')
  t.ok(Array.isArray(acceptances), 'acceptances is an array')
  // Relay won't auto-accept in this setup, so acceptances may be empty — that's ok
  // The important thing is the request reached the network

  const status = client.getSeedStatus(appKeyHex)
  t.ok(status, 'seed status exists')
  t.is(status.appKey, appKeyHex, 'correct app key in status')
})

test('integration: multiple clients find same relay', async (t) => {
  const testnet = await createTestnet(3)

  const relay = new RelayNode({
    storage: tmpStorage(),
    bootstrapNodes: testnet.bootstrap,
    enableAPI: false,
    enableMetrics: false
  })
  await relay.start()

  const swarmA = new Hyperswarm({ bootstrap: testnet.bootstrap })
  const swarmB = new Hyperswarm({ bootstrap: testnet.bootstrap })
  const clientA = new HiveRelayClient({ swarm: swarmA })
  const clientB = new HiveRelayClient({ swarm: swarmB })

  t.teardown(async () => {
    await clientA.destroy()
    await clientB.destroy()
    await swarmA.destroy()
    await swarmB.destroy()
    await relay.stop()
    await testnet.destroy()
  })

  await clientA.start()
  await clientB.start()

  // Wait for both clients to discover the relay
  for (let i = 0; i < 20; i++) {
    if (clientA.relays.size > 0 && clientB.relays.size > 0) break
    await swarmA.flush()
    await swarmB.flush()
    await new Promise((resolve) => setTimeout(resolve, 500))
  }

  const relaysA = clientA.getRelays()
  const relaysB = clientB.getRelays()

  t.ok(relaysA.length >= 1, 'client A found relay')
  t.ok(relaysB.length >= 1, 'client B found relay')
})

test('integration: client destroy cleans up gracefully', async (t) => {
  const testnet = await createTestnet(3)

  const clientSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
  const client = new HiveRelayClient({ swarm: clientSwarm })

  t.teardown(async () => {
    await clientSwarm.destroy()
    await testnet.destroy()
  })

  await client.start()
  t.is(client._started, true, 'client started')

  await client.destroy()
  t.is(client._started, false, 'client stopped')
  t.is(client.relays.size, 0, 'relays cleared')
  t.is(client.seedRequests.size, 0, 'seed requests cleared')
})