diff --git a/src/dialer/dial-request.js b/src/dialer/dial-request.js index 171ba4cd..d297257b 100644 --- a/src/dialer/dial-request.js +++ b/src/dialer/dial-request.js @@ -14,7 +14,7 @@ class DialRequest { * * @param {object} options * @param {Multiaddr[]} options.addrs - * @param {TransportManager} options.transportManager + * @param {function(Multiaddr):Promise} options.dialAction * @param {Dialer} options.dialer */ constructor ({ @@ -31,7 +31,6 @@ class DialRequest { * @async * @param {object} options * @param {AbortSignal} options.signal An AbortController signal - * @param {number} options.timeout The max dial time for each request * @returns {Connection} */ async run (options) { diff --git a/test/dialing/dial-request.spec.js b/test/dialing/dial-request.spec.js new file mode 100644 index 00000000..477e992d --- /dev/null +++ b/test/dialing/dial-request.spec.js @@ -0,0 +1,180 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +chai.use(require('chai-as-promised')) +const { expect } = chai +const sinon = require('sinon') + +const { AbortError } = require('libp2p-interfaces/src/transport/errors') +const AbortController = require('abort-controller') +const AggregateError = require('aggregate-error') +const pDefer = require('p-defer') +const delay = require('delay') + +const { DialRequest } = require('../../src/dialer/dial-request') +const createMockConnection = require('../utils/mockConnection') + +describe('Dial Request', () => { + it('should end when a single multiaddr dials succeeds', async () => { + const mockConnection = await createMockConnection() + const actions = { + [1]: () => Promise.reject(), + [2]: () => Promise.resolve(mockConnection), + [3]: () => Promise.reject() + } + const dialAction = (num) => actions[num]() + const tokens = ['a', 'b'] + const controller = new AbortController() + const dialer = { + getTokens: () => [...tokens], + releaseToken: () => {} + } + + const dialRequest = new DialRequest({ + addrs: Object.keys(actions), + dialer, + dialAction + }) + + sinon.spy(actions, 1) + sinon.spy(actions, 2) + sinon.spy(actions, 3) + sinon.spy(dialer, 'releaseToken') + const result = await dialRequest.run({ signal: controller.signal }) + expect(result).to.equal(mockConnection) + expect(actions[1]).to.have.property('callCount', 1) + expect(actions[2]).to.have.property('callCount', 1) + expect(actions[3]).to.have.property('callCount', 0) + expect(dialer.releaseToken).to.have.property('callCount', tokens.length) + }) + + it('should throw an AggregateError if all dials fail', async () => { + const actions = { + [1]: () => Promise.reject(), + [2]: () => Promise.reject(), + [3]: () => Promise.reject() + } + const dialAction = (num) => actions[num]() + const addrs = Object.keys(actions) + const tokens = ['a', 'b'] + const controller = new AbortController() + const dialer = { + getTokens: () => [...tokens], + releaseToken: () => {} + } + + const dialRequest = new DialRequest({ + addrs, + dialer, + dialAction + }) + + sinon.spy(actions, 1) + sinon.spy(actions, 2) + sinon.spy(actions, 3) + sinon.spy(dialer, 'getTokens') + sinon.spy(dialer, 'releaseToken') + + try { + await dialRequest.run({ signal: controller.signal }) + expect.fail('Should have thrown') + } catch (err) { + expect(err).to.be.an.instanceof(AggregateError) + } + + expect(actions[1]).to.have.property('callCount', 1) + expect(actions[2]).to.have.property('callCount', 1) + expect(actions[3]).to.have.property('callCount', 1) + expect(dialer.getTokens.calledWith(addrs.length)).to.equal(true) + expect(dialer.releaseToken).to.have.property('callCount', tokens.length) + }) + + it('should handle a large number of addrs', async () => { + const reject = sinon.stub().callsFake(() => Promise.reject()) + const actions = {} + const addrs = [...new Array(25)].map((_, index) => index + 1) + addrs.forEach(addr => { + actions[addr] = reject + }) + + const dialAction = (addr) => actions[addr]() + const tokens = ['a', 'b'] + const controller = new AbortController() + const dialer = { + getTokens: () => [...tokens], + releaseToken: () => {} + } + + const dialRequest = new DialRequest({ + addrs, + dialer, + dialAction + }) + + sinon.spy(dialer, 'releaseToken') + try { + await dialRequest.run({ signal: controller.signal }) + expect.fail('Should have thrown') + } catch (err) { + expect(err).to.be.an.instanceof(AggregateError) + } + + expect(reject).to.have.property('callCount', addrs.length) + expect(dialer.releaseToken).to.have.property('callCount', tokens.length) + }) + + it('should abort all dials when its signal is aborted', async () => { + const deferToAbort = ({ signal }) => { + if (signal.aborted) throw new Error('already aborted') + const deferred = pDefer() + const onAbort = () => { + deferred.reject(new AbortError()) + signal.removeEventListener('abort', onAbort) + } + signal.addEventListener('abort', onAbort) + return deferred.promise + } + + const actions = { + [1]: deferToAbort, + [2]: deferToAbort, + [3]: deferToAbort + } + const dialAction = (num, opts) => actions[num](opts) + const addrs = Object.keys(actions) + const tokens = ['a', 'b'] + const controller = new AbortController() + const dialer = { + getTokens: () => [...tokens], + releaseToken: () => {} + } + + const dialRequest = new DialRequest({ + addrs, + dialer, + dialAction + }) + + sinon.spy(actions, 1) + sinon.spy(actions, 2) + sinon.spy(actions, 3) + sinon.spy(dialer, 'getTokens') + sinon.spy(dialer, 'releaseToken') + + try { + setTimeout(() => controller.abort(), 100) + await dialRequest.run({ signal: controller.signal }) + expect.fail('dial should have failed') + } catch (err) { + expect(err).to.be.an.instanceof(AggregateError) + } + + expect(actions[1]).to.have.property('callCount', 1) + expect(actions[2]).to.have.property('callCount', 1) + expect(actions[3]).to.have.property('callCount', 1) + expect(dialer.getTokens.calledWith(addrs.length)).to.equal(true) + expect(dialer.releaseToken).to.have.property('callCount', tokens.length) + }) +})