diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 47310e25f1..412071bf80 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -53,6 +53,7 @@ "@libp2p/kad-dht": "^13.0.0", "@libp2p/logger": "^5.0.0", "@libp2p/mdns": "^11.0.0", + "@libp2p/memory": "^0.0.0", "@libp2p/mplex": "^11.0.0", "@libp2p/peer-id": "^5.0.0", "@libp2p/ping": "^2.0.0", @@ -62,6 +63,7 @@ "@libp2p/webrtc": "^5.0.0", "@libp2p/websockets": "^9.0.0", "@libp2p/webtransport": "^5.0.0", + "@multiformats/dns": "^1.0.6", "@multiformats/mafmt": "^12.1.6", "@multiformats/multiaddr": "^12.2.3", "@multiformats/multiaddr-matcher": "^1.2.1", @@ -71,6 +73,7 @@ "execa": "^9.1.0", "go-libp2p": "^1.5.0", "it-all": "^3.0.6", + "it-map": "^3.1.1", "it-pipe": "^3.0.1", "libp2p": "^2.0.0", "merge-options": "^3.0.4", diff --git a/packages/integration-tests/test/addresses.spec.ts b/packages/integration-tests/test/addresses.spec.ts new file mode 100644 index 0000000000..26123516f1 --- /dev/null +++ b/packages/integration-tests/test/addresses.spec.ts @@ -0,0 +1,159 @@ +/* eslint-env mocha */ + +import { memory } from '@libp2p/memory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { createLibp2p } from 'libp2p' +import { pEvent } from 'p-event' +import type { Libp2p, PeerUpdate } from '@libp2p/interface' +import type { AddressManager } from '@libp2p/interface-internal' +import type { Multiaddr } from '@multiformats/multiaddr' + +const listenAddresses = ['/memory/address-1', '/memory/address-2'] +const announceAddresses = ['/dns4/peer.io/tcp/433/p2p/12D3KooWNvSZnPi3RrhrTwEY4LuuBeB6K6facKUCJcyWG1aoDd2p'] + +describe('addresses', () => { + let libp2p: Libp2p + + afterEach(async () => { + await libp2p?.stop() + }) + + it('should return transport listen addresses if announce addresses are not provided', async () => { + libp2p = await createLibp2p({ + addresses: { + listen: listenAddresses + }, + transports: [ + memory() + ] + }) + + expect(libp2p.getMultiaddrs().map(ma => ma.decapsulate('/p2p').toString())).to.deep.equal(listenAddresses) + }) + + it('should override listen addresses with announce addresses when provided', async () => { + libp2p = await createLibp2p({ + addresses: { + listen: listenAddresses, + announce: announceAddresses + }, + transports: [ + memory() + ] + }) + + expect(libp2p.getMultiaddrs().map(ma => ma.decapsulate('/p2p').toString())).to.deep.equal(announceAddresses) + }) + + it('should filter listen addresses filtered by the announce filter', async () => { + libp2p = await createLibp2p({ + addresses: { + listen: listenAddresses, + announceFilter: (multiaddrs: Multiaddr[]) => multiaddrs.slice(1) + }, + transports: [ + memory() + ] + }) + + expect(libp2p.getMultiaddrs().map(ma => ma.decapsulate('/p2p').toString())).to.deep.equal([listenAddresses[1]]) + }) + + it('should filter announce addresses filtered by the announce filter', async () => { + libp2p = await createLibp2p({ + addresses: { + listen: listenAddresses, + announce: announceAddresses, + announceFilter: () => [] + }, + transports: [ + memory() + ] + }) + + expect(libp2p.getMultiaddrs().map(ma => ma.decapsulate('/p2p').toString())).to.have.lengthOf(0) + }) + + it('should include observed addresses in returned multiaddrs', async () => { + const ma = '/ip4/83.32.123.53/tcp/43928' + + libp2p = await createLibp2p({ + start: false, + addresses: { + listen: listenAddresses + }, + transports: [ + memory() + ], + services: { + observer: (components: { addressManager: AddressManager }) => { + components.addressManager.confirmObservedAddr(multiaddr(ma)) + } + } + }) + + expect(libp2p.getMultiaddrs().map(ma => ma.decapsulate('/p2p').toString())).to.include(ma) + }) + + it('should update our peer record with announce addresses on startup', async () => { + libp2p = await createLibp2p({ + start: false, + addresses: { + listen: listenAddresses, + announce: announceAddresses + }, + transports: [ + memory() + ] + }) + + const eventPromise = pEvent<'self:peer:update', CustomEvent>(libp2p, 'self:peer:update', { + filter: (event) => { + return event.detail.peer.addresses.map(({ multiaddr }) => multiaddr.toString()) + .includes(announceAddresses[0]) + } + }) + + await libp2p.start() + + const event = await eventPromise + + expect(event.detail.peer.addresses.map(({ multiaddr }) => multiaddr.toString())) + .to.include.members(announceAddresses, 'peer info did not include announce addresses') + }) + + it('should only include confirmed observed addresses in peer record', async () => { + const unconfirmedAddress = '/ip4/127.0.0.1/tcp/4010/ws' + const confirmedAddress = '/ip4/127.0.0.1/tcp/4011/ws' + + libp2p = await createLibp2p({ + start: false, + addresses: { + listen: listenAddresses, + announce: announceAddresses + }, + transports: [ + memory() + ], + services: { + observer: (components: { addressManager: AddressManager }) => { + components.addressManager.confirmObservedAddr(multiaddr(confirmedAddress)) + components.addressManager.addObservedAddr(multiaddr(unconfirmedAddress)) + } + } + }) + + await libp2p.start() + + const eventPromise = pEvent<'self:peer:update', CustomEvent>(libp2p, 'self:peer:update') + + const event = await eventPromise + + expect(event.detail.peer.addresses.map(({ multiaddr }) => multiaddr.toString())) + .to.not.include(unconfirmedAddress, 'peer info included unconfirmed observed address') + + expect(event.detail.peer.addresses.map(({ multiaddr }) => multiaddr.toString())) + .to.include(confirmedAddress, 'peer info did not include confirmed observed address') + }) +}) diff --git a/packages/integration-tests/test/connections.spec.ts b/packages/integration-tests/test/connections.spec.ts new file mode 100644 index 0000000000..5906ed48e4 --- /dev/null +++ b/packages/integration-tests/test/connections.spec.ts @@ -0,0 +1,86 @@ +/* eslint-env mocha */ + +import { stop } from '@libp2p/interface' +import { dns } from '@multiformats/dns' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import sinon from 'sinon' +import { createPeers } from './fixtures/create-peers.js' +import type { Echo } from '@libp2p/echo' +import type { Libp2p } from '@libp2p/interface' + +describe('connections', () => { + let dialer: Libp2p<{ echo: Echo }> + let listener: Libp2p<{ echo: Echo }> + + afterEach(async () => { + await stop(dialer, listener) + }) + + it('libp2p.getConnections gets the conns', async () => { + ({ dialer, listener } = await createPeers()) + + const conn = await dialer.dial(listener.getMultiaddrs()) + + expect(conn).to.be.ok() + expect(dialer.getConnections()).to.have.lengthOf(1) + }) + + it('should open multiple connections when forced', async () => { + ({ dialer, listener } = await createPeers()) + + // connect once, should have one connection + await dialer.dial(listener.getMultiaddrs()) + expect(dialer.getConnections()).to.have.lengthOf(1) + + // connect twice, should still only have one connection + await dialer.dial(listener.getMultiaddrs()) + expect(dialer.getConnections()).to.have.lengthOf(1) + + // force connection, should have two connections now + await dialer.dial(listener.getMultiaddrs(), { + force: true + }) + expect(dialer.getConnections()).to.have.lengthOf(2) + }) + + it('should use custom DNS resolver', async () => { + const resolver = sinon.stub() + + ;({ dialer, listener } = await createPeers({ + dns: dns({ + resolvers: { + '.': resolver + } + }) + })) + + const ma = multiaddr('/dnsaddr/example.com/tcp/12345') + const err = new Error('Could not resolve') + + resolver.withArgs('_dnsaddr.example.com').rejects(err) + + await expect(dialer.dial(ma)).to.eventually.be.rejectedWith(err) + }) + + it('should fail to dial if resolve fails and there are no addresses to dial', async () => { + const resolver = sinon.stub() + + ;({ dialer, listener } = await createPeers({ + dns: dns({ + resolvers: { + '.': resolver + } + }) + })) + + const ma = multiaddr('/dnsaddr/example.com/tcp/12345') + + resolver.withArgs('_dnsaddr.example.com').resolves({ + Answer: [] + }) + + await expect(dialer.dial(ma)).to.eventually.be.rejected + .with.property('name', 'NoValidAddressesError') + }) +}) diff --git a/packages/integration-tests/test/core.spec.ts b/packages/integration-tests/test/core.spec.ts new file mode 100644 index 0000000000..e927b22043 --- /dev/null +++ b/packages/integration-tests/test/core.spec.ts @@ -0,0 +1,38 @@ +import { stop } from '@libp2p/interface' +import { memory } from '@libp2p/memory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { createLibp2p } from 'libp2p' +import type { Libp2p } from '@libp2p/interface' + +describe('core', () => { + let libp2p: Libp2p + + afterEach(async () => { + await stop(libp2p) + }) + + it('should say an address is dialable if a transport is configured', async () => { + libp2p = await createLibp2p({ + transports: [ + memory() + ] + }) + + const ma = multiaddr('/memory/address-1') + + await expect(libp2p.isDialable(ma)).to.eventually.be.true() + }) + + it('should say an address is not dialable if a transport is not configured', async () => { + libp2p = await createLibp2p({ + transports: [ + memory() + ] + }) + + const ma = multiaddr('/ip4/123.123.123.123/tcp/1234') + + await expect(libp2p.isDialable(ma)).to.eventually.be.false() + }) +}) diff --git a/packages/integration-tests/test/events.spec.ts b/packages/integration-tests/test/events.spec.ts new file mode 100644 index 0000000000..c22f55d893 --- /dev/null +++ b/packages/integration-tests/test/events.spec.ts @@ -0,0 +1,88 @@ +import { stop } from '@libp2p/interface' +import { expect } from 'aegir/chai' +import pDefer from 'p-defer' +import { createPeers } from './fixtures/create-peers.js' +import type { Echo } from '@libp2p/echo' +import type { Libp2p } from 'libp2p' + +describe('events', () => { + let dialer: Libp2p<{ echo: Echo }> + let listener: Libp2p<{ echo: Echo }> + + afterEach(async () => { + await stop(dialer, listener) + }) + + it('should emit connection events', async () => { + ({ dialer, listener } = await createPeers()) + + const localConnectionEventReceived = pDefer() + const localConnectionEndEventReceived = pDefer() + const localPeerConnectEventReceived = pDefer() + const localPeerDisconnectEventReceived = pDefer() + const remoteConnectionEventReceived = pDefer() + const remoteConnectionEndEventReceived = pDefer() + const remotePeerConnectEventReceived = pDefer() + const remotePeerDisconnectEventReceived = pDefer() + + dialer.addEventListener('connection:open', (event) => { + expect(event.detail.remotePeer.equals(listener.peerId)).to.be.true() + localConnectionEventReceived.resolve() + }) + dialer.addEventListener('connection:close', (event) => { + expect(event.detail.remotePeer.equals(listener.peerId)).to.be.true() + localConnectionEndEventReceived.resolve() + }) + dialer.addEventListener('peer:connect', (event) => { + expect(event.detail.equals(listener.peerId)).to.be.true() + localPeerConnectEventReceived.resolve() + }) + dialer.addEventListener('peer:disconnect', (event) => { + expect(event.detail.equals(listener.peerId)).to.be.true() + localPeerDisconnectEventReceived.resolve() + }) + + listener.addEventListener('connection:open', (event) => { + expect(event.detail.remotePeer.equals(dialer.peerId)).to.be.true() + remoteConnectionEventReceived.resolve() + }) + listener.addEventListener('connection:close', (event) => { + expect(event.detail.remotePeer.equals(dialer.peerId)).to.be.true() + remoteConnectionEndEventReceived.resolve() + }) + listener.addEventListener('peer:connect', (event) => { + expect(event.detail.equals(dialer.peerId)).to.be.true() + remotePeerConnectEventReceived.resolve() + }) + listener.addEventListener('peer:disconnect', (event) => { + expect(event.detail.equals(dialer.peerId)).to.be.true() + remotePeerDisconnectEventReceived.resolve() + }) + + await dialer.dial(listener.getMultiaddrs()) + + // Verify onConnection is called with the connection + const connections = await Promise.all([ + ...dialer.getConnections(listener.peerId), + ...listener.getConnections(dialer.peerId) + ]) + expect(connections).to.have.lengthOf(2) + + await Promise.all([ + localConnectionEventReceived.promise, + localPeerConnectEventReceived.promise, + remoteConnectionEventReceived.promise, + remotePeerConnectEventReceived.promise + ]) + + // Verify onConnectionEnd is called with the connection + await Promise.all(connections.map(async conn => { await conn.close() })) + + await Promise.all([ + localConnectionEndEventReceived.promise, + localPeerDisconnectEventReceived.promise, + remoteConnectionEndEventReceived.promise, + remotePeerDisconnectEventReceived.promise + ]) + }) +}) diff --git a/packages/integration-tests/test/fixtures/create-peers.ts b/packages/integration-tests/test/fixtures/create-peers.ts new file mode 100644 index 0000000000..5d35c1a22e --- /dev/null +++ b/packages/integration-tests/test/fixtures/create-peers.ts @@ -0,0 +1,50 @@ +/* eslint-env mocha */ + +import { yamux } from '@chainsafe/libp2p-yamux' +import { echo } from '@libp2p/echo' +import { memory } from '@libp2p/memory' +import { plaintext } from '@libp2p/plaintext' +import { createLibp2p } from 'libp2p' +import type { Echo } from '@libp2p/echo' +import type { Libp2p } from '@libp2p/interface' +import type { Libp2pOptions } from 'libp2p' + +async function createNode (config: Partial> = {}): Promise> { + const node = await createLibp2p({ + transports: [ + memory() + ], + connectionEncrypters: [ + plaintext() + ], + streamMuxers: [ + yamux() + ], + ...config, + services: { + ...config.services, + echo: echo() + } + }) + + return node +} + +interface DialerAndListener { + dialer: Libp2p<{ echo: Echo }> + listener: Libp2p<{ echo: Echo }> +} + +export async function createPeers (dialerConfig: Partial> = {}, listenerConfig: Partial> = {}): Promise { + return { + dialer: await createNode(dialerConfig), + listener: await createNode({ + ...listenerConfig, + addresses: { + listen: [ + '/memory/address-1' + ] + } + }) + } +} diff --git a/packages/integration-tests/test/fixtures/slow-muxer.ts b/packages/integration-tests/test/fixtures/slow-muxer.ts new file mode 100644 index 0000000000..c98c59de47 --- /dev/null +++ b/packages/integration-tests/test/fixtures/slow-muxer.ts @@ -0,0 +1,28 @@ +/* eslint-env mocha */ + +import { yamux } from '@chainsafe/libp2p-yamux' +import delay from 'delay' +import map from 'it-map' +import type { StreamMuxerFactory } from '@libp2p/interface' + +/** + * Creates a muxer with a delay between each sent packet + */ +export function slowMuxer (packetDelay: number): ((components: any) => StreamMuxerFactory) { + return (components) => { + const muxerFactory = yamux()(components) + const originalCreateStreamMuxer = muxerFactory.createStreamMuxer.bind(muxerFactory) + + muxerFactory.createStreamMuxer = (init) => { + const muxer = originalCreateStreamMuxer(init) + muxer.source = map(muxer.source, async (buf) => { + await delay(packetDelay) + return buf + }) + + return muxer + } + + return muxerFactory + } +} diff --git a/packages/integration-tests/test/listening.spec.ts b/packages/integration-tests/test/listening.spec.ts new file mode 100644 index 0000000000..0139c62d1c --- /dev/null +++ b/packages/integration-tests/test/listening.spec.ts @@ -0,0 +1,98 @@ +/* eslint-env mocha */ + +import { FaultTolerance, stop } from '@libp2p/interface' +import { memory } from '@libp2p/memory' +import { plaintext } from '@libp2p/plaintext' +import { expect } from 'aegir/chai' +import { createLibp2p } from 'libp2p' +import type { Libp2p } from '@libp2p/interface' + +describe('Listening', () => { + let libp2p: Libp2p + + afterEach(async () => { + await stop(libp2p) + }) + + it('should replace wildcard host and port with actual host and port on startup', async () => { + const listenAddress = '/memory/address-1' + + libp2p = await createLibp2p({ + addresses: { + listen: [ + listenAddress + ] + }, + transports: [ + memory() + ], + connectionEncrypters: [ + plaintext() + ] + }) + + await libp2p.start() + + // @ts-expect-error components field is private + const addrs = libp2p.components.transportManager.getAddrs() + + // Should get something like: + // /memory/address-1 + expect(addrs).to.have.lengthOf(1) + expect(addrs[0].toString()).to.equal(listenAddress) + }) + + it('fails to start if multiaddr fails to listen', async () => { + libp2p = await createLibp2p({ + addresses: { + listen: ['/ip4/127.0.0.1/tcp/0'] + }, + transports: [memory()], + connectionEncrypters: [plaintext()], + start: false + }) + + await expect(libp2p.start()).to.eventually.be.rejected + .with.property('name', 'NoValidAddressesError') + }) + + it('does not fail to start if provided listen multiaddr are not compatible to configured transports (when supporting dial only mode)', async () => { + libp2p = await createLibp2p({ + addresses: { + listen: ['/ip4/127.0.0.1/tcp/0'] + }, + transportManager: { + faultTolerance: FaultTolerance.NO_FATAL + }, + transports: [ + memory() + ], + connectionEncrypters: [ + plaintext() + ], + start: false + }) + + await expect(libp2p.start()).to.eventually.be.undefined() + }) + + it('does not fail to start if provided listen multiaddr fail to listen on configured transports (when supporting dial only mode)', async () => { + libp2p = await createLibp2p({ + addresses: { + listen: ['/ip4/127.0.0.1/tcp/12345/p2p/QmWDn2LY8nannvSWJzruUYoLZ4vV83vfCBwd8DipvdgQc3/p2p-circuit'] + }, + transportManager: { + faultTolerance: FaultTolerance.NO_FATAL + }, + transports: [ + memory() + ], + connectionEncrypters: [ + plaintext() + ], + start: false + }) + + await expect(libp2p.start()).to.eventually.be.undefined() + }) +})