mirror of
https://github.com/fluencelabs/js-libp2p-interfaces
synced 2025-06-12 08:11:27 +00:00
feat: interface pubsub (#60)
* feat: interface pubsub * chore: pubsub router tests * chore: move pubsub abstractions from gossipsub * chore: address review * chore: revamp docs * chore: add emit self tests to interface * chore: refactor base tests * chore: publish should only accept one topic per api call * chore: normalize msg before emit * chore: do not reset inbound stream * chore: apply suggestions from code review Co-authored-by: Jacob Heun <jacobheun@gmail.com> * chore: address review * fix: remove subscribe handler * chore: remove bits from create peerId Co-authored-by: Jacob Heun <jacobheun@gmail.com> * chore: remove delay from topic validators tests * chore: add event emitter information * fix: topic validator docs Co-authored-by: Jacob Heun <jacobheun@gmail.com>
This commit is contained in:
78
test/pubsub/emit-self.spec.js
Normal file
78
test/pubsub/emit-self.spec.js
Normal file
@ -0,0 +1,78 @@
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('aegir/utils/chai')
|
||||
|
||||
const {
|
||||
createPeerId,
|
||||
mockRegistrar,
|
||||
PubsubImplementation
|
||||
} = require('./utils')
|
||||
|
||||
const uint8ArrayFromString = require('uint8arrays/from-string')
|
||||
|
||||
const protocol = '/pubsub/1.0.0'
|
||||
const topic = 'foo'
|
||||
const data = uint8ArrayFromString('bar')
|
||||
const shouldNotHappen = (_) => expect.fail()
|
||||
|
||||
describe('emitSelf', () => {
|
||||
let pubsub
|
||||
|
||||
describe('enabled', () => {
|
||||
before(async () => {
|
||||
const peerId = await createPeerId()
|
||||
|
||||
pubsub = new PubsubImplementation(protocol, {
|
||||
peerId,
|
||||
registrar: mockRegistrar
|
||||
}, { emitSelf: true })
|
||||
})
|
||||
|
||||
before(() => {
|
||||
pubsub.start()
|
||||
pubsub.subscribe(topic)
|
||||
})
|
||||
|
||||
after(() => {
|
||||
pubsub.stop()
|
||||
})
|
||||
|
||||
it('should emit to self on publish', () => {
|
||||
const promise = new Promise((resolve) => pubsub.once(topic, resolve))
|
||||
|
||||
pubsub.publish(topic, data)
|
||||
|
||||
return promise
|
||||
})
|
||||
})
|
||||
|
||||
describe('disabled', () => {
|
||||
before(async () => {
|
||||
const peerId = await createPeerId()
|
||||
|
||||
pubsub = new PubsubImplementation(protocol, {
|
||||
peerId,
|
||||
registrar: mockRegistrar
|
||||
}, { emitSelf: false })
|
||||
})
|
||||
|
||||
before(() => {
|
||||
pubsub.start()
|
||||
pubsub.subscribe(topic)
|
||||
})
|
||||
|
||||
after(() => {
|
||||
pubsub.stop()
|
||||
})
|
||||
|
||||
it('should not emit to self on publish', () => {
|
||||
pubsub.once(topic, (m) => shouldNotHappen)
|
||||
|
||||
pubsub.publish(topic, data)
|
||||
|
||||
// Wait 1 second to guarantee that self is not noticed
|
||||
return new Promise((resolve) => setTimeout(() => resolve(), 1000))
|
||||
})
|
||||
})
|
||||
})
|
54
test/pubsub/instance.spec.js
Normal file
54
test/pubsub/instance.spec.js
Normal file
@ -0,0 +1,54 @@
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('aegir/utils/chai')
|
||||
|
||||
const PubsubBaseImpl = require('../../src/pubsub')
|
||||
const {
|
||||
createPeerId,
|
||||
mockRegistrar
|
||||
} = require('./utils')
|
||||
|
||||
describe('pubsub instance', () => {
|
||||
let peerId
|
||||
|
||||
before(async () => {
|
||||
peerId = await createPeerId()
|
||||
})
|
||||
|
||||
it('should throw if no debugName is provided', () => {
|
||||
expect(() => {
|
||||
new PubsubBaseImpl() // eslint-disable-line no-new
|
||||
}).to.throw()
|
||||
})
|
||||
|
||||
it('should throw if no multicodec is provided', () => {
|
||||
expect(() => {
|
||||
new PubsubBaseImpl({ // eslint-disable-line no-new
|
||||
debugName: 'pubsub'
|
||||
})
|
||||
}).to.throw()
|
||||
})
|
||||
|
||||
it('should throw if no libp2p is provided', () => {
|
||||
expect(() => {
|
||||
new PubsubBaseImpl({ // eslint-disable-line no-new
|
||||
debugName: 'pubsub',
|
||||
multicodecs: '/pubsub/1.0.0'
|
||||
})
|
||||
}).to.throw()
|
||||
})
|
||||
|
||||
it('should accept valid parameters', () => {
|
||||
expect(() => {
|
||||
new PubsubBaseImpl({ // eslint-disable-line no-new
|
||||
debugName: 'pubsub',
|
||||
multicodecs: '/pubsub/1.0.0',
|
||||
libp2p: {
|
||||
peerId: peerId,
|
||||
registrar: mockRegistrar
|
||||
}
|
||||
})
|
||||
}).not.to.throw()
|
||||
})
|
||||
})
|
227
test/pubsub/lifesycle.spec.js
Normal file
227
test/pubsub/lifesycle.spec.js
Normal file
@ -0,0 +1,227 @@
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('aegir/utils/chai')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const PubsubBaseImpl = require('../../src/pubsub')
|
||||
const {
|
||||
createPeerId,
|
||||
createMockRegistrar,
|
||||
PubsubImplementation,
|
||||
ConnectionPair
|
||||
} = require('./utils')
|
||||
|
||||
describe('pubsub base lifecycle', () => {
|
||||
describe('should start and stop properly', () => {
|
||||
let pubsub
|
||||
let sinonMockRegistrar
|
||||
|
||||
beforeEach(async () => {
|
||||
const peerId = await createPeerId()
|
||||
sinonMockRegistrar = {
|
||||
handle: sinon.stub(),
|
||||
register: sinon.stub(),
|
||||
unregister: sinon.stub()
|
||||
}
|
||||
|
||||
pubsub = new PubsubBaseImpl({
|
||||
debugName: 'pubsub',
|
||||
multicodecs: '/pubsub/1.0.0',
|
||||
libp2p: {
|
||||
peerId: peerId,
|
||||
registrar: sinonMockRegistrar
|
||||
}
|
||||
})
|
||||
|
||||
expect(pubsub.peers.size).to.be.eql(0)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('should be able to start and stop', async () => {
|
||||
await pubsub.start()
|
||||
expect(sinonMockRegistrar.handle.calledOnce).to.be.true()
|
||||
expect(sinonMockRegistrar.register.calledOnce).to.be.true()
|
||||
|
||||
await pubsub.stop()
|
||||
expect(sinonMockRegistrar.unregister.calledOnce).to.be.true()
|
||||
})
|
||||
|
||||
it('starting should not throw if already started', async () => {
|
||||
await pubsub.start()
|
||||
await pubsub.start()
|
||||
expect(sinonMockRegistrar.handle.calledOnce).to.be.true()
|
||||
expect(sinonMockRegistrar.register.calledOnce).to.be.true()
|
||||
|
||||
await pubsub.stop()
|
||||
expect(sinonMockRegistrar.unregister.calledOnce).to.be.true()
|
||||
})
|
||||
|
||||
it('stopping should not throw if not started', async () => {
|
||||
await pubsub.stop()
|
||||
expect(sinonMockRegistrar.register.calledOnce).to.be.false()
|
||||
expect(sinonMockRegistrar.unregister.calledOnce).to.be.false()
|
||||
})
|
||||
})
|
||||
|
||||
describe('should be able to register two nodes', () => {
|
||||
const protocol = '/pubsub/1.0.0'
|
||||
let pubsubA, pubsubB
|
||||
let peerIdA, peerIdB
|
||||
const registrarRecordA = {}
|
||||
const registrarRecordB = {}
|
||||
|
||||
// mount pubsub
|
||||
beforeEach(async () => {
|
||||
peerIdA = await createPeerId()
|
||||
peerIdB = await createPeerId()
|
||||
|
||||
pubsubA = new PubsubImplementation(protocol, {
|
||||
peerId: peerIdA,
|
||||
registrar: createMockRegistrar(registrarRecordA)
|
||||
})
|
||||
pubsubB = new PubsubImplementation(protocol, {
|
||||
peerId: peerIdB,
|
||||
registrar: createMockRegistrar(registrarRecordB)
|
||||
})
|
||||
})
|
||||
|
||||
// start pubsub
|
||||
beforeEach(() => {
|
||||
pubsubA.start()
|
||||
pubsubB.start()
|
||||
|
||||
expect(Object.keys(registrarRecordA)).to.have.lengthOf(1)
|
||||
expect(Object.keys(registrarRecordB)).to.have.lengthOf(1)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
|
||||
return Promise.all([
|
||||
pubsubA.stop(),
|
||||
pubsubB.stop()
|
||||
])
|
||||
})
|
||||
|
||||
it('should handle onConnect as expected', async () => {
|
||||
const onConnectA = registrarRecordA[protocol].onConnect
|
||||
const handlerB = registrarRecordB[protocol].handler
|
||||
|
||||
// Notice peers of connection
|
||||
const [c0, c1] = ConnectionPair()
|
||||
|
||||
await onConnectA(peerIdB, c0)
|
||||
await handlerB({
|
||||
protocol,
|
||||
stream: c1.stream,
|
||||
connection: {
|
||||
remotePeer: peerIdA
|
||||
}
|
||||
})
|
||||
|
||||
expect(pubsubA.peers.size).to.be.eql(1)
|
||||
expect(pubsubB.peers.size).to.be.eql(1)
|
||||
})
|
||||
|
||||
it('should use the latest connection if onConnect is called more than once', async () => {
|
||||
const onConnectA = registrarRecordA[protocol].onConnect
|
||||
const handlerB = registrarRecordB[protocol].handler
|
||||
|
||||
// Notice peers of connection
|
||||
const [c0, c1] = ConnectionPair()
|
||||
const [c2] = ConnectionPair()
|
||||
|
||||
sinon.spy(c0, 'newStream')
|
||||
|
||||
await onConnectA(peerIdB, c0)
|
||||
await handlerB({
|
||||
protocol,
|
||||
stream: c1.stream,
|
||||
connection: {
|
||||
remotePeer: peerIdA
|
||||
}
|
||||
})
|
||||
expect(c0.newStream).to.have.property('callCount', 1)
|
||||
|
||||
sinon.spy(pubsubA, '_removePeer')
|
||||
|
||||
sinon.spy(c2, 'newStream')
|
||||
|
||||
await onConnectA(peerIdB, c2)
|
||||
expect(c2.newStream).to.have.property('callCount', 1)
|
||||
expect(pubsubA._removePeer).to.have.property('callCount', 0)
|
||||
|
||||
// Verify the first stream was closed
|
||||
const { stream: firstStream } = await c0.newStream.returnValues[0]
|
||||
try {
|
||||
await firstStream.sink(['test'])
|
||||
} catch (err) {
|
||||
expect(err).to.exist()
|
||||
return
|
||||
}
|
||||
expect.fail('original stream should have ended')
|
||||
})
|
||||
|
||||
it('should handle newStream errors in onConnect', async () => {
|
||||
const onConnectA = registrarRecordA[protocol].onConnect
|
||||
const handlerB = registrarRecordB[protocol].handler
|
||||
|
||||
// Notice peers of connection
|
||||
const [c0, c1] = ConnectionPair()
|
||||
const error = new Error('new stream error')
|
||||
sinon.stub(c0, 'newStream').throws(error)
|
||||
|
||||
await onConnectA(peerIdB, c0)
|
||||
await handlerB({
|
||||
protocol,
|
||||
stream: c1.stream,
|
||||
connection: {
|
||||
remotePeer: peerIdA
|
||||
}
|
||||
})
|
||||
|
||||
expect(c0.newStream).to.have.property('callCount', 1)
|
||||
})
|
||||
|
||||
it('should handle onDisconnect as expected', async () => {
|
||||
const onConnectA = registrarRecordA[protocol].onConnect
|
||||
const onDisconnectA = registrarRecordA[protocol].onDisconnect
|
||||
const handlerB = registrarRecordB[protocol].handler
|
||||
const onDisconnectB = registrarRecordB[protocol].onDisconnect
|
||||
|
||||
// Notice peers of connection
|
||||
const [c0, c1] = ConnectionPair()
|
||||
|
||||
await onConnectA(peerIdB, c0)
|
||||
await handlerB({
|
||||
protocol,
|
||||
stream: c1.stream,
|
||||
connection: {
|
||||
remotePeer: peerIdA
|
||||
}
|
||||
})
|
||||
|
||||
// Notice peers of disconnect
|
||||
onDisconnectA(peerIdB)
|
||||
onDisconnectB(peerIdA)
|
||||
|
||||
expect(pubsubA.peers.size).to.be.eql(0)
|
||||
expect(pubsubB.peers.size).to.be.eql(0)
|
||||
})
|
||||
|
||||
it('should handle onDisconnect for unknown peers', () => {
|
||||
const onDisconnectA = registrarRecordA[protocol].onDisconnect
|
||||
|
||||
expect(pubsubA.peers.size).to.be.eql(0)
|
||||
|
||||
// Notice peers of disconnect
|
||||
onDisconnectA(peerIdB)
|
||||
|
||||
expect(pubsubA.peers.size).to.be.eql(0)
|
||||
})
|
||||
})
|
||||
})
|
73
test/pubsub/message.spec.js
Normal file
73
test/pubsub/message.spec.js
Normal file
@ -0,0 +1,73 @@
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('aegir/utils/chai')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const PubsubBaseImpl = require('../../src/pubsub')
|
||||
const { randomSeqno } = require('../../src/pubsub/utils')
|
||||
const {
|
||||
createPeerId,
|
||||
mockRegistrar
|
||||
} = require('./utils')
|
||||
|
||||
describe('pubsub base messages', () => {
|
||||
let peerId
|
||||
let pubsub
|
||||
|
||||
before(async () => {
|
||||
peerId = await createPeerId()
|
||||
pubsub = new PubsubBaseImpl({
|
||||
debugName: 'pubsub',
|
||||
multicodecs: '/pubsub/1.0.0',
|
||||
libp2p: {
|
||||
peerId: peerId,
|
||||
registrar: mockRegistrar
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('_buildMessage normalizes and signs messages', async () => {
|
||||
const message = {
|
||||
receivedFrom: peerId.id,
|
||||
from: peerId.id,
|
||||
data: 'hello',
|
||||
seqno: randomSeqno(),
|
||||
topicIDs: ['test-topic']
|
||||
}
|
||||
|
||||
const signedMessage = await pubsub._buildMessage(message)
|
||||
expect(pubsub.validate(signedMessage)).to.not.be.rejected()
|
||||
})
|
||||
|
||||
it('validate with strict signing off will validate a present signature', async () => {
|
||||
const message = {
|
||||
receivedFrom: peerId.id,
|
||||
from: peerId.id,
|
||||
data: 'hello',
|
||||
seqno: randomSeqno(),
|
||||
topicIDs: ['test-topic']
|
||||
}
|
||||
|
||||
sinon.stub(pubsub, 'strictSigning').value(false)
|
||||
|
||||
const signedMessage = await pubsub._buildMessage(message)
|
||||
expect(pubsub.validate(signedMessage)).to.not.be.rejected()
|
||||
})
|
||||
|
||||
it('validate with strict signing requires a signature', async () => {
|
||||
const message = {
|
||||
receivedFrom: peerId.id,
|
||||
from: peerId.id,
|
||||
data: 'hello',
|
||||
seqno: randomSeqno(),
|
||||
topicIDs: ['test-topic']
|
||||
}
|
||||
|
||||
await expect(pubsub.validate(message)).to.be.rejectedWith(Error, 'Signing required and no signature was present')
|
||||
})
|
||||
})
|
358
test/pubsub/pubsub.spec.js
Normal file
358
test/pubsub/pubsub.spec.js
Normal file
@ -0,0 +1,358 @@
|
||||
/* eslint-env mocha */
|
||||
/* eslint max-nested-callbacks: ["error", 6] */
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('aegir/utils/chai')
|
||||
const sinon = require('sinon')
|
||||
const pWaitFor = require('p-wait-for')
|
||||
|
||||
const uint8ArrayFromString = require('uint8arrays/from-string')
|
||||
|
||||
const PeerStreams = require('../../src/pubsub/peer-streams')
|
||||
const {
|
||||
createPeerId,
|
||||
createMockRegistrar,
|
||||
ConnectionPair,
|
||||
mockRegistrar,
|
||||
PubsubImplementation
|
||||
} = require('./utils')
|
||||
|
||||
const protocol = '/pubsub/1.0.0'
|
||||
const topic = 'test-topic'
|
||||
const message = uint8ArrayFromString('hello')
|
||||
|
||||
describe('pubsub base implementation', () => {
|
||||
describe('publish', () => {
|
||||
let pubsub
|
||||
|
||||
beforeEach(async () => {
|
||||
const peerId = await createPeerId()
|
||||
pubsub = new PubsubImplementation(protocol, {
|
||||
peerId: peerId,
|
||||
registrar: mockRegistrar
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => pubsub.stop())
|
||||
|
||||
it('calls _publish for router to forward messages', async () => {
|
||||
sinon.spy(pubsub, '_publish')
|
||||
|
||||
pubsub.start()
|
||||
await pubsub.publish(topic, message)
|
||||
|
||||
expect(pubsub._publish.callCount).to.eql(1)
|
||||
})
|
||||
|
||||
it('should sign messages on publish', async () => {
|
||||
sinon.spy(pubsub, '_publish')
|
||||
|
||||
pubsub.start()
|
||||
await pubsub.publish(topic, message)
|
||||
|
||||
// Get the first message sent to _publish, and validate it
|
||||
const signedMessage = pubsub._publish.getCall(0).lastArg
|
||||
try {
|
||||
await pubsub.validate(signedMessage)
|
||||
} catch (e) {
|
||||
expect.fail('validation should not throw')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('subscribe', () => {
|
||||
describe('basics', () => {
|
||||
let pubsub
|
||||
|
||||
beforeEach(async () => {
|
||||
const peerId = await createPeerId()
|
||||
pubsub = new PubsubImplementation(protocol, {
|
||||
peerId: peerId,
|
||||
registrar: mockRegistrar
|
||||
})
|
||||
pubsub.start()
|
||||
})
|
||||
|
||||
afterEach(() => pubsub.stop())
|
||||
|
||||
it('should add subscription', () => {
|
||||
pubsub.subscribe(topic)
|
||||
|
||||
expect(pubsub.subscriptions.size).to.eql(1)
|
||||
expect(pubsub.subscriptions.has(topic)).to.be.true()
|
||||
})
|
||||
})
|
||||
|
||||
describe('two nodes', () => {
|
||||
let pubsubA, pubsubB
|
||||
let peerIdA, peerIdB
|
||||
const registrarRecordA = {}
|
||||
const registrarRecordB = {}
|
||||
|
||||
beforeEach(async () => {
|
||||
peerIdA = await createPeerId()
|
||||
peerIdB = await createPeerId()
|
||||
|
||||
pubsubA = new PubsubImplementation(protocol, {
|
||||
peerId: peerIdA,
|
||||
registrar: createMockRegistrar(registrarRecordA)
|
||||
})
|
||||
pubsubB = new PubsubImplementation(protocol, {
|
||||
peerId: peerIdB,
|
||||
registrar: createMockRegistrar(registrarRecordB)
|
||||
})
|
||||
})
|
||||
|
||||
// start pubsub and connect nodes
|
||||
beforeEach(async () => {
|
||||
pubsubA.start()
|
||||
pubsubB.start()
|
||||
|
||||
const onConnectA = registrarRecordA[protocol].onConnect
|
||||
const handlerB = registrarRecordB[protocol].handler
|
||||
|
||||
// Notice peers of connection
|
||||
const [c0, c1] = ConnectionPair()
|
||||
|
||||
await onConnectA(peerIdB, c0)
|
||||
await handlerB({
|
||||
protocol,
|
||||
stream: c1.stream,
|
||||
connection: {
|
||||
remotePeer: peerIdA
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
pubsubA.stop()
|
||||
pubsubB.stop()
|
||||
})
|
||||
|
||||
it('should send subscribe message to connected peers', async () => {
|
||||
sinon.spy(pubsubA, '_sendSubscriptions')
|
||||
sinon.spy(pubsubB, '_processRpcSubOpt')
|
||||
|
||||
pubsubA.subscribe(topic)
|
||||
|
||||
// Should send subscriptions to a peer
|
||||
expect(pubsubA._sendSubscriptions.callCount).to.eql(1)
|
||||
|
||||
// Other peer should receive subscription message
|
||||
await pWaitFor(() => {
|
||||
const subscribers = pubsubB.getSubscribers(topic)
|
||||
|
||||
return subscribers.length === 1
|
||||
})
|
||||
expect(pubsubB._processRpcSubOpt.callCount).to.eql(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('unsubscribe', () => {
|
||||
describe('basics', () => {
|
||||
let pubsub
|
||||
|
||||
beforeEach(async () => {
|
||||
const peerId = await createPeerId()
|
||||
pubsub = new PubsubImplementation(protocol, {
|
||||
peerId: peerId,
|
||||
registrar: mockRegistrar
|
||||
})
|
||||
pubsub.start()
|
||||
})
|
||||
|
||||
afterEach(() => pubsub.stop())
|
||||
|
||||
it('should remove all subscriptions for a topic', () => {
|
||||
pubsub.subscribe(topic, (msg) => {})
|
||||
pubsub.subscribe(topic, (msg) => {})
|
||||
|
||||
expect(pubsub.subscriptions.size).to.eql(1)
|
||||
|
||||
pubsub.unsubscribe(topic)
|
||||
|
||||
expect(pubsub.subscriptions.size).to.eql(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('two nodes', () => {
|
||||
let pubsubA, pubsubB
|
||||
let peerIdA, peerIdB
|
||||
const registrarRecordA = {}
|
||||
const registrarRecordB = {}
|
||||
|
||||
beforeEach(async () => {
|
||||
peerIdA = await createPeerId()
|
||||
peerIdB = await createPeerId()
|
||||
|
||||
pubsubA = new PubsubImplementation(protocol, {
|
||||
peerId: peerIdA,
|
||||
registrar: createMockRegistrar(registrarRecordA)
|
||||
})
|
||||
pubsubB = new PubsubImplementation(protocol, {
|
||||
peerId: peerIdB,
|
||||
registrar: createMockRegistrar(registrarRecordB)
|
||||
})
|
||||
})
|
||||
|
||||
// start pubsub and connect nodes
|
||||
beforeEach(async () => {
|
||||
pubsubA.start()
|
||||
pubsubB.start()
|
||||
|
||||
const onConnectA = registrarRecordA[protocol].onConnect
|
||||
const handlerB = registrarRecordB[protocol].handler
|
||||
|
||||
// Notice peers of connection
|
||||
const [c0, c1] = ConnectionPair()
|
||||
|
||||
await onConnectA(peerIdB, c0)
|
||||
await handlerB({
|
||||
protocol,
|
||||
stream: c1.stream,
|
||||
connection: {
|
||||
remotePeer: peerIdA
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
pubsubA.stop()
|
||||
pubsubB.stop()
|
||||
})
|
||||
|
||||
it('should send unsubscribe message to connected peers', async () => {
|
||||
sinon.spy(pubsubA, '_sendSubscriptions')
|
||||
sinon.spy(pubsubB, '_processRpcSubOpt')
|
||||
|
||||
pubsubA.subscribe(topic)
|
||||
// Should send subscriptions to a peer
|
||||
expect(pubsubA._sendSubscriptions.callCount).to.eql(1)
|
||||
|
||||
// Other peer should receive subscription message
|
||||
await pWaitFor(() => {
|
||||
const subscribers = pubsubB.getSubscribers(topic)
|
||||
|
||||
return subscribers.length === 1
|
||||
})
|
||||
expect(pubsubB._processRpcSubOpt.callCount).to.eql(1)
|
||||
|
||||
// Unsubscribe
|
||||
pubsubA.unsubscribe(topic)
|
||||
// Should send subscriptions to a peer
|
||||
expect(pubsubA._sendSubscriptions.callCount).to.eql(2)
|
||||
|
||||
// Other peer should receive subscription message
|
||||
await pWaitFor(() => {
|
||||
const subscribers = pubsubB.getSubscribers(topic)
|
||||
|
||||
return subscribers.length === 0
|
||||
})
|
||||
expect(pubsubB._processRpcSubOpt.callCount).to.eql(2)
|
||||
})
|
||||
|
||||
it('should not send unsubscribe message to connected peers if not subscribed', () => {
|
||||
sinon.spy(pubsubA, '_sendSubscriptions')
|
||||
sinon.spy(pubsubB, '_processRpcSubOpt')
|
||||
|
||||
// Unsubscribe
|
||||
pubsubA.unsubscribe(topic)
|
||||
|
||||
// Should send subscriptions to a peer
|
||||
expect(pubsubA._sendSubscriptions.callCount).to.eql(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getTopics', () => {
|
||||
let peerId
|
||||
let pubsub
|
||||
|
||||
beforeEach(async () => {
|
||||
peerId = await createPeerId()
|
||||
pubsub = new PubsubImplementation(protocol, {
|
||||
peerId: peerId,
|
||||
registrar: mockRegistrar
|
||||
})
|
||||
pubsub.start()
|
||||
})
|
||||
|
||||
afterEach(() => pubsub.stop())
|
||||
|
||||
it('returns the subscribed topics', () => {
|
||||
let subsTopics = pubsub.getTopics()
|
||||
expect(subsTopics).to.have.lengthOf(0)
|
||||
|
||||
pubsub.subscribe(topic)
|
||||
|
||||
subsTopics = pubsub.getTopics()
|
||||
expect(subsTopics).to.have.lengthOf(1)
|
||||
expect(subsTopics[0]).to.eql(topic)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSubscribers', () => {
|
||||
let peerId
|
||||
let pubsub
|
||||
|
||||
beforeEach(async () => {
|
||||
peerId = await createPeerId()
|
||||
pubsub = new PubsubImplementation(protocol, {
|
||||
peerId: peerId,
|
||||
registrar: mockRegistrar
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => pubsub.stop())
|
||||
|
||||
it('should fail if pubsub is not started', () => {
|
||||
const topic = 'topic-test'
|
||||
|
||||
try {
|
||||
pubsub.getSubscribers(topic)
|
||||
} catch (err) {
|
||||
expect(err).to.exist()
|
||||
expect(err.code).to.eql('ERR_NOT_STARTED_YET')
|
||||
return
|
||||
}
|
||||
throw new Error('should fail if pubsub is not started')
|
||||
})
|
||||
|
||||
it('should fail if no topic is provided', () => {
|
||||
// start pubsub
|
||||
pubsub.start()
|
||||
|
||||
try {
|
||||
pubsub.getSubscribers()
|
||||
} catch (err) {
|
||||
expect(err).to.exist()
|
||||
expect(err.code).to.eql('ERR_NOT_VALID_TOPIC')
|
||||
return
|
||||
}
|
||||
throw new Error('should fail if no topic is provided')
|
||||
})
|
||||
|
||||
it('should get peer subscribed to one topic', () => {
|
||||
const topic = 'topic-test'
|
||||
|
||||
// start pubsub
|
||||
pubsub.start()
|
||||
|
||||
let peersSubscribed = pubsub.getSubscribers(topic)
|
||||
expect(peersSubscribed).to.be.empty()
|
||||
|
||||
// Set mock peer subscribed
|
||||
const peer = new PeerStreams({ id: peerId })
|
||||
const id = peer.id.toB58String()
|
||||
|
||||
pubsub.topics.set(topic, new Set([id]))
|
||||
pubsub.peers.set(id, peer)
|
||||
|
||||
peersSubscribed = pubsub.getSubscribers(topic)
|
||||
|
||||
expect(peersSubscribed).to.not.be.empty()
|
||||
expect(peersSubscribed[0]).to.eql(id)
|
||||
})
|
||||
})
|
||||
})
|
93
test/pubsub/sign.spec.js
Normal file
93
test/pubsub/sign.spec.js
Normal file
@ -0,0 +1,93 @@
|
||||
/* eslint-env mocha */
|
||||
/* eslint max-nested-callbacks: ["error", 5] */
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('aegir/utils/chai')
|
||||
const uint8ArrayConcat = require('uint8arrays/concat')
|
||||
const uint8ArrayFromString = require('uint8arrays/from-string')
|
||||
|
||||
const { Message } = require('../../src/pubsub/message')
|
||||
const {
|
||||
signMessage,
|
||||
SignPrefix,
|
||||
verifySignature
|
||||
} = require('../../src/pubsub/message/sign')
|
||||
const PeerId = require('peer-id')
|
||||
const { randomSeqno } = require('../../src/pubsub/utils')
|
||||
|
||||
describe('message signing', () => {
|
||||
let peerId
|
||||
before(async () => {
|
||||
peerId = await PeerId.create({
|
||||
bits: 1024
|
||||
})
|
||||
})
|
||||
|
||||
it('should be able to sign and verify a message', async () => {
|
||||
const message = {
|
||||
from: peerId.id,
|
||||
data: uint8ArrayFromString('hello'),
|
||||
seqno: randomSeqno(),
|
||||
topicIDs: ['test-topic']
|
||||
}
|
||||
|
||||
const bytesToSign = uint8ArrayConcat([SignPrefix, Message.encode(message)])
|
||||
const expectedSignature = await peerId.privKey.sign(bytesToSign)
|
||||
|
||||
const signedMessage = await signMessage(peerId, message)
|
||||
|
||||
// Check the signature and public key
|
||||
expect(signedMessage.signature).to.eql(expectedSignature)
|
||||
expect(signedMessage.key).to.eql(peerId.pubKey.bytes)
|
||||
|
||||
// Verify the signature
|
||||
const verified = await verifySignature(signedMessage)
|
||||
expect(verified).to.eql(true)
|
||||
})
|
||||
|
||||
it('should be able to extract the public key from an inlined key', async () => {
|
||||
const secPeerId = await PeerId.create({ keyType: 'secp256k1' })
|
||||
|
||||
const message = {
|
||||
from: secPeerId.id,
|
||||
data: uint8ArrayFromString('hello'),
|
||||
seqno: randomSeqno(),
|
||||
topicIDs: ['test-topic']
|
||||
}
|
||||
|
||||
const bytesToSign = uint8ArrayConcat([SignPrefix, Message.encode(message)])
|
||||
const expectedSignature = await secPeerId.privKey.sign(bytesToSign)
|
||||
|
||||
const signedMessage = await signMessage(secPeerId, message)
|
||||
|
||||
// Check the signature and public key
|
||||
expect(signedMessage.signature).to.eql(expectedSignature)
|
||||
signedMessage.key = undefined
|
||||
|
||||
// Verify the signature
|
||||
const verified = await verifySignature(signedMessage)
|
||||
expect(verified).to.eql(true)
|
||||
})
|
||||
|
||||
it('should be able to extract the public key from the message', async () => {
|
||||
const message = {
|
||||
from: peerId.id,
|
||||
data: uint8ArrayFromString('hello'),
|
||||
seqno: randomSeqno(),
|
||||
topicIDs: ['test-topic']
|
||||
}
|
||||
|
||||
const bytesToSign = uint8ArrayConcat([SignPrefix, Message.encode(message)])
|
||||
const expectedSignature = await peerId.privKey.sign(bytesToSign)
|
||||
|
||||
const signedMessage = await signMessage(peerId, message)
|
||||
|
||||
// Check the signature and public key
|
||||
expect(signedMessage.signature).to.eql(expectedSignature)
|
||||
expect(signedMessage.key).to.eql(peerId.pubKey.bytes)
|
||||
|
||||
// Verify the signature
|
||||
const verified = await verifySignature(signedMessage)
|
||||
expect(verified).to.eql(true)
|
||||
})
|
||||
})
|
110
test/pubsub/topic-validators.spec.js
Normal file
110
test/pubsub/topic-validators.spec.js
Normal file
@ -0,0 +1,110 @@
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('aegir/utils/chai')
|
||||
const sinon = require('sinon')
|
||||
const pWaitFor = require('p-wait-for')
|
||||
const errCode = require('err-code')
|
||||
|
||||
const PeerId = require('peer-id')
|
||||
const uint8ArrayEquals = require('uint8arrays/equals')
|
||||
const uint8ArrayFromString = require('uint8arrays/from-string')
|
||||
|
||||
const { utils } = require('../../src/pubsub')
|
||||
const PeerStreams = require('../../src/pubsub/peer-streams')
|
||||
|
||||
const {
|
||||
createPeerId,
|
||||
mockRegistrar,
|
||||
PubsubImplementation
|
||||
} = require('./utils')
|
||||
|
||||
const protocol = '/pubsub/1.0.0'
|
||||
|
||||
describe('topic validators', () => {
|
||||
let pubsub
|
||||
|
||||
beforeEach(async () => {
|
||||
const peerId = await createPeerId()
|
||||
|
||||
pubsub = new PubsubImplementation(protocol, {
|
||||
peerId: peerId,
|
||||
registrar: mockRegistrar
|
||||
})
|
||||
|
||||
pubsub.start()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('should filter messages by topic validator', async () => {
|
||||
// use _publish.callCount() to see if a message is valid or not
|
||||
sinon.spy(pubsub, '_publish')
|
||||
// Disable strict signing
|
||||
sinon.stub(pubsub, 'strictSigning').value(false)
|
||||
sinon.stub(pubsub.peers, 'get').returns({})
|
||||
const filteredTopic = 't'
|
||||
const peer = new PeerStreams({ id: await PeerId.create() })
|
||||
|
||||
// Set a trivial topic validator
|
||||
pubsub.topicValidators.set(filteredTopic, (topic, message) => {
|
||||
if (!uint8ArrayEquals(message.data, uint8ArrayFromString('a message'))) {
|
||||
throw errCode(new Error(), 'ERR_TOPIC_VALIDATOR_REJECT')
|
||||
}
|
||||
})
|
||||
|
||||
// valid case
|
||||
const validRpc = {
|
||||
subscriptions: [],
|
||||
msgs: [{
|
||||
from: peer.id.toBytes(),
|
||||
data: uint8ArrayFromString('a message'),
|
||||
seqno: utils.randomSeqno(),
|
||||
topicIDs: [filteredTopic]
|
||||
}]
|
||||
}
|
||||
|
||||
// process valid message
|
||||
pubsub.subscribe(filteredTopic)
|
||||
pubsub._processRpc(peer.id.toB58String(), peer, validRpc)
|
||||
|
||||
await pWaitFor(() => pubsub._publish.callCount === 1)
|
||||
|
||||
// invalid case
|
||||
const invalidRpc = {
|
||||
subscriptions: [],
|
||||
msgs: [{
|
||||
from: peer.id.toBytes(),
|
||||
data: uint8ArrayFromString('a different message'),
|
||||
seqno: utils.randomSeqno(),
|
||||
topicIDs: [filteredTopic]
|
||||
}]
|
||||
}
|
||||
|
||||
// process invalid message
|
||||
pubsub._processRpc(peer.id.toB58String(), peer, invalidRpc)
|
||||
expect(pubsub._publish.callCount).to.eql(1)
|
||||
|
||||
// remove topic validator
|
||||
pubsub.topicValidators.delete(filteredTopic)
|
||||
|
||||
// another invalid case
|
||||
const invalidRpc2 = {
|
||||
subscriptions: [],
|
||||
msgs: [{
|
||||
from: peer.id.toB58String(),
|
||||
data: uint8ArrayFromString('a different message'),
|
||||
seqno: utils.randomSeqno(),
|
||||
topicIDs: [filteredTopic]
|
||||
}]
|
||||
}
|
||||
|
||||
// process previously invalid message, now is valid
|
||||
pubsub._processRpc(peer.id.toB58String(), peer, invalidRpc2)
|
||||
pubsub.unsubscribe(filteredTopic)
|
||||
|
||||
await pWaitFor(() => pubsub._publish.callCount === 2)
|
||||
})
|
||||
})
|
82
test/pubsub/utils.spec.js
Normal file
82
test/pubsub/utils.spec.js
Normal file
@ -0,0 +1,82 @@
|
||||
/* eslint-env mocha */
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('aegir/utils/chai')
|
||||
const utils = require('../../src/pubsub/utils')
|
||||
const uint8ArrayFromString = require('uint8arrays/from-string')
|
||||
|
||||
describe('utils', () => {
|
||||
it('randomSeqno', () => {
|
||||
const first = utils.randomSeqno()
|
||||
const second = utils.randomSeqno()
|
||||
|
||||
expect(first).to.have.length(8)
|
||||
expect(second).to.have.length(8)
|
||||
expect(first).to.not.eql(second)
|
||||
})
|
||||
|
||||
it('msgId', () => {
|
||||
expect(utils.msgId('hello', uint8ArrayFromString('world'))).to.be.eql('hello776f726c64')
|
||||
})
|
||||
|
||||
it('msgId should not generate same ID for two different Uint8Arrays', () => {
|
||||
const peerId = 'QmPNdSYk5Rfpo5euNqwtyizzmKXMNHdXeLjTQhcN4yfX22'
|
||||
const msgId0 = utils.msgId(peerId, uint8ArrayFromString('15603533e990dfde', 'base16'))
|
||||
const msgId1 = utils.msgId(peerId, uint8ArrayFromString('15603533e990dfe0', 'base16'))
|
||||
expect(msgId0).to.not.eql(msgId1)
|
||||
})
|
||||
|
||||
it('anyMatch', () => {
|
||||
[
|
||||
[[1, 2, 3], [4, 5, 6], false],
|
||||
[[1, 2], [1, 2], true],
|
||||
[[1, 2, 3], [4, 5, 1], true],
|
||||
[[5, 6, 1], [1, 2, 3], true],
|
||||
[[], [], false],
|
||||
[[1], [2], false]
|
||||
].forEach((test) => {
|
||||
expect(utils.anyMatch(new Set(test[0]), new Set(test[1])))
|
||||
.to.eql(test[2])
|
||||
|
||||
expect(utils.anyMatch(new Set(test[0]), test[1]))
|
||||
.to.eql(test[2])
|
||||
})
|
||||
})
|
||||
|
||||
it('ensureArray', () => {
|
||||
expect(utils.ensureArray('hello')).to.be.eql(['hello'])
|
||||
expect(utils.ensureArray([1, 2])).to.be.eql([1, 2])
|
||||
})
|
||||
|
||||
it('converts an IN msg.from to b58', () => {
|
||||
const binaryId = uint8ArrayFromString('1220e2187eb3e6c4fb3e7ff9ad4658610624a6315e0240fc6f37130eedb661e939cc', 'base16')
|
||||
const stringId = 'QmdZEWgtaWAxBh93fELFT298La1rsZfhiC2pqwMVwy3jZM'
|
||||
const m = [
|
||||
{ from: binaryId },
|
||||
{ from: stringId }
|
||||
]
|
||||
const expected = [
|
||||
{ from: stringId },
|
||||
{ from: stringId }
|
||||
]
|
||||
for (let i = 0; i < m.length; i++) {
|
||||
expect(utils.normalizeInRpcMessage(m[i])).to.deep.eql(expected[i])
|
||||
}
|
||||
})
|
||||
|
||||
it('converts an OUT msg.from to binary', () => {
|
||||
const binaryId = uint8ArrayFromString('1220e2187eb3e6c4fb3e7ff9ad4658610624a6315e0240fc6f37130eedb661e939cc', 'base16')
|
||||
const stringId = 'QmdZEWgtaWAxBh93fELFT298La1rsZfhiC2pqwMVwy3jZM'
|
||||
const m = [
|
||||
{ from: binaryId },
|
||||
{ from: stringId }
|
||||
]
|
||||
const expected = [
|
||||
{ from: binaryId },
|
||||
{ from: binaryId }
|
||||
]
|
||||
for (let i = 0; i < m.length; i++) {
|
||||
expect(utils.normalizeOutRpcMessage(m[i])).to.deep.eql(expected[i])
|
||||
}
|
||||
})
|
||||
})
|
85
test/pubsub/utils/index.js
Normal file
85
test/pubsub/utils/index.js
Normal file
@ -0,0 +1,85 @@
|
||||
'use strict'
|
||||
|
||||
const DuplexPair = require('it-pair/duplex')
|
||||
|
||||
const PeerId = require('peer-id')
|
||||
|
||||
const PubsubBaseProtocol = require('../../../src/pubsub')
|
||||
const { message } = require('../../../src/pubsub')
|
||||
|
||||
exports.createPeerId = async () => {
|
||||
const peerId = await PeerId.create({ bits: 1024 })
|
||||
|
||||
return peerId
|
||||
}
|
||||
|
||||
class PubsubImplementation extends PubsubBaseProtocol {
|
||||
constructor (protocol, libp2p, options = {}) {
|
||||
super({
|
||||
debugName: 'libp2p:pubsub',
|
||||
multicodecs: protocol,
|
||||
libp2p,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
_publish (message) {
|
||||
// ...
|
||||
}
|
||||
|
||||
_decodeRpc (bytes) {
|
||||
return message.rpc.RPC.decode(bytes)
|
||||
}
|
||||
|
||||
_encodeRpc (rpc) {
|
||||
return message.rpc.RPC.encode(rpc)
|
||||
}
|
||||
}
|
||||
|
||||
exports.PubsubImplementation = PubsubImplementation
|
||||
|
||||
exports.mockRegistrar = {
|
||||
handle: () => {},
|
||||
register: () => {},
|
||||
unregister: () => {}
|
||||
}
|
||||
|
||||
exports.createMockRegistrar = (registrarRecord) => ({
|
||||
handle: (multicodecs, handler) => {
|
||||
const rec = registrarRecord[multicodecs[0]] || {}
|
||||
|
||||
registrarRecord[multicodecs[0]] = {
|
||||
...rec,
|
||||
handler
|
||||
}
|
||||
},
|
||||
register: ({ multicodecs, _onConnect, _onDisconnect }) => {
|
||||
const rec = registrarRecord[multicodecs[0]] || {}
|
||||
|
||||
registrarRecord[multicodecs[0]] = {
|
||||
...rec,
|
||||
onConnect: _onConnect,
|
||||
onDisconnect: _onDisconnect
|
||||
}
|
||||
|
||||
return multicodecs[0]
|
||||
},
|
||||
unregister: (id) => {
|
||||
delete registrarRecord[id]
|
||||
}
|
||||
})
|
||||
|
||||
exports.ConnectionPair = () => {
|
||||
const [d0, d1] = DuplexPair()
|
||||
|
||||
return [
|
||||
{
|
||||
stream: d0,
|
||||
newStream: () => Promise.resolve({ stream: d0 })
|
||||
},
|
||||
{
|
||||
stream: d1,
|
||||
newStream: () => Promise.resolve({ stream: d1 })
|
||||
}
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user