OverviewFilesCommitsIssuesDocsReleasesPeers

p2pbuilders / test / m3.js

142 lines · javascript ref:
'use strict'

// M3: peer key-exchange via the announce protomux channel.
// Two nodes that join the same board should auto-discover each other's pubkey
// without any manual trackUser() call.

const assert = require('assert/strict')
const b4a = require('b4a')
const { Node } = require('../src/backend/node')
const createTestnet = require('hyperdht/testnet')
const { decodeOp } = require('../src/backend/ops')
const { localSwarm } = require('./_helpers')

const tests = []
function test (name, fn) { tests.push({ name, fn }) }

test('swarm+announce: B auto-tracks A after joining same board', async () => {
  const testnet = await createTestnet(3, { teardown: () => {} })
  const bootstrap = testnet.bootstrap

  const a = await Node.openTemp({ swarm: localSwarm(bootstrap) })
  const b = await Node.openTemp({ swarm: localSwarm(bootstrap) })

  await a.post('general', 'auto-discovery test', 'body')

  await a.joinBoard('general')
  await b.joinBoard('general')

  // Wait for B to receive an announce for A's pubkey.
  await waitForEvent(b, 'track', (pubkey) => b4a.equals(pubkey, a.pubkey), 15000)

  const remote = b.userCore(a.pubkey)
  assert.ok(remote, 'B has tracked A')
  await remote.update({ wait: true })
  assert.equal(remote.length, 1)
  const op = decodeOp(await remote.get(0))
  assert.equal(op.payload.title, 'auto-discovery test')

  await a.close()
  await b.close()
  await testnet.destroy()
})

test('swarm+announce: transitive discovery (C learns A via B)', async () => {
  const testnet = await createTestnet(3, { teardown: () => {} })
  const bootstrap = testnet.bootstrap

  const a = await Node.openTemp({ swarm: localSwarm(bootstrap) })
  const b = await Node.openTemp({ swarm: localSwarm(bootstrap) })
  const c = await Node.openTemp({ swarm: localSwarm(bootstrap) })

  await a.post('general', 'hello', 'from A')

  // B tracks A first, A and B on 'general' board.
  await a.joinBoard('general')
  await b.joinBoard('general')
  await waitForEvent(b, 'track', (pk) => b4a.equals(pk, a.pubkey), 15000)

  // C joins a DIFFERENT board from A, but same as B (call it 'meta').
  // B also joins 'meta'. Via B, C should learn about A's pubkey.
  await b.joinBoard('meta')
  await c.joinBoard('meta')

  await waitForEvent(c, 'track', (pk) => b4a.equals(pk, a.pubkey), 15000)

  const aCoreOnC = c.userCore(a.pubkey)
  assert.ok(aCoreOnC, 'C discovered A via B')

  await a.close()
  await b.close()
  await c.close()
  await testnet.destroy()
})

test('swarm+announce: late announce — C learns a key B tracked after C connected', async () => {
  const testnet = await createTestnet(3, { teardown: () => {} })
  const bootstrap = testnet.bootstrap

  const a = await Node.openTemp({ swarm: localSwarm(bootstrap) })
  const b = await Node.openTemp({ swarm: localSwarm(bootstrap) })
  const c = await Node.openTemp({ swarm: localSwarm(bootstrap) })

  // B and C connect first, before B knows anything about A. The channel-open
  // key exchange between B and C therefore cannot carry A's key.
  await b.joinBoard('meta')
  await c.joinBoard('meta')
  await waitForEvent(c, 'track', (pk) => b4a.equals(pk, b.pubkey), 15000)

  // Now A shows up on a board shared with B only.
  await a.joinBoard('general')
  await b.joinBoard('general')
  await waitForEvent(b, 'track', (pk) => b4a.equals(pk, a.pubkey), 15000)

  // C must learn A via B's re-broadcast on the already-open channel.
  if (!c.userCore(a.pubkey)) {
    await waitForEvent(c, 'track', (pk) => b4a.equals(pk, a.pubkey), 15000)
  }
  assert.ok(c.userCore(a.pubkey), 'C discovered A via late announce')

  await a.close()
  await b.close()
  await c.close()
  await testnet.destroy()
})

// ---------------------------------------------------------------------------

function waitForEvent (emitter, event, predicate, timeoutMs) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      emitter.off(event, handler)
      reject(new Error(`timeout waiting for ${event}`))
    }, timeoutMs)
    const handler = (arg) => {
      if (predicate(arg)) {
        clearTimeout(timer)
        emitter.off(event, handler)
        resolve(arg)
      }
    }
    emitter.on(event, handler)
  })
}

// runner
;(async () => {
  let pass = 0; let fail = 0
  for (const { name, fn } of tests) {
    try {
      await fn()
      console.log(`  ✓ ${name}`)
      pass++
    } catch (err) {
      console.log(`  ✗ ${name}`)
      console.log(`    ${err.stack || err.message}`)
      fail++
    }
  }
  console.log(`\n${pass}/${pass + fail} passed`)
  process.exit(fail ? 1 : 0)
})()