From a6c1c533cf7741b1988d2ee283da52c2e7798487 Mon Sep 17 00:00:00 2001 From: Andrew Osheroff Date: Tue, 7 Sep 2021 14:13:03 +0200 Subject: [PATCH] Extract tests into separate files --- test/chaos.js | 102 ++++++++++++++++++ test/firewall.js | 96 +++++++++++++++++ test/helpers/index.js | 64 ++++++++++- test/{basic.js => swarm.js} | 206 +----------------------------------- 4 files changed, 262 insertions(+), 206 deletions(-) create mode 100644 test/chaos.js create mode 100644 test/firewall.js rename test/{basic.js => swarm.js} (63%) diff --git a/test/chaos.js b/test/chaos.js new file mode 100644 index 00000000..49090915 --- /dev/null +++ b/test/chaos.js @@ -0,0 +1,102 @@ +const crypto = require('hypercore-crypto') +const random = require('math-random-seed') +const { timeout } = require('nonsynchronous') + +const Hyperswarm = require('..') +const { test, destroyAll } = require('./helpers') + +const BACKOFFS = [ + 100, + 200, + 300 +] + +test('chaos - recovers after random disconnections (takes ~60s)', async (bootstrap, t) => { + const SEED = 'hyperswarm v3' + const NUM_SWARMS = 10 + const NUM_TOPICS = 15 + const NUM_FORCE_DISCONNECTS = 30 + + const STARTUP_DURATION = 1000 * 5 + const TEST_DURATION = 1000 * 45 + const CHAOS_DURATION = 1000 * 10 + + const swarms = [] + const topics = [] + const connections = [] + const peersBySwarm = new Map() + const rand = random(SEED) + + for (let i = 0; i < NUM_SWARMS; i++) { + const swarm = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) + swarms.push(swarm) + peersBySwarm.set(swarm, new Set()) + swarm.on('connection', conn => { + connections.push(conn) + + conn.on('error', noop) + conn.on('close', () => { + clearInterval(timer) + const idx = connections.indexOf(conn) + if (idx === -1) return + connections.splice(idx, 1) + }) + + const timer = setInterval(() => { + conn.write(Buffer.alloc(10)) + }, 100) + conn.write(Buffer.alloc(10)) + }) + } + for (let i = 0; i < NUM_TOPICS; i++) { + const topic = crypto.randomBytes(32) + topics.push(topic) + } + + for (const topic of topics) { + const numSwarms = Math.round(rand() * NUM_SWARMS) + const topicSwarms = [] + for (let i = 0; i < numSwarms; i++) { + topicSwarms.push(swarms[Math.floor(rand() * NUM_SWARMS)]) + } + for (const swarm of topicSwarms) { + const peers = peersBySwarm.get(swarm) + for (const s of topicSwarms) { + if (swarm === s) continue + peers.add(s.keyPair.publicKey.toString('hex')) + } + await swarm.join(topic).flushed() + } + } + + await Promise.all(swarms.map(s => s.flush())) + await timeout(STARTUP_DURATION) + + // Randomly destroy connections during the chaos period. + for (let i = 0; i < NUM_FORCE_DISCONNECTS; i++) { + const timeout = Math.floor(rand() * CHAOS_DURATION) // Leave a lot of room at the end for reestablishing connections (timeouts) + setTimeout(() => { + if (!connections.length) return + const idx = Math.floor(rand() * connections.length) + const conn = connections[idx] + conn.destroy() + }, timeout) + } + + await timeout(TEST_DURATION) // Wait for the chaos to resolve + + for (const [swarm, expectedPeers] of peersBySwarm) { + t.same(swarm.connections.size, expectedPeers.size, 'swarm has the correct number of connections') + const missingKeys = [] + for (const conn of swarm.connections) { + const key = conn.remotePublicKey.toString('hex') + if (!expectedPeers.has(key)) missingKeys.push(key) + } + t.same(missingKeys.length, 0, 'swarm is not missing any expected peers') + } + + await destroyAll(...swarms) + t.end() +}) + +function noop () {} diff --git a/test/firewall.js b/test/firewall.js new file mode 100644 index 00000000..d4803459 --- /dev/null +++ b/test/firewall.js @@ -0,0 +1,96 @@ +const { timeout } = require('nonsynchronous') + +const Hyperswarm = require('..') +const { test, destroyAll } = require('./helpers') + +const CONNECTION_TIMEOUT = 100 +const BACKOFFS = [ + 100, + 200, + 300 +] + +test('firewalled server - bad client is rejected', async (bootstrap, t) => { + const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) + const swarm2 = new Hyperswarm({ + bootstrap, + backoffs: BACKOFFS, + jitter: 0, + firewall: remotePublicKey => { + return !remotePublicKey.equals(swarm1.keyPair.publicKey) + } + }) + + let serverConnections = 0 + swarm2.on('connection', () => serverConnections++) + + const topic = Buffer.alloc(32).fill('hello world') + await swarm2.join(topic, { client: false, server: true }).flushed() + + swarm1.join(topic, { client: true, server: false }) + + await timeout(CONNECTION_TIMEOUT) + + t.same(serverConnections, 0, 'server did not receive an incoming connection') + + await destroyAll(swarm1, swarm2) + t.end() +}) + +test('firewalled client - bad server is rejected', async (bootstrap, t) => { + const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) + const swarm2 = new Hyperswarm({ + bootstrap, + backoffs: BACKOFFS, + jitter: 0, + firewall: remotePublicKey => { + return !remotePublicKey.equals(swarm1.keyPair.publicKey) + } + }) + + let clientConnections = 0 + swarm2.on('connection', () => clientConnections++) + + const topic = Buffer.alloc(32).fill('hello world') + await swarm1.join(topic, { client: false, server: true }).flushed() + + swarm2.join(topic, { client: true, server: false }) + + await timeout(CONNECTION_TIMEOUT) + + t.same(clientConnections, 0, 'client did not receive an incoming connection') + + await destroyAll(swarm1, swarm2) + t.end() +}) + +test('firewalled server - rejection does not trigger retry cascade', async (bootstrap, t) => { + const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) + + let firewallCalls = 0 + const swarm2 = new Hyperswarm({ + bootstrap, + backoffs: BACKOFFS, + jitter: 0, + firewall: remotePublicKey => { + firewallCalls++ + return !remotePublicKey.equals(swarm1.keyPair.publicKey) + } + }) + + let serverConnections = 0 + swarm2.on('connection', () => serverConnections++) + + const topic = Buffer.alloc(32).fill('hello world') + await swarm2.join(topic).flushed() + + swarm1.join(topic) + + await timeout(BACKOFFS[2] * 5) // Wait for many retries -- there should only be 3 + + t.same(serverConnections, 0, 'server did not receive an incoming connection') + t.same(firewallCalls, 3, 'client retried 3 times') + + await destroyAll(swarm1, swarm2) + t.end() +}) diff --git a/test/helpers/index.js b/test/helpers/index.js index d58fec34..607c2377 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -2,7 +2,9 @@ const tape = require('tape') const HyperDHT = require('@hyperswarm/dht') const Hyperswarm = require('../../') -module.exports = { test, swarm, destroy, defer } +const CONNECTION_TIMEOUT = 100 + +module.exports = { test, swarm, destroy, destroyAll, timeoutPromise, planPromise, defer } test.only = (name, fn) => test(name, fn, true, false) test.skip = (name, fn) => test(name, fn, false, true) @@ -64,3 +66,63 @@ async function test (name, fn, only = false, skip = false) { destroy(nodes) } } + +async function destroyAll (...swarms) { + for (const swarm of swarms) { + await swarm.clear() + } + for (const swarm of swarms) { + await swarm.destroy() + } +} + +function timeoutPromise (ms = CONNECTION_TIMEOUT) { + let res = null + let rej = null + let timer = null + + const p = new Promise((resolve, reject) => { + res = resolve + rej = reject + }) + p.resolve = res + p.reject = rej + p.reset = () => { + if (timer) clearTimeout(timer) + timer = setTimeout(() => p.reject(new Error('Timed out')), ms) + } + + p.reset() + return p +} + +function planPromise (t, count) { + let tick = 0 + let res = null + let rej = null + let timer = null + + let p = new Promise((resolve, reject) => { + res = resolve + rej = reject + }) + + return { + plan: (n) => { + const promise = planPromise(t, n) + p = Promise.all([p, promise]) + return promise + }, + pass: (msg) => { + t.pass(msg) + if (++tick === count) { + if (timer) clearTimeout(timer) + return res() + } + }, + timeout: (ms) => { + timer = setTimeout(rej, ms, new Error('Plan promise timed out')) + }, + then: (...args) => p.then(...args) + } +} diff --git a/test/basic.js b/test/swarm.js similarity index 63% rename from test/basic.js rename to test/swarm.js index a277da18..8f6db9f3 100644 --- a/test/basic.js +++ b/test/swarm.js @@ -1,9 +1,7 @@ -const crypto = require('hypercore-crypto') -const random = require('math-random-seed') const { timeout } = require('nonsynchronous') const Hyperswarm = require('..') -const { test } = require('./helpers') +const { test, destroyAll, timeoutPromise } = require('./helpers') const CONNECTION_TIMEOUT = 100 const BACKOFFS = [ @@ -296,91 +294,6 @@ test('two servers, one client - refreshing a peer discovery instance discovers n t.end() }) -test('firewalled server - bad client is rejected', async (bootstrap, t) => { - const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) - const swarm2 = new Hyperswarm({ - bootstrap, - backoffs: BACKOFFS, - jitter: 0, - firewall: remotePublicKey => { - return !remotePublicKey.equals(swarm1.keyPair.publicKey) - } - }) - - let serverConnections = 0 - swarm2.on('connection', () => serverConnections++) - - const topic = Buffer.alloc(32).fill('hello world') - await swarm2.join(topic, { client: false, server: true }).flushed() - - swarm1.join(topic, { client: true, server: false }) - - await timeout(CONNECTION_TIMEOUT) - - t.same(serverConnections, 0, 'server did not receive an incoming connection') - - await destroyAll(swarm1, swarm2) - t.end() -}) - -test('firewalled client - bad server is rejected', async (bootstrap, t) => { - const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) - const swarm2 = new Hyperswarm({ - bootstrap, - backoffs: BACKOFFS, - jitter: 0, - firewall: remotePublicKey => { - return !remotePublicKey.equals(swarm1.keyPair.publicKey) - } - }) - - let clientConnections = 0 - swarm2.on('connection', () => clientConnections++) - - const topic = Buffer.alloc(32).fill('hello world') - await swarm1.join(topic, { client: false, server: true }).flushed() - - swarm2.join(topic, { client: true, server: false }) - - await timeout(CONNECTION_TIMEOUT) - - t.same(clientConnections, 0, 'client did not receive an incoming connection') - - await destroyAll(swarm1, swarm2) - t.end() -}) - -test('firewalled server - rejection does not trigger retry cascade', async (bootstrap, t) => { - const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) - - let firewallCalls = 0 - const swarm2 = new Hyperswarm({ - bootstrap, - backoffs: BACKOFFS, - jitter: 0, - firewall: remotePublicKey => { - firewallCalls++ - return !remotePublicKey.equals(swarm1.keyPair.publicKey) - } - }) - - let serverConnections = 0 - swarm2.on('connection', () => serverConnections++) - - const topic = Buffer.alloc(32).fill('hello world') - await swarm2.join(topic).flushed() - - swarm1.join(topic) - - await timeout(BACKOFFS[2] * 5) // Wait for many retries -- there should only be 3 - - t.same(serverConnections, 0, 'server did not receive an incoming connection') - t.same(firewallCalls, 3, 'client retried 3 times') - - await destroyAll(swarm1, swarm2) - t.end() -}) - test('one server, one client - correct deduplication when a client connection is destroyed', async (bootstrap, t) => { const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) const swarm2 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) @@ -423,121 +336,4 @@ test('one server, one client - correct deduplication when a client connection is t.end() }) -test('chaos - recovers after random disconnections (takes ~60s)', async (bootstrap, t) => { - const SEED = 'hyperswarm v3' - const NUM_SWARMS = 10 - const NUM_TOPICS = 15 - const NUM_FORCE_DISCONNECTS = 30 - - const STARTUP_DURATION = 1000 * 5 - const TEST_DURATION = 1000 * 45 - const CHAOS_DURATION = 1000 * 10 - - const swarms = [] - const topics = [] - const connections = [] - const peersBySwarm = new Map() - const rand = random(SEED) - - for (let i = 0; i < NUM_SWARMS; i++) { - const swarm = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 }) - swarms.push(swarm) - peersBySwarm.set(swarm, new Set()) - swarm.on('connection', conn => { - connections.push(conn) - - conn.on('error', noop) - conn.on('close', () => { - clearInterval(timer) - const idx = connections.indexOf(conn) - if (idx === -1) return - connections.splice(idx, 1) - }) - - const timer = setInterval(() => { - conn.write(Buffer.alloc(10)) - }, 100) - conn.write(Buffer.alloc(10)) - }) - } - for (let i = 0; i < NUM_TOPICS; i++) { - const topic = crypto.randomBytes(32) - topics.push(topic) - } - - for (const topic of topics) { - const numSwarms = Math.round(rand() * NUM_SWARMS) - const topicSwarms = [] - for (let i = 0; i < numSwarms; i++) { - topicSwarms.push(swarms[Math.floor(rand() * NUM_SWARMS)]) - } - for (const swarm of topicSwarms) { - const peers = peersBySwarm.get(swarm) - for (const s of topicSwarms) { - if (swarm === s) continue - peers.add(s.keyPair.publicKey.toString('hex')) - } - await swarm.join(topic).flushed() - } - } - - await Promise.all(swarms.map(s => s.flush())) - await timeout(STARTUP_DURATION) - - // Randomly destroy connections during the chaos period. - for (let i = 0; i < NUM_FORCE_DISCONNECTS; i++) { - const timeout = Math.floor(rand() * CHAOS_DURATION) // Leave a lot of room at the end for reestablishing connections (timeouts) - setTimeout(() => { - if (!connections.length) return - const idx = Math.floor(rand() * connections.length) - const conn = connections[idx] - conn.destroy() - }, timeout) - } - - await timeout(TEST_DURATION) // Wait for the chaos to resolve - - for (const [swarm, expectedPeers] of peersBySwarm) { - t.same(swarm.connections.size, expectedPeers.size, 'swarm has the correct number of connections') - const missingKeys = [] - for (const conn of swarm.connections) { - const key = conn.remotePublicKey.toString('hex') - if (!expectedPeers.has(key)) missingKeys.push(key) - } - t.same(missingKeys.length, 0, 'swarm is not missing any expected peers') - } - - await destroyAll(...swarms) - t.end() -}) - -async function destroyAll (...swarms) { - for (const swarm of swarms) { - await swarm.clear() - } - for (const swarm of swarms) { - await swarm.destroy() - } -} - -function timeoutPromise (ms = CONNECTION_TIMEOUT) { - let res = null - let rej = null - let timer = null - - const p = new Promise((resolve, reject) => { - res = resolve - rej = reject - }) - p.resolve = res - p.reject = rej - p.reset = () => { - if (timer) clearTimeout(timer) - timer = setTimeout(() => p.reject(new Error('Timed out')), ms) - } - - p.reset() - return p -} - function noop () {}