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:
Vasco Santos
2020-08-25 13:05:58 +02:00
committed by GitHub
parent c4be5eed4b
commit ba15a48dd9
26 changed files with 3377 additions and 0 deletions

View 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))
})
})
})

View 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()
})
})

View 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)
})
})
})

View 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
View 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
View 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)
})
})

View 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
View 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])
}
})
})

View 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 })
}
]
}