首次提交

This commit is contained in:
2026-07-13 12:06:16 +08:00
commit e3c810b1a6
1690 changed files with 228324 additions and 0 deletions
+1177
View File
File diff suppressed because it is too large Load Diff
+865
View File
@@ -0,0 +1,865 @@
'use strict'
const { test } = require('tap')
const eos = require('end-of-stream')
const { setup, connect, subscribe, subscribeMultiple, noError } = require('./helper')
const aedes = require('../')
const proxyquire = require('proxyquire')
test('test aedes.createBroker', function (t) {
t.plan(1)
const broker = aedes.createBroker()
t.teardown(broker.close.bind(broker))
connect(setup(broker), {}, function () {
t.pass('connected')
})
})
test('publish QoS 0', function (t) {
t.plan(2)
const s = connect(setup(), { clientId: 'my-client-xyz-5' })
t.teardown(s.broker.close.bind(s.broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
retain: false,
dup: false,
clientId: 'my-client-xyz-5'
}
s.broker.mq.on('hello', function (packet, cb) {
expected.brokerId = s.broker.id
expected.brokerCounter = s.broker.counter
t.equal(packet.messageId, undefined, 'MUST not contain a packet identifier in QoS 0')
t.same(packet, expected, 'packet matches')
cb()
})
s.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world'
})
})
test('messageId shoud reset to 1 if it reached 65535', function (t) {
t.plan(7)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
const publishPacket = {
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
}
let count = 0
s.broker.on('clientReady', function (client) {
subscribe(t, s, 'hello', 1, function () {
client._nextId = 65535
s.outStream.on('data', function (packet) {
if (packet.cmd === 'puback') {
t.equal(packet.messageId, 42)
}
if (packet.cmd === 'publish') {
t.equal(packet.messageId, count++ === 0 ? 65535 : 1)
}
})
s.inStream.write(publishPacket)
s.inStream.write(publishPacket)
})
})
})
test('publish empty topic throws error', function (t) {
t.plan(1)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
s.inStream.write({
cmd: 'publish',
topic: '',
payload: 'world'
})
s.broker.on('clientError', function (client, err) {
t.pass('should emit error')
})
})
test('publish to $SYS topic throws error', function (t) {
t.plan(1)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
s.inStream.write({
cmd: 'publish',
topic: '$SYS/not/allowed',
payload: 'world'
})
s.broker.on('clientError', function (client, err) {
t.pass('should emit error')
})
})
;[{ qos: 0, clean: false }, { qos: 0, clean: true }, { qos: 1, clean: false }, { qos: 1, clean: true }].forEach(function (ele) {
test('subscribe a single topic in QoS ' + ele.qos + ' [clean=' + ele.clean + ']', function (t) {
t.plan(5)
const s = connect(setup(), { clean: ele.clean })
t.teardown(s.broker.close.bind(s.broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
dup: false,
length: 12,
qos: 0,
retain: false
}
const expectedSubs = ele.clean ? null : [{ topic: 'hello', qos: ele.qos, rh: undefined, rap: undefined, nl: undefined }]
subscribe(t, s, 'hello', ele.qos, function () {
s.outStream.once('data', function (packet) {
t.same(packet, expected, 'packet matches')
})
s.broker.persistence.subscriptionsByClient(s.client, function (_, subs) {
t.same(subs, expectedSubs)
})
s.broker.publish({
cmd: 'publish',
topic: 'hello',
payload: 'world'
})
})
})
})
// Catch invalid packet writeToStream errors
test('return write errors to callback', function (t) {
t.plan(1)
const write = proxyquire('../lib/write.js', {
'mqtt-packet': {
writeToStream: () => {
throw Error('error')
}
}
})
const client = {
conn: {
writable: true
},
connecting: true
}
write(client, {}, function (err) {
t.equal(err.message, 'packet received not valid', 'should return the error to callback')
})
})
;[{ qos: 0, clean: false }, { qos: 0, clean: true }, { qos: 1, clean: false }, { qos: 1, clean: true }].forEach(function (ele) {
test('subscribe multipe topics in QoS ' + ele.qos + ' [clean=' + ele.clean + ']', function (t) {
t.plan(5)
const s = connect(setup(), { clean: ele.clean })
t.teardown(s.broker.close.bind(s.broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
dup: false,
length: 12,
qos: 0,
retain: false
}
const subs = [
{ topic: 'hello', qos: ele.qos, rh: undefined, rap: undefined, nl: undefined },
{ topic: 'world', qos: ele.qos, rh: undefined, rap: undefined, nl: undefined }
]
const expectedSubs = ele.clean ? null : subs
subscribeMultiple(t, s, subs, [ele.qos, ele.qos], function () {
s.outStream.on('data', function (packet) {
t.same(packet, expected, 'packet matches')
})
s.broker.persistence.subscriptionsByClient(s.client, function (_, saveSubs) {
t.same(saveSubs, expectedSubs)
})
s.broker.publish({
cmd: 'publish',
topic: 'hello',
payload: 'world'
})
})
})
})
test('does not die badly on connection error', function (t) {
t.plan(3)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
s.inStream.write({
cmd: 'subscribe',
messageId: 42,
subscriptions: [{
topic: 'hello',
qos: 0
}]
})
s.broker.on('clientError', function (client, err) {
t.ok(client, 'client is passed')
t.ok(err, 'err is passed')
})
s.outStream.on('data', function (packet) {
s.conn.destroy()
s.broker.publish({
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world')
}, function () {
t.pass('calls the callback')
})
})
})
// Guarded in mqtt-packet
test('subscribe should have messageId', function (t) {
t.plan(1)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
s.inStream.write({
cmd: 'subscribe',
subscriptions: [{
topic: 'hello',
qos: 0
}]
})
s.broker.on('connectionError', function (client, err) {
t.ok(err.message, 'Invalid messageId')
})
})
test('subscribe with messageId 0 should return suback', function (t) {
t.plan(1)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
s.inStream.write({
cmd: 'subscribe',
subscriptions: [{
topic: 'hello',
qos: 0
}],
messageId: 0
})
s.outStream.once('data', function (packet) {
t.same(packet, {
cmd: 'suback',
messageId: 0,
dup: false,
length: 3,
qos: 0,
retain: false,
granted: [
0
]
}, 'packet matches')
})
})
test('unsubscribe', function (t) {
t.plan(5)
const s = noError(connect(setup()), t)
t.teardown(s.broker.close.bind(s.broker))
subscribe(t, s, 'hello', 0, function () {
s.inStream.write({
cmd: 'unsubscribe',
messageId: 43,
unsubscriptions: ['hello']
})
s.outStream.once('data', function (packet) {
t.same(packet, {
cmd: 'unsuback',
messageId: 43,
dup: false,
length: 2,
qos: 0,
retain: false
}, 'packet matches')
s.outStream.on('data', function (packet) {
t.fail('packet received')
})
s.broker.publish({
cmd: 'publish',
topic: 'hello',
payload: 'world'
}, function () {
t.pass('publish finished')
})
})
})
})
test('unsubscribe without subscribe', function (t) {
t.plan(1)
const s = noError(connect(setup()), t)
t.teardown(s.broker.close.bind(s.broker))
s.inStream.write({
cmd: 'unsubscribe',
messageId: 43,
unsubscriptions: ['hello']
})
s.outStream.once('data', function (packet) {
t.same(packet, {
cmd: 'unsuback',
messageId: 43,
dup: false,
length: 2,
qos: 0,
retain: false
}, 'packet matches')
})
})
test('unsubscribe on disconnect for a clean=true client', function (t) {
t.plan(6)
const opts = { clean: true }
const s = connect(setup(), opts)
t.teardown(s.broker.close.bind(s.broker))
subscribe(t, s, 'hello', 0, function () {
s.conn.destroy(null, function () {
t.pass('closed streams')
})
s.outStream.on('data', function () {
t.fail('should not receive any more messages')
})
s.broker.once('unsubscribe', function () {
t.pass('should emit unsubscribe')
})
s.broker.publish({
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world')
}, function () {
t.pass('calls the callback')
})
})
})
test('unsubscribe on disconnect for a clean=false client', function (t) {
t.plan(5)
const opts = { clean: false }
const s = connect(setup(), opts)
t.teardown(s.broker.close.bind(s.broker))
subscribe(t, s, 'hello', 0, function () {
s.conn.destroy(null, function () {
t.pass('closed streams')
})
s.outStream.on('data', function () {
t.fail('should not receive any more messages')
})
s.broker.once('unsubscribe', function () {
t.fail('should not emit unsubscribe')
})
s.broker.publish({
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world')
}, function () {
t.pass('calls the callback')
})
})
})
test('disconnect', function (t) {
t.plan(1)
const s = noError(connect(setup()), t)
t.teardown(s.broker.close.bind(s.broker))
s.broker.on('clientDisconnect', function () {
t.pass('closed stream')
})
s.inStream.write({
cmd: 'disconnect'
})
})
test('disconnect client on wrong cmd', function (t) {
t.plan(1)
const s = noError(connect(setup()), t)
t.teardown(s.broker.close.bind(s.broker))
s.broker.on('clientDisconnect', function () {
t.pass('closed stream')
})
s.broker.on('clientReady', function (c) {
// don't use stream write here because it will throw an error on mqtt_packet genetete
c._parser.emit('packet', { cmd: 'pippo' })
})
})
test('client closes', function (t) {
t.plan(5)
const broker = aedes()
const client = noError(connect(setup(broker), { clientId: 'abcde' }))
broker.on('clientReady', function () {
const brokerClient = broker.clients.abcde
t.equal(brokerClient.connected, true, 'client connected')
eos(client.conn, t.pass.bind(t, 'client closes'))
setImmediate(() => {
brokerClient.close(function () {
t.equal(broker.clients.abcde, undefined, 'client instance is removed')
})
t.equal(brokerClient.connected, false, 'client disconnected')
broker.close(function (err) {
t.error(err, 'no error')
})
})
})
})
test('broker closes', function (t) {
t.plan(4)
const broker = aedes()
const client = noError(connect(setup(broker), {
clientId: 'abcde'
}, function () {
eos(client.conn, t.pass.bind(t, 'client closes'))
broker.close(function (err) {
t.error(err, 'no error')
t.ok(broker.closed)
t.equal(broker.clients.abcde, undefined, 'client instance is removed')
})
}))
})
test('broker closes gracefully', function (t) {
t.plan(7)
const broker = aedes()
const client1 = noError(connect(setup(broker), {
}, function () {
const client2 = noError(connect(setup(broker), {
}, function () {
t.equal(broker.connectedClients, 2, '2 connected clients')
eos(client1.conn, t.pass.bind(t, 'client1 closes'))
eos(client2.conn, t.pass.bind(t, 'client2 closes'))
broker.close(function (err) {
t.error(err, 'no error')
t.ok(broker.mq.closed, 'broker mq closes')
t.ok(broker.closed, 'broker closes')
t.equal(broker.connectedClients, 0, 'no connected clients')
})
}))
}))
})
test('testing other event', function (t) {
t.plan(1)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const client = setup(broker)
broker.on('connectionError', function (client, error) {
t.notOk(client.id, null)
})
client.conn.emit('error', 'Connect not yet arrived')
})
test('connect without a clientId for MQTT 3.1.1', function (t) {
t.plan(1)
const s = setup()
t.teardown(s.broker.close.bind(s.broker))
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.same(packet, {
cmd: 'connack',
returnCode: 0,
length: 2,
qos: 0,
retain: false,
dup: false,
topic: null,
payload: null,
sessionPresent: false
}, 'successful connack')
})
})
test('disconnect existing client with the same clientId', function (t) {
t.plan(2)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const c1 = connect(setup(broker), {
clientId: 'abcde'
}, function () {
eos(c1.conn, function () {
t.pass('first client disconnected')
})
connect(setup(broker), {
clientId: 'abcde'
}, function () {
t.pass('second client connected')
})
})
})
test('disconnect if another broker connects the same clientId', function (t) {
t.plan(2)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const c1 = connect(setup(broker), {
clientId: 'abcde'
}, function () {
eos(c1.conn, function () {
t.pass('disconnect first client')
})
broker.publish({
topic: '$SYS/anotherBroker/new/clients',
payload: Buffer.from('abcde')
}, function () {
t.pass('second client connects to another broker')
})
})
})
test('publish to $SYS/broker/new/clients', function (t) {
t.plan(1)
const broker = aedes()
t.teardown(broker.close.bind(broker))
broker.mq.on('$SYS/' + broker.id + '/new/clients', function (packet, done) {
t.equal(packet.payload.toString(), 'abcde', 'clientId matches')
done()
})
connect(setup(broker), {
clientId: 'abcde'
})
})
test('publish to $SYS/broker/new/subsribers and $SYS/broker/new/unsubsribers', function (t) {
t.plan(7)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const sub = {
topic: 'hello',
qos: 0
}
broker.mq.on('$SYS/' + broker.id + '/new/subscribes', function (packet, done) {
const payload = JSON.parse(packet.payload.toString())
t.equal(payload.clientId, 'abcde', 'clientId matches')
t.same(payload.subs, [sub], 'subscriptions matches')
done()
})
broker.mq.on('$SYS/' + broker.id + '/new/unsubscribes', function (packet, done) {
const payload = JSON.parse(packet.payload.toString())
t.equal(payload.clientId, 'abcde', 'clientId matches')
t.same(payload.subs, [sub.topic], 'unsubscriptions matches')
done()
})
const subscriber = connect(setup(broker), {
clean: false, clientId: 'abcde'
}, function () {
subscribe(t, subscriber, sub.topic, sub.qos, function () {
subscriber.inStream.write({
cmd: 'unsubscribe',
messageId: 43,
unsubscriptions: ['hello']
})
})
})
})
test('restore QoS 0 subscriptions not clean', function (t) {
t.plan(5)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
dup: false,
length: 12,
retain: false
}
let subscriber = connect(setup(broker), {
clean: false, clientId: 'abcde'
}, function () {
subscribe(t, subscriber, 'hello', 0, function () {
subscriber.inStream.end()
const publisher = connect(setup(broker), {
}, function () {
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
t.equal(connect.sessionPresent, true, 'session present is set to true')
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 0
})
})
subscriber.outStream.once('data', function (packet) {
t.same(packet, expected, 'packet must match')
})
})
})
})
})
test('do not restore QoS 0 subscriptions when clean', function (t) {
t.plan(5)
const broker = aedes()
t.teardown(broker.close.bind(broker))
let subscriber = connect(setup(broker), {
clean: true, clientId: 'abcde'
}, function () {
subscribe(t, subscriber, 'hello', 0, function () {
subscriber.inStream.end()
subscriber.broker.persistence.subscriptionsByClient(broker.clients.abcde, function (_, subs, client) {
t.equal(subs, null, 'no previous subscriptions restored')
})
const publisher = connect(setup(broker), {
}, function () {
subscriber = connect(setup(broker), {
clean: true, clientId: 'abcde'
}, function (connect) {
t.equal(connect.sessionPresent, false, 'session present is set to false')
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 0
})
})
subscriber.outStream.once('data', function (packet) {
t.fail('packet received')
})
})
})
})
})
test('double sub does not double deliver', function (t) {
t.plan(7)
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
dup: false,
length: 12,
qos: 0,
retain: false
}
const s = connect(setup(), {
}, function () {
subscribe(t, s, 'hello', 0, function () {
subscribe(t, s, 'hello', 0, function () {
s.outStream.once('data', function (packet) {
t.same(packet, expected, 'packet matches')
s.outStream.on('data', function () {
t.fail('double deliver')
})
})
s.broker.publish({
cmd: 'publish',
topic: 'hello',
payload: 'world'
})
})
})
})
t.teardown(s.broker.close.bind(s.broker))
})
test('overlapping sub does not double deliver', function (t) {
t.plan(7)
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
dup: false,
length: 12,
qos: 0,
retain: false
}
const s = connect(setup(), {
}, function () {
subscribe(t, s, 'hello', 0, function () {
subscribe(t, s, 'hello/#', 0, function () {
s.outStream.once('data', function (packet) {
t.same(packet, expected, 'packet matches')
s.outStream.on('data', function () {
t.fail('double deliver')
})
})
s.broker.publish({
cmd: 'publish',
topic: 'hello',
payload: 'world'
})
})
})
})
t.teardown(s.broker.close.bind(s.broker))
})
test('clear drain', function (t) {
t.plan(4)
const s = connect(setup(), {
}, function () {
subscribe(t, s, 'hello', 0, function () {
// fake a busy socket
s.conn.write = function (chunk, enc, cb) {
return false
}
s.broker.publish({
cmd: 'publish',
topic: 'hello',
payload: 'world'
}, function () {
t.pass('callback called')
})
s.conn.destroy()
})
})
t.teardown(s.broker.close.bind(s.broker))
})
test('id option', function (t) {
t.plan(2)
const broker1 = aedes()
setup(broker1).conn.destroy()
t.ok(broker1.id, 'broker gets random id when id option not set')
const broker2 = aedes({ id: 'abc' })
setup(broker2).conn.destroy()
t.equal(broker2.id, 'abc', 'broker id equals id option when set')
t.teardown(() => {
broker1.close()
broker2.close()
})
})
test('not duplicate client close when client error occurs', function (t) {
t.plan(1)
const broker = aedes()
t.teardown(broker.close.bind(broker))
connect(setup(broker))
broker.on('client', function (client) {
client.conn.on('drain', () => {
t.pass('client closed ok')
})
client.close()
// add back to test if there is duplicated close() call
client.conn.on('drain', () => {
t.fail('double client close calls')
})
})
})
test('not duplicate client close when double close() called', function (t) {
t.plan(1)
const broker = aedes()
t.teardown(broker.close.bind(broker))
connect(setup(broker))
broker.on('clientReady', function (client) {
client.conn.on('drain', () => {
t.pass('client closed ok')
})
client.close()
// add back to test if there is duplicated close() call
client.conn.on('drain', () => {
t.fail('double execute client close function')
})
client.close()
})
})
+57
View File
@@ -0,0 +1,57 @@
'use strict'
const { test } = require('tap')
const { setup, connect, subscribe } = require('./helper')
for (const qos of [0, 1, 2]) {
const packet = {
qos,
cmd: 'publish',
topic: 'hello',
payload: 'world'
}
if (qos > 0) packet.messageId = 42
test('normal client sends a publish message and shall receive it back, qos = ' + qos, function (t) {
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
const handle = setTimeout(() => {
t.fail('did not receive packet back')
t.end()
}, 1000)
subscribe(t, s, 'hello', qos, function () {
s.outStream.on('data', (packet) => {
if (packet.cmd === 'publish') {
clearTimeout(handle)
t.end()
} else if (packet.cmd === 'pubrec') {
s.inStream.write({ cmd: 'pubrel', messageId: 42 })
}
})
s.inStream.write(packet)
})
})
test('bridge client sends a publish message but shall not receive it back, qos = ' + qos, function (t) {
// protocolVersion 128 + 4 means mqtt 3.1.1 with bridgeMode enabled
// https://github.com/mqttjs/mqtt-packet/blob/7f7c2ed8bcb4b2c582851d120a94e0b4a731f661/parser.js#L171
const s = connect(setup(), { clientId: 'my-client-bridge-1', protocolVersion: 128 + 4 })
t.teardown(s.broker.close.bind(s.broker))
const handle = setTimeout(() => t.end(), 1000)
subscribe(t, s, 'hello', qos, function () {
s.outStream.on('data', function () {
clearTimeout(handle)
t.fail('should not receive packet back')
t.end()
})
s.inStream.write(packet)
})
})
}
File diff suppressed because it is too large Load Diff
+177
View File
@@ -0,0 +1,177 @@
'use strict'
const { test } = require('tap')
const EventEmitter = require('events')
const { setup, connect, subscribe } = require('./helper')
const aedes = require('../')
test('aedes is closed before client authenticate returns', function (t) {
t.plan(1)
const evt = new EventEmitter()
const broker = aedes({
authenticate: (client, username, password, done) => {
evt.emit('AuthenticateBegin', client)
setTimeout(function () {
done(null, true)
}, 2000)
}
})
broker.on('client', function (client) {
t.fail('should no client registration')
})
broker.on('connackSent', function () {
t.fail('should no connack be sent')
})
broker.on('clientError', function (client, err) {
t.error(err)
})
connect(setup(broker))
evt.on('AuthenticateBegin', function (client) {
t.equal(broker.connectedClients, 0)
broker.close()
})
})
test('client is closed before authenticate returns', function (t) {
t.plan(1)
const evt = new EventEmitter()
const broker = aedes({
authenticate: async (client, username, password, done) => {
evt.emit('AuthenticateBegin', client)
setTimeout(function () {
done(null, true)
}, 2000)
}
})
t.teardown(broker.close.bind(broker))
broker.on('client', function (client) {
t.fail('should no client registration')
})
broker.on('connackSent', function () {
t.fail('should no connack be sent')
})
broker.on('clientError', function (client, err) {
t.error(err)
})
connect(setup(broker))
evt.on('AuthenticateBegin', function (client) {
t.equal(broker.connectedClients, 0)
client.close()
})
})
test('client is closed before authorizePublish returns', function (t) {
t.plan(3)
const evt = new EventEmitter()
const broker = aedes({
authorizePublish: (client, packet, done) => {
evt.emit('AuthorizePublishBegin', client)
// simulate latency writing to persistent store.
setTimeout(function () {
done()
evt.emit('AuthorizePublishEnd', client)
}, 2000)
}
})
broker.on('clientError', function (client, err) {
t.equal(err.message, 'connection closed')
})
const s = connect(setup(broker))
s.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 10,
retain: false
})
evt.on('AuthorizePublishBegin', function (client) {
t.equal(broker.connectedClients, 1)
client.close()
})
evt.on('AuthorizePublishEnd', function (client) {
t.equal(broker.connectedClients, 0)
broker.close()
})
})
test('close client when its socket is closed', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const subscriber = connect(setup(broker))
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.inStream.end()
subscriber.conn.on('close', function () {
t.equal(broker.connectedClients, 0, 'no connected client')
})
})
})
test('multiple clients subscribe same topic, and all clients still receive message except the closed one', function (t) {
t.plan(5)
const mqtt = require('mqtt')
const broker = aedes()
let client2
t.teardown(() => {
client2.end()
broker.close()
server.close()
})
const server = require('net').createServer(broker.handle)
const port = 1883
server.listen(port)
broker.on('clientError', function (client, err) {
t.error(err)
})
const _sameTopic = 'hello'
// client 1
const client1 = mqtt.connect('mqtt://localhost', { clientId: 'client1', resubscribe: false, reconnectPeriod: -1 })
client1.on('message', () => {
t.fail('client1 receives message')
})
client1.subscribe(_sameTopic, { qos: 0, retain: false }, () => {
t.pass('client1 sub callback')
// stimulate closed socket by users
client1.stream.destroy()
// client 2
client2 = mqtt.connect('mqtt://localhost', { clientId: 'client2', resubscribe: false })
client2.on('message', () => {
t.pass('client2 receives message')
t.equal(broker.connectedClients, 1)
})
client2.subscribe(_sameTopic, { qos: 0, retain: false }, () => {
t.pass('client2 sub callback')
// pubClient
const pubClient = mqtt.connect('mqtt://localhost', { clientId: 'pubClient' })
pubClient.publish(_sameTopic, 'world', { qos: 0, retain: false }, () => {
t.pass('pubClient publish event')
pubClient.end()
})
})
})
})
+762
View File
@@ -0,0 +1,762 @@
'use strict'
const { test } = require('tap')
const http = require('http')
const ws = require('websocket-stream')
const mqtt = require('mqtt')
const { setup, connect, delay } = require('./helper')
const aedes = require('../')
;[{ ver: 3, id: 'MQIsdp' }, { ver: 4, id: 'MQTT' }].forEach(function (ele) {
test('connect and connack (minimal)', function (t) {
t.plan(2)
const s = setup()
t.teardown(s.broker.close.bind(s.broker))
s.inStream.write({
cmd: 'connect',
protocolId: ele.id,
protocolVersion: ele.ver,
clean: true,
clientId: 'my-client',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.same(packet, {
cmd: 'connack',
returnCode: 0,
length: 2,
qos: 0,
retain: false,
dup: false,
topic: null,
payload: null,
sessionPresent: false
}, 'successful connack')
t.equal(s.client.version, ele.ver)
})
})
})
// [MQTT-3.1.2-2]
test('reject client requested for unacceptable protocol version', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQIsdp',
protocolVersion: 5,
clean: true,
clientId: 'my-client',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'connack')
t.equal(packet.returnCode, 1, 'unacceptable protocol version')
t.equal(broker.connectedClients, 0)
})
broker.on('clientError', function (client, err) {
t.fail('should not raise clientError error')
})
broker.on('connectionError', function (client, err) {
t.equal(err.message, 'unacceptable protocol version')
})
})
// [MQTT-3.1.2-1], Guarded in mqtt-packet
test('reject client requested for unsupported protocol version', function (t) {
t.plan(3)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 2,
clean: true,
clientId: 'my-client',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.fail('no data sent')
})
broker.on('connectionError', function (client, err) {
t.equal(client.version, null)
t.equal(err.message, 'Invalid protocol version')
t.equal(broker.connectedClients, 0)
})
})
test('reject clients that exceed the keepalive limit', function (t) {
t.plan(3)
const broker = aedes({
keepaliveLimit: 100
})
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
keepalive: 150
})
s.outStream.on('data', function (packet) {
console.log(packet)
t.same(packet, {
cmd: 'connack',
returnCode: 6,
length: 2,
qos: 0,
retain: false,
dup: false,
topic: null,
payload: null,
sessionPresent: false
}, 'unsuccessful connack, keep alive limit exceeded')
})
broker.on('connectionError', function (client, err) {
t.equal(err.message, 'keep alive limit exceeded')
t.equal(broker.connectedClients, 0)
})
})
// Guarded in mqtt-packet
test('reject clients with no clientId running on MQTT 3.1.0', function (t) {
t.plan(3)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQIsdp',
protocolVersion: 3,
clean: true,
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.fail('no data sent')
})
broker.on('connectionError', function (client, err) {
t.equal(client.version, null)
t.equal(err.message, 'clientId must be supplied before 3.1.1')
t.equal(broker.connectedClients, 0)
})
})
// [MQTT-3.1.3-7], Guarded in mqtt-packet
test('reject clients without clientid and clean=false on MQTT 3.1.1', function (t) {
t.plan(2)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: false,
clientId: '',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.fail('no data sent')
})
broker.on('connectionError', function (client, err) {
t.equal(err.message, 'clientId must be given if cleanSession set to 0')
t.equal(broker.connectedClients, 0)
})
})
test('clients without clientid and clean=true on MQTT 3.1.1 will get a generated clientId', function (t) {
t.plan(5)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'connack')
t.equal(packet.returnCode, 0)
t.equal(broker.connectedClients, 1)
t.equal(s.client.version, 4)
})
broker.on('connectionError', function (client, err) {
t.error(err, 'no error')
})
broker.on('client', function (client) {
t.ok(client.id.startsWith('aedes_'))
})
})
test('client connect error while fetching subscriptions', function (t) {
t.plan(2)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
broker.persistence.subscriptionsByClient = function (c, cb) {
cb(new Error('error'), [], c)
}
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: false,
clientId: 'my-client',
keepalive: 0
})
broker.on('clientError', function (client, err) {
t.equal(client.version, 4)
t.pass('throws error')
})
})
test('client connect clear outgoing', function (t) {
t.plan(1)
const clientId = 'abcde'
const brokerId = 'pippo'
const broker = aedes({ id: brokerId })
t.teardown(broker.close.bind(broker))
const subs = [{ clientId }]
const packet = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
brokerId,
brokerCounter: 2,
retain: true,
messageId: 42,
dup: false
}
broker.persistence.outgoingEnqueueCombi(subs, packet, function () {
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId,
keepalive: 0
})
broker.on('clientReady', function (client) {
broker.persistence.outgoingUpdate(client, packet, function (err) {
t.equal('no such packet', err.message, 'packet not found')
})
})
})
})
test('clients with zero-byte clientid and clean=true on MQTT 3.1.1 will get a generated clientId', function (t) {
t.plan(5)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: '',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'connack')
t.equal(packet.returnCode, 0)
t.equal(broker.connectedClients, 1)
t.equal(s.client.version, 4)
})
broker.on('connectionError', function (client, err) {
t.error(err, 'no error')
})
broker.on('client', function (client) {
t.ok(client.id.startsWith('aedes_'))
})
})
// [MQTT-3.1.3-7]
test('reject clients with > 23 clientId length in MQTT 3.1.0', function (t) {
t.plan(7)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
const conn = s.client.conn
const end = conn.end
conn.end = function () {
t.fail('should not call `conn.end()`')
end()
}
function drain () {
t.pass('should empty connection request queue')
}
conn._writableState.getBuffer = () => [{ callback: drain }, { callback: drain }]
s.inStream.write({
cmd: 'connect',
protocolId: 'MQIsdp',
protocolVersion: 3,
clean: true,
clientId: 'abcdefghijklmnopqrstuvwxyz',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'connack')
t.equal(packet.returnCode, 2, 'identifier rejected')
t.equal(broker.connectedClients, 0)
t.equal(s.client.version, null)
})
broker.on('connectionError', function (client, err) {
t.equal(err.message, 'identifier rejected')
})
})
test('connect clients with > 23 clientId length using aedes maxClientsIdLength option in MQTT 3.1.0', function (t) {
t.plan(4)
const broker = aedes({ maxClientsIdLength: 26 })
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 3,
clean: true,
clientId: 'abcdefghijklmnopqrstuvwxyz',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'connack')
t.equal(packet.returnCode, 0)
t.equal(broker.connectedClients, 1)
t.equal(s.client.version, 3)
})
broker.on('connectionError', function (client, err) {
t.error(err, 'no error')
})
})
test('connect with > 23 clientId length in MQTT 3.1.1', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'abcdefghijklmnopqrstuvwxyz',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'connack')
t.equal(packet.returnCode, 0)
t.equal(broker.connectedClients, 1)
t.equal(s.client.version, 4)
})
broker.on('connectionError', function (client, err) {
t.error(err, 'no error')
})
})
// [MQTT-3.1.0-1]
test('the first Packet MUST be a CONNECT Packet', function (t) {
t.plan(2)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const packet = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
retain: false
}
const s = setup(broker)
s.inStream.write(packet)
broker.on('connectionError', function (client, err) {
t.equal(err.message, 'Invalid protocol')
})
setImmediate(() => {
t.ok(s.conn.destroyed, 'close connection if first packet is not a CONNECT')
s.conn.destroy()
})
})
// [MQTT-3.1.0-2]
test('second CONNECT Packet sent from a Client as a protocol violation and disconnect the Client', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const packet = {
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'my-client',
keepalive: 0
}
broker.on('clientError', function (client, err) {
t.equal(err.message, 'Invalid protocol')
})
const s = connect(setup(broker), { clientId: 'abcde' })
s.broker.on('clientReady', function () {
t.ok(broker.clients.abcde.connected)
// destory client when there is a 2nd cmd:connect, even the clientId is dfferent
s.inStream.write(packet)
setImmediate(() => {
t.equal(broker.clients.abcde, undefined, 'client instance is removed')
t.ok(s.conn.destroyed, 'close connection if packet is a CONNECT after network is established')
})
})
})
test('connect handler calls done when preConnect throws error', function (t) {
t.plan(1)
const broker = aedes({
preConnect: function (client, packet, done) {
done(Error('error in preconnect'))
}
})
t.teardown(broker.close.bind(broker))
const s = setup(broker)
const handleConnect = require('../lib/handlers/connect')
handleConnect(s.client, {}, function done (err) {
t.equal(err.message, 'error in preconnect', 'calls done with error')
})
})
test('handler calls done when disconnect or unknown packet cmd is received', function (t) {
t.plan(2)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
const handle = require('../lib/handlers/index')
handle(s.client, { cmd: 'disconnect' }, function done () {
t.pass('calls done when disconnect cmd is received')
})
handle(s.client, { cmd: 'fsfadgragae' }, function done () {
t.pass('calls done when unknown cmd is received')
})
})
test('reject second CONNECT Packet sent while first CONNECT still in preConnect stage', function (t) {
t.plan(2)
const packet1 = {
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'my-client-1',
keepalive: 0
}
const packet2 = {
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'my-client-2',
keepalive: 0
}
let i = 0
const broker = aedes({
preConnect: function (client, packet, done) {
const ms = i++ === 0 ? 2000 : 500
setTimeout(function () {
done(null, true)
}, ms)
}
})
t.teardown(broker.close.bind(broker))
const s = setup(broker)
broker.on('connectionError', function (client, err) {
t.equal(err.info.clientId, 'my-client-2')
t.equal(err.message, 'Invalid protocol')
})
const msg = async (s, ms, msg) => {
await delay(ms)
s.inStream.write(msg)
}
;(async () => {
await Promise.all([msg(s, 100, packet1), msg(s, 200, packet2)])
})().catch(
(error) => {
t.fail(error)
}
)
})
// [MQTT-3.1.2-1], Guarded in mqtt-packet
test('reject clients with wrong protocol name', function (t) {
t.plan(2)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT_hello',
protocolVersion: 3,
clean: true,
clientId: 'my-client',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.fail('no data sent')
})
broker.on('connectionError', function (client, err) {
t.equal(err.message, 'Invalid protocolId')
t.equal(broker.connectedClients, 0)
})
})
test('After first CONNECT Packet, others are queued until \'connect\' event', function (t) {
t.plan(2)
const queueLimit = 50
const broker = aedes({ queueLimit })
t.teardown(broker.close.bind(broker))
const publishP = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
retain: false
}
const connectP = {
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'abcde',
keepalive: 0
}
const s = setup(broker)
s.inStream.write(connectP)
process.once('warning', e => t.fail('Memory leak detected'))
for (let i = 0; i < queueLimit; i++) {
s.inStream.write(publishP)
}
broker.on('client', function (client) {
t.equal(client._parser._queue.length, queueLimit, 'Packets have been queued')
client.once('connected', () => {
t.equal(client._parser._queue, null, 'Queue is empty')
s.conn.destroy()
})
})
})
test('Test queue limit', function (t) {
t.plan(1)
const queueLimit = 50
const broker = aedes({ queueLimit })
t.teardown(broker.close.bind(broker))
const publishP = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
retain: false
}
const connectP = {
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'abcde',
keepalive: 0
}
const s = setup(broker)
s.inStream.write(connectP)
process.once('warning', e => t.fail('Memory leak detected'))
for (let i = 0; i < queueLimit + 1; i++) {
s.inStream.write(publishP)
}
broker.on('connectionError', function (conn, err) {
t.equal(err.message, 'Client queue limit reached', 'Queue error is thrown')
s.conn.destroy()
})
})
;[['fail with no error msg', 3, null, false], ['succeed with no error msg', 9, null, true], ['fail with error msg', 6, new Error('connection banned'), false], ['succeed with error msg', 6, new Error('connection banned'), true]].forEach(function (ele, idx) {
const title = ele[0]
const plan = ele[1]
const err = ele[2]
const ok = ele[3]
test('preConnect handler - ' + title, function (t) {
t.plan(plan)
const broker = aedes({
preConnect: function (client, packet, done) {
t.ok(client.connecting)
t.notOk(client.connected)
t.equal(client.version, null)
return done(err, ok)
}
})
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'my-client-' + idx,
keepalive: 0
})
broker.on('client', function (client) {
if (ok && !err) {
t.ok(client.connecting)
t.notOk(client.connected)
t.pass('register client ok')
} else {
t.fail('no reach here')
}
})
broker.on('clientReady', function (client) {
t.notOk(client.connecting)
t.ok(client.connected)
t.pass('connect ok')
})
broker.on('clientError', function (client, err) {
t.fail('no client error')
})
broker.on('connectionError', function (client, err) {
if (err) {
t.notOk(client.connecting)
t.notOk(client.connected)
t.equal(err.message, 'connection banned')
} else {
t.fail('no connection error')
}
})
})
})
// websocket-stream based connections
test('websocket clients have access to the request object', function (t) {
t.plan(3)
const port = 4883
const broker = aedes()
broker.on('client', function (client) {
if (client.req) {
t.pass('client request object present')
if (client.req.headers) {
t.equal('sample', client.req.headers['x-test-protocol'])
}
} else {
t.fail('no request object present')
}
})
const server = http.createServer()
ws.createServer({
server
}, broker.handle)
server.listen(port, function (err) {
t.error(err, 'no error')
})
const client = mqtt.connect(`ws://localhost:${port}`, {
wsOptions: {
headers: {
'X-Test-Protocol': 'sample'
}
}
})
t.teardown(() => {
client.end(true)
broker.close()
server.close()
})
})
+242
View File
@@ -0,0 +1,242 @@
'use strict'
const { test } = require('tap')
const mqemitter = require('mqemitter')
const { setup, connect, subscribe } = require('./helper')
const aedes = require('../')
test('publishes an hearbeat', function (t) {
t.plan(2)
const broker = aedes({
heartbeatInterval: 10 // ms
})
t.teardown(broker.close.bind(broker))
broker.subscribe('$SYS/+/heartbeat', function (message, cb) {
const id = message.topic.match(/\$SYS\/([^/]+)\/heartbeat/)[1]
t.equal(id, broker.id, 'broker id matches')
t.same(message.payload.toString(), id, 'message has id as the payload')
cb()
})
})
test('publishes birth', function (t) {
t.plan(4)
const mq = mqemitter()
const brokerId = 'test-broker'
const fakeBroker = 'fake-broker'
const clientId = 'test-client'
mq.on(`$SYS/${brokerId}/birth`, (message, cb) => {
t.pass('broker birth received')
t.same(message.payload.toString(), brokerId, 'message has id as the payload')
cb()
})
const broker = aedes({
id: brokerId,
mq
})
broker.on('client', (client) => {
t.equal(client.id, clientId, 'client connected')
// set a fake counter on a fake broker
process.nextTick(() => {
broker.clients[clientId].duplicates[fakeBroker] = 42
mq.emit({ topic: `$SYS/${fakeBroker}/birth`, payload: Buffer.from(fakeBroker) })
})
})
mq.on(`$SYS/${fakeBroker}/birth`, (message, cb) => {
process.nextTick(() => {
t.equal(!!broker.clients[clientId].duplicates[fakeBroker], false, 'client duplicates has been resetted')
cb()
})
})
const s = connect(setup(broker), { clientId })
t.teardown(s.broker.close.bind(s.broker))
})
;['$mcollina', '$SYS'].forEach(function (topic) {
test('does not forward $ prefixed topics to # subscription - ' + topic, function (t) {
t.plan(4)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
subscribe(t, s, '#', 0, function () {
s.outStream.once('data', function (packet) {
t.fail('no packet should be received')
})
s.broker.mq.emit({
cmd: 'publish',
topic: topic + '/hello',
payload: 'world'
}, function () {
t.pass('nothing happened')
})
})
})
test('does not forward $ prefixed topics to +/# subscription - ' + topic, function (t) {
t.plan(4)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
subscribe(t, s, '+/#', 0, function () {
s.outStream.once('data', function (packet) {
t.fail('no packet should be received')
})
s.broker.mq.emit({
cmd: 'publish',
topic: topic + '/hello',
payload: 'world'
}, function () {
t.pass('nothing happened')
})
})
})
})
test('does not store $SYS topics to QoS 1 # subscription', function (t) {
t.plan(3)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const opts = { clean: false, clientId: 'abcde' }
let s = connect(setup(broker), opts)
subscribe(t, s, '#', 1, function () {
s.inStream.end()
s.broker.publish({
cmd: 'publish',
topic: '$SYS/hello',
payload: 'world',
qos: 1
}, function () {
s = connect(setup(broker), { clean: false, clientId: 'abcde' })
s.outStream.once('data', function (packet) {
t.fail('no packet should be received')
})
})
})
})
test('Emit event when receives a ping', { timeout: 2000 }, function (t) {
t.plan(5)
const broker = aedes()
t.teardown(broker.close.bind(broker))
broker.on('ping', function (packet, client) {
if (client && client) {
t.equal(client.id, 'abcde')
t.equal(packet.cmd, 'pingreq')
t.equal(packet.payload, null)
t.equal(packet.topic, null)
t.equal(packet.length, 0)
}
})
const s = connect(setup(broker), { clientId: 'abcde' })
s.inStream.write({
cmd: 'pingreq'
})
})
test('Emit event when broker closed', function (t) {
t.plan(1)
const broker = aedes()
broker.once('closed', function () {
t.ok(true)
})
broker.close()
})
test('Emit closed event one only when double broker.close()', function (t) {
t.plan(4)
const broker = aedes()
broker.on('closed', function () {
t.pass('closed')
})
t.notOk(broker.closed)
broker.close()
t.ok(broker.closed)
broker.close()
t.ok(broker.closed)
})
test('Test backpressure aedes published function', function (t) {
t.plan(2)
let publishCount = 10
let count = 0
const broker = aedes({
published: function (packet, client, done) {
if (client) {
count++
setTimeout(() => {
publisher.end()
done()
})
} else { done() }
}
})
const mqtt = require('mqtt')
const server = require('net').createServer(broker.handle)
let publisher
server.listen(0, function () {
const port = server.address().port
publisher = mqtt.connect({ port, host: 'localhost', clean: true, keepalive: 30 })
function next () {
if (--publishCount > 0) { process.nextTick(publish) }
}
function publish () {
publisher.publish('test', 'payload', next)
}
publisher.on('connect', publish)
publisher.on('end', function () {
t.ok(count > publishCount)
t.equal(publishCount, 0)
broker.close()
server.close()
})
})
})
test('clear closed clients when the same clientId is managed by another broker', function (t) {
t.plan(2)
const clientId = 'closed-client'
const aedesBroker = aedes()
// simulate a closed client on the broker
aedesBroker.clients[clientId] = { closed: true, broker: aedesBroker }
aedesBroker.connectedClients = 1
// simulate the creation of the same client on another broker of the cluster
aedesBroker.publish({ topic: '$SYS/anotherbroker/new/clients', payload: clientId }, () => {
t.equal(aedesBroker.clients[clientId], undefined) // check that the closed client was removed
t.equal(aedesBroker.connectedClients, 0)
})
t.teardown(aedesBroker.close.bind(aedesBroker))
})
+119
View File
@@ -0,0 +1,119 @@
'use strict'
const duplexify = require('duplexify')
const mqtt = require('mqtt-connection')
const { through } = require('../lib/utils')
const util = require('util')
const aedes = require('../')
const parseStream = mqtt.parseStream
const generateStream = mqtt.generateStream
let clients = 0
function setup (broker) {
const inStream = generateStream()
const outStream = parseStream()
const conn = duplexify(outStream, inStream)
broker = broker || aedes()
return {
client: broker.handle(conn),
conn,
inStream,
outStream,
broker
}
}
function connect (s, opts, connected) {
s = Object.create(s)
s.outStream = s.outStream.pipe(through(filter))
opts = opts || {}
opts.cmd = 'connect'
opts.protocolId = opts.protocolId || 'MQTT'
opts.protocolVersion = opts.protocolVersion || 4
opts.clean = !!opts.clean
opts.clientId = opts.clientId || 'my-client-' + clients++
opts.keepalive = opts.keepalive || 0
s.inStream.write(opts)
return s
function filter (packet, enc, cb) {
if (packet.cmd !== 'publish') {
delete packet.topic
delete packet.payload
}
// using setImmediate to wait for connected to be fired
// setup also needs to return first
if (packet.cmd !== 'connack') {
setImmediate(this.push.bind(this, packet))
} else if (connected && packet.returnCode === 0) {
setImmediate(connected, packet)
}
cb()
}
}
function noError (s, t) {
s.broker.on('clientError', function (client, err) {
if (err) throw err
t.notOk(err, 'must not error')
})
return s
}
function subscribe (t, subscriber, topic, qos, done) {
subscriber.inStream.write({
cmd: 'subscribe',
messageId: 24,
subscriptions: [{
topic,
qos
}]
})
subscriber.outStream.once('data', function (packet) {
t.equal(packet.cmd, 'suback')
t.same(packet.granted, [qos])
t.equal(packet.messageId, 24)
if (done) {
done(null, packet)
}
})
}
// subs: [{topic:, qos:}]
function subscribeMultiple (t, subscriber, subs, expectedGranted, done) {
subscriber.inStream.write({
cmd: 'subscribe',
messageId: 24,
subscriptions: subs
})
subscriber.outStream.once('data', function (packet) {
t.equal(packet.cmd, 'suback')
t.same(packet.granted, expectedGranted)
t.equal(packet.messageId, 24)
if (done) {
done(null, packet)
}
})
}
module.exports = {
setup,
connect,
noError,
subscribe,
subscribeMultiple,
delay: util.promisify(setTimeout)
}
+92
View File
@@ -0,0 +1,92 @@
'use strict'
const { test } = require('tap')
const eos = require('end-of-stream')
const Faketimers = require('@sinonjs/fake-timers')
const { setup, connect, noError } = require('./helper')
const aedes = require('../')
test('supports pingreq/pingresp', function (t) {
t.plan(1)
const s = noError(connect(setup()))
t.teardown(s.broker.close.bind(s.broker))
s.broker.on('keepaliveTimeout', function (client) {
t.fail('keep alive should not timeout')
})
s.inStream.write({
cmd: 'pingreq'
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'pingresp', 'the response is a pingresp')
})
})
test('supports keep alive disconnections', function (t) {
t.plan(2)
const clock = Faketimers.install()
const s = connect(setup(), { keepalive: 1 })
t.teardown(s.broker.close.bind(s.broker))
s.broker.on('keepaliveTimeout', function (client) {
t.pass('keep alive timeout')
})
eos(s.conn, function () {
t.pass('waits 1 and a half the keepalive timeout')
})
setTimeout(() => {
clock.uninstall()
}, 1.5)
clock.tick(1.5)
})
test('supports keep alive disconnections after a pingreq', function (t) {
t.plan(3)
const clock = Faketimers.install()
const s = connect(setup(), { keepalive: 1 })
t.teardown(s.broker.close.bind(s.broker))
eos(s.conn, function () {
t.pass('waits 1 and a half the keepalive timeout')
})
s.broker.on('keepaliveTimeout', function (client) {
t.pass('keep alive timeout')
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'pingresp', 'the response is a pingresp')
})
setTimeout(() => {
s.inStream.write({
cmd: 'pingreq'
})
clock.uninstall()
}, 1)
clock.tick(3)
})
test('disconnect if a connect does not arrive in time', function (t) {
t.plan(2)
const clock = Faketimers.install()
const s = setup(aedes({
connectTimeout: 500
}))
t.teardown(s.broker.close.bind(s.broker))
s.client.on('error', function (err) {
t.equal(err.message, 'connect did not arrive in time')
})
eos(s.conn, function () {
t.pass('waits waitConnectTimeout before ending')
})
setTimeout(() => {
clock.uninstall()
}, 1000)
clock.tick(1000)
})
+327
View File
@@ -0,0 +1,327 @@
'use strict'
const { test } = require('tap')
const { setup, connect, subscribe, noError } = require('./helper')
const aedes = require('../')
test('count connected clients', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
t.equal(broker.connectedClients, 0, 'no connected clients')
connect(setup(broker), {
}, function () {
t.equal(broker.connectedClients, 1, 'one connected clients')
const last = connect(setup(broker), {
}, function () {
t.equal(broker.connectedClients, 2, 'two connected clients')
last.conn.destroy()
// needed because destroy() will do the trick before
// the next tick
setImmediate(function () {
t.equal(broker.connectedClients, 1, 'one connected clients')
})
})
})
})
test('call published method', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
broker.published = function (packet, client, done) {
t.equal(packet.topic, 'hello', 'topic matches')
t.equal(packet.payload.toString(), 'world', 'payload matches')
t.equal(client, null, 'no client')
done()
}
broker.publish({
topic: 'hello',
payload: Buffer.from('world')
}, function (err) {
t.error(err, 'no error')
})
})
test('call published method with client', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
broker.published = function (packet, client, done) {
// for internal messages, client will be null
if (client) {
t.equal(packet.topic, 'hello', 'topic matches')
t.equal(packet.payload.toString(), 'world', 'payload matches')
t.equal(packet.qos, 1)
t.equal(packet.messageId, 42)
done()
}
}
const s = connect(setup(broker))
s.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
messageId: 42
})
})
test('emit publish event with client - QoS 0', function (t) {
t.plan(3)
const broker = aedes()
t.teardown(broker.close.bind(broker))
broker.on('publish', function (packet, client) {
// for internal messages, client will be null
if (client) {
t.equal(packet.qos, 0)
t.equal(packet.topic, 'hello', 'topic matches')
t.equal(packet.payload.toString(), 'world', 'payload matches')
}
})
const s = connect(setup(broker))
s.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0
})
})
test('emit publish event with client - QoS 1', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
broker.on('publish', function (packet, client) {
// for internal messages, client will be null
if (client) {
t.equal(packet.qos, 1)
t.equal(packet.messageId, 42)
t.equal(packet.topic, 'hello', 'topic matches')
t.equal(packet.payload.toString(), 'world', 'payload matches')
}
})
const s = connect(setup(broker))
s.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
messageId: 42
})
})
test('emit subscribe event', function (t) {
t.plan(6)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = connect(setup(broker), { clientId: 'abcde' })
broker.on('subscribe', function (subscriptions, client) {
t.same(subscriptions, [{
topic: 'hello',
qos: 0
}], 'topic matches')
t.equal(client.id, 'abcde', 'client matches')
})
subscribe(t, s, 'hello', 0, function () {
t.pass('subscribe completed')
})
})
test('emit subscribe event if unrecognized params in subscribe packet structure', function (t) {
t.plan(3)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = noError(connect(setup(broker)))
const subs = [{ topic: 'hello', qos: 0 }]
broker.on('subscribe', function (subscriptions, client) {
t.equal(subscriptions, subs)
t.same(client, s.client)
})
s.client.subscribe({
subscriptions: subs,
restore: true
}, function (err) {
t.error(err)
})
})
test('emit unsubscribe event', function (t) {
t.plan(6)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = connect(setup(broker), { clean: true, clientId: 'abcde' })
broker.on('unsubscribe', function (unsubscriptions, client) {
t.same(unsubscriptions, [
'hello'
], 'unsubscription matches')
t.equal(client.id, 'abcde', 'client matches')
})
subscribe(t, s, 'hello', 0, function () {
s.inStream.write({
cmd: 'unsubscribe',
messageId: 43,
unsubscriptions: ['hello']
})
s.outStream.once('data', function (packet) {
t.pass('subscribe completed')
})
})
})
test('emit unsubscribe event if unrecognized params in unsubscribe packet structure', function (t) {
t.plan(3)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = noError(connect(setup(broker)))
const unsubs = [{ topic: 'hello', qos: 0 }]
broker.on('unsubscribe', function (unsubscriptions, client) {
t.equal(unsubscriptions, unsubs)
t.same(client, s.client)
})
s.client.unsubscribe({
unsubscriptions: unsubs,
close: true
}, function (err) {
t.error(err)
})
})
test('dont emit unsubscribe event on client close', function (t) {
t.plan(3)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = noError(connect(setup(broker), { clientId: 'abcde' }), t)
broker.on('unsubscribe', function (unsubscriptions, client) {
t.error('unsubscribe should not be emitted')
})
subscribe(t, s, 'hello', 0, function () {
s.inStream.end({
cmd: 'disconnect'
})
s.outStream.once('data', function (packet) {
t.pass('unsubscribe completed')
})
})
})
test('emit clientDisconnect event', function (t) {
t.plan(1)
const broker = aedes()
t.teardown(broker.close.bind(broker))
broker.on('clientDisconnect', function (client) {
t.equal(client.id, 'abcde', 'client matches')
})
const s = noError(connect(setup(broker), { clientId: 'abcde' }), t)
s.inStream.end({
cmd: 'disconnect'
})
s.outStream.resume()
})
test('emits client', function (t) {
t.plan(1)
const broker = aedes()
t.teardown(broker.close.bind(broker))
broker.on('client', function (client) {
t.equal(client.id, 'abcde', 'clientId matches')
})
connect(setup(broker), {
clientId: 'abcde'
})
})
test('get aedes version', function (t) {
t.plan(1)
const broker = aedes()
t.teardown(broker.close.bind(broker))
t.equal(broker.version, require('../package.json').version)
})
test('connect and connackSent event', { timeout: 50 }, function (t) {
t.plan(3)
const s = setup()
t.teardown(s.broker.close.bind(s.broker))
const clientId = 'my-client'
s.broker.on('connackSent', function (packet, client) {
t.equal(packet.returnCode, 0)
t.equal(client.id, clientId, 'connackSent event and clientId matches')
})
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId,
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.same(packet, {
cmd: 'connack',
returnCode: 0,
length: 2,
qos: 0,
retain: false,
dup: false,
topic: null,
payload: null,
sessionPresent: false
}, 'successful connack')
})
})
+231
View File
@@ -0,0 +1,231 @@
'use strict'
const { test } = require('tap')
const EventEmitter = require('events')
const mqtt = require('mqtt')
const net = require('net')
const Faketimers = require('@sinonjs/fake-timers')
const aedes = require('../')
test('connect 500 concurrent clients', function (t) {
t.plan(3)
const evt = new EventEmitter()
const broker = aedes()
const server = net.createServer(broker.handle)
const total = 500
server.listen(0, function (err) {
t.error(err, 'no error')
const clock = Faketimers.createClock()
t.teardown(clock.reset.bind(clock))
const port = server.address().port
let connected = 0
const clients = []
clock.setTimeout(function () {
t.equal(clients.length, total)
t.equal(connected, total)
while (clients.length) {
clients.shift().end()
}
}, total)
evt.on('finish', function () {
if (clients.length === 0) {
broker.close()
server.close()
}
})
for (let i = 0; i < total; i++) {
clients[i] = mqtt.connect({
port,
keepalive: 0,
reconnectPeriod: 100
}).on('connect', function () {
connected++
if ((connected % (total / 10)) === 0) {
console.log('connected', connected)
}
clock.tick(1)
}).on('close', function () {
evt.emit('finish')
})
}
})
})
test('do not block after a subscription', function (t) {
t.plan(3)
const evt = new EventEmitter()
const broker = aedes()
const server = net.createServer(broker.handle)
const total = 10000
let sent = 0
let received = 0
server.listen(0, function (err) {
t.error(err, 'no error')
const clock = Faketimers.createClock()
t.teardown(clock.reset.bind(clock))
const clockId = clock.setTimeout(finish, total)
const port = server.address().port
const publisher = mqtt.connect({
port,
keepalive: 0
}).on('error', function (err) {
clock.clearTimeout(clockId)
t.fail(err)
})
let subscriber
function immediatePublish () {
setImmediate(publish)
}
function publish () {
if (sent === total) {
publisher.end()
} else {
sent++
publisher.publish('test', 'payload', immediatePublish)
}
}
function startSubscriber () {
subscriber = mqtt.connect({
port,
keepalive: 0
}).on('error', function (err) {
clock.clearTimeout(clockId)
t.fail(err)
})
subscriber.subscribe('test', publish)
subscriber.on('message', function () {
if (received % (total / 10) === 0) {
console.log('sent / received', sent, received)
}
received++
clock.tick(1)
})
subscriber.on('close', function () {
evt.emit('finish')
})
}
publisher.on('connect', startSubscriber)
publisher.on('close', function () {
evt.emit('finish')
})
evt.on('finish', function () {
if (publisher.connected || subscriber.connected) { return }
broker.close()
server.close()
t.equal(total, sent, 'messages sent')
t.equal(total, received, 'messages received')
})
function finish () {
subscriber.end()
publisher.end()
}
})
})
test('do not block with overlapping subscription', function (t) {
t.plan(3)
const evt = new EventEmitter()
const broker = aedes({ concurrency: 15 })
const server = net.createServer(broker.handle)
const total = 10000
let sent = 0
let received = 0
server.listen(0, function (err) {
t.error(err, 'no error')
const clock = Faketimers.createClock()
t.teardown(clock.reset.bind(clock))
const clockId = clock.setTimeout(finish, total)
const port = server.address().port
const publisher = mqtt.connect({
port,
keepalive: 0
}).on('error', function (err) {
clock.clearTimeout(clockId)
t.fail(err)
})
let subscriber
function immediatePublish (e) {
setImmediate(publish)
}
function publish () {
if (sent === total) {
publisher.end()
} else {
sent++
publisher.publish('test', 'payload', immediatePublish)
}
}
function startSubscriber () {
subscriber = mqtt.connect({
port,
keepalive: 0
}).on('error', function (err) {
clock.clearTimeout(clockId)
t.fail(err)
})
subscriber.subscribe('#', function () {
subscriber.subscribe('test', function () {
immediatePublish()
})
})
subscriber.on('message', function () {
if (received % (total / 10) === 0) {
console.log('sent / received', sent, received)
}
received++
clock.tick(1)
})
subscriber.on('close', function () {
evt.emit('finish')
})
}
publisher.on('connect', startSubscriber)
publisher.on('close', function () {
evt.emit('finish')
})
evt.on('finish', function () {
if (publisher.connected || subscriber.connected) { return }
broker.close()
server.close()
t.equal(total, sent, 'messages sent')
t.equal(total, received, 'messages received')
})
function finish () {
subscriber.end()
publisher.end()
}
})
})
+880
View File
@@ -0,0 +1,880 @@
'use strict'
const { test } = require('tap')
const concat = require('concat-stream')
const { setup, connect, subscribe } = require('./helper')
const Faketimers = require('@sinonjs/fake-timers')
const aedes = require('../')
test('publish QoS 1', function (t) {
t.plan(1)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
const expected = {
cmd: 'puback',
messageId: 42,
qos: 0,
dup: false,
length: 2,
retain: false
}
s.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
s.outStream.on('data', function (packet) {
t.same(packet, expected, 'packet must match')
})
})
test('publish QoS 1 throws error', function (t) {
t.plan(1)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
s.broker.persistence.subscriptionsByTopic = function (packet, done) {
return done(new Error('Throws error'))
}
s.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
s.broker.on('error', function (err) {
t.equal('Throws error', err.message, 'Throws error')
})
})
test('publish QoS 1 throws error on write', function (t) {
t.plan(1)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
s.broker.on('client', function (client) {
client.connected = false
client.connecting = false
s.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
})
s.broker.on('clientError', function (client, err) {
t.equal(err.message, 'connection closed', 'throws error')
})
})
test('publish QoS 1 and check offline queue', function (t) {
t.plan(13)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker), { clean: false })
const subscriberClient = {
id: 'abcde'
}
const subscriber = connect(setup(broker), { clean: false, clientId: subscriberClient.id })
const expected = {
cmd: 'publish',
topic: 'hello',
qos: 1,
dup: false,
retain: false
}
const expectedAck = {
cmd: 'puback',
retain: false,
qos: 0,
dup: false,
length: 2,
messageId: 10
}
const sent = {
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 10,
retain: false,
dup: false
}
const queue = []
subscribe(t, subscriber, 'hello', 1, function () {
publisher.outStream.on('data', function (packet) {
t.same(packet, expectedAck, 'ack packet must patch')
})
subscriber.outStream.on('data', function (packet) {
queue.push(packet)
delete packet.payload
delete packet.length
t.not(packet.messageId, undefined, 'messageId is assigned a value')
t.not(packet.messageId, 10, 'messageId should be unique')
expected.messageId = packet.messageId
t.same(packet, expected, 'publish packet must patch')
if (queue.length === 2) {
setImmediate(() => {
for (let i = 0; i < queue.length; i++) {
broker.persistence.outgoingClearMessageId(subscriberClient, queue[i], function (_, origPacket) {
if (origPacket) {
delete origPacket.brokerId
delete origPacket.brokerCounter
delete origPacket.payload
delete origPacket.messageId
delete sent.payload
delete sent.messageId
t.same(origPacket, sent, 'origPacket must match')
}
})
}
})
}
})
publisher.inStream.write(sent)
sent.payload = 'world2world'
publisher.inStream.write(sent)
})
})
test('publish QoS 1 and empty offline queue', function (t) {
t.plan(13)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker), { clean: false })
const subscriberClient = {
id: 'abcde'
}
const subscriber = connect(setup(broker), { clean: false, clientId: subscriberClient.id })
const expected = {
cmd: 'publish',
topic: 'hello',
qos: 1,
dup: false,
retain: false
}
const expectedAck = {
cmd: 'puback',
retain: false,
qos: 0,
dup: false,
length: 2,
messageId: 10
}
const sent = {
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 10,
retain: false,
dup: false
}
const queue = []
subscribe(t, subscriber, 'hello', 1, function () {
publisher.outStream.on('data', function (packet) {
t.same(packet, expectedAck, 'ack packet must patch')
})
subscriber.outStream.on('data', function (packet) {
queue.push(packet)
delete packet.payload
delete packet.length
t.not(packet.messageId, undefined, 'messageId is assigned a value')
t.not(packet.messageId, 10, 'messageId should be unique')
expected.messageId = packet.messageId
t.same(packet, expected, 'publish packet must patch')
if (queue.length === 2) {
setImmediate(() => {
broker.clients[subscriberClient.id].emptyOutgoingQueue(function () {
for (let i = 0; i < queue.length; i++) {
broker.persistence.outgoingClearMessageId(subscriberClient, queue[i], function (_, origPacket) {
t.equal(!!origPacket, false, 'Packet has been removed')
})
}
})
})
}
})
publisher.inStream.write(sent)
sent.payload = 'world2world'
publisher.inStream.write(sent)
})
})
test('subscribe QoS 1', function (t) {
t.plan(5)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker))
const subscriber = connect(setup(broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
dup: false,
length: 14,
retain: false
}
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.outStream.once('data', function (packet) {
subscriber.inStream.write({
cmd: 'puback',
messageId: packet.messageId
})
t.not(packet.messageId, 42, 'messageId must differ')
delete packet.messageId
t.same(packet, expected, 'packet must match')
})
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
})
})
test('subscribe QoS 0, but publish QoS 1', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker))
const subscriber = connect(setup(broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
dup: false,
length: 12,
retain: false
}
subscribe(t, subscriber, 'hello', 0, function () {
subscriber.outStream.once('data', function (packet) {
t.same(packet, expected, 'packet must match')
})
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
})
})
test('restore QoS 1 subscriptions not clean', function (t) {
t.plan(7)
const broker = aedes()
t.teardown(broker.close.bind(broker))
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
dup: false,
length: 14,
retain: false
}
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.inStream.end()
const publisher = connect(setup(broker))
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
t.equal(connect.sessionPresent, true, 'session present is set to true')
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
})
publisher.outStream.once('data', function (packet) {
t.equal(packet.cmd, 'puback')
})
subscriber.outStream.once('data', function (packet) {
subscriber.inStream.write({
cmd: 'puback',
messageId: packet.messageId
})
t.not(packet.messageId, 42, 'messageId must differ')
delete packet.messageId
t.same(packet, expected, 'packet must match')
})
})
})
test('restore multiple QoS 1 subscriptions not clean w/ authorizeSubscribe', function (t) {
t.plan(11)
const broker = aedes()
t.teardown(broker.close.bind(broker))
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
const expected = {
cmd: 'publish',
topic: 'foo',
payload: Buffer.from('bar'),
qos: 1,
dup: false,
length: 10,
retain: false
}
const publisher = connect(setup(broker))
subscribe(t, subscriber, 'hello', 1, function () {
subscribe(t, subscriber, 'foo', 1, function () {
subscriber.inStream.end()
broker.authorizeSubscribe = function (client, sub, done) {
done(null, sub.topic === 'hello' ? 123 : sub)
}
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
t.equal(connect.sessionPresent, true, 'session present is set to true')
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
publisher.inStream.write({
cmd: 'publish',
topic: 'foo',
payload: 'bar',
qos: 1,
messageId: 48
})
})
publisher.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'puback')
})
subscriber.outStream.on('data', function (packet) {
subscriber.inStream.write({
cmd: 'puback',
messageId: packet.messageId
})
t.not(packet.messageId, 48, 'messageId must differ')
delete packet.messageId
t.same(packet, expected, 'packet must match')
})
})
})
})
test('remove stored subscriptions if connected with clean=true', function (t) {
t.plan(5)
const broker = aedes()
t.teardown(broker.close.bind(broker))
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.inStream.end()
const publisher = connect(setup(broker))
subscriber = connect(setup(broker), { clean: true, clientId: 'abcde' }, function (packet) {
t.equal(packet.sessionPresent, false, 'session present is set to false')
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
subscriber.inStream.end()
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
t.equal(connect.sessionPresent, false, 'session present is set to false')
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 43
})
})
subscriber.outStream.once('data', function (packet) {
t.fail('publish received')
})
})
subscriber.outStream.once('data', function (packet) {
t.fail('publish received')
})
})
})
test('resend publish on non-clean reconnect QoS 1', function (t) {
t.plan(8)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const opts = { clean: false, clientId: 'abcde' }
let subscriber = connect(setup(broker), opts)
const subscriberClient = {
id: opts.clientId
}
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
dup: false,
length: 14,
retain: false
}
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.inStream.end()
const publisher = connect(setup(broker))
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world world',
qos: 1,
messageId: 42
})
publisher.outStream.once('data', function (packet) {
t.equal(packet.cmd, 'puback')
subscriber = connect(setup(broker), opts)
subscriber.outStream.once('data', function (packet) {
subscriber.inStream.write({
cmd: 'puback',
messageId: packet.messageId
})
t.not(packet.messageId, 42, 'messageId must differ')
delete packet.messageId
t.same(packet, expected, 'packet must match')
setImmediate(() => {
const stream = broker.persistence.outgoingStream(subscriberClient)
stream.pipe(concat(function (list) {
t.equal(list.length, 1, 'should remain one item in queue')
t.same(list[0].payload, Buffer.from('world world'), 'packet must match')
}))
})
})
})
})
})
test('resend many publish on non-clean reconnect QoS 1', function (t) {
t.plan(4)
const broker = aedes()
const clock = Faketimers.createClock()
t.teardown(() => {
broker.close.bind(broker)
clock.reset.bind(clock)
})
const opts = { clean: false, clientId: 'abcde' }
let subscriber = connect(setup(broker), opts)
const publisher = connect(setup(broker))
const { through } = require('../lib/utils')
const total = through().writableHighWaterMark * 2
let received = 0
clock.setTimeout(() => {
broker.close()
t.equal(received, total)
}, total)
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.inStream.end()
for (let sent = 0; sent < total; sent++) {
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'message-' + sent,
qos: 1,
messageId: 42 + sent
})
}
publisher.outStream.once('data', function (packet) {
subscriber = connect(setup(broker), opts)
subscriber.outStream.on('data', function (packet) {
subscriber.inStream.write({
cmd: 'puback',
messageId: packet.messageId
})
received++
clock.tick(1)
})
})
})
})
test('do not resend QoS 1 packets at each reconnect', function (t) {
t.plan(6)
const broker = aedes()
t.teardown(broker.close.bind(broker))
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
dup: false,
length: 14,
retain: false
}
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.inStream.end()
const publisher = connect(setup(broker))
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
publisher.outStream.once('data', function (packet) {
t.equal(packet.cmd, 'puback')
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
subscriber.outStream.once('data', function (packet) {
subscriber.inStream.end({
cmd: 'puback',
messageId: packet.messageId
})
t.not(packet.messageId, 42, 'messageId must differ')
delete packet.messageId
t.same(packet, expected, 'packet must match')
const subscriber2 = connect(setup(broker), { clean: false, clientId: 'abcde' })
subscriber2.outStream.once('data', function (packet) {
t.fail('this should never happen')
})
})
})
})
})
test('do not resend QoS 1 packets if reconnect is clean', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.inStream.end()
const publisher = connect(setup(broker))
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
publisher.outStream.once('data', function (packet) {
t.equal(packet.cmd, 'puback')
subscriber = connect(setup(broker), { clean: true, clientId: 'abcde' })
subscriber.outStream.once('data', function (packet) {
t.fail('this should never happen')
})
})
})
})
test('do not resend QoS 1 packets at reconnect if puback was received', function (t) {
t.plan(5)
const broker = aedes()
t.teardown(broker.close.bind(broker))
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
dup: false,
length: 14,
retain: false
}
subscribe(t, subscriber, 'hello', 1, function () {
const publisher = connect(setup(broker))
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
publisher.outStream.once('data', function (packet) {
t.equal(packet.cmd, 'puback')
})
subscriber.outStream.once('data', function (packet) {
subscriber.inStream.end({
cmd: 'puback',
messageId: packet.messageId
})
delete packet.messageId
t.same(packet, expected, 'packet must match')
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
subscriber.outStream.once('data', function (packet) {
t.fail('this should never happen')
})
})
})
})
test('remove stored subscriptions after unsubscribe', function (t) {
t.plan(5)
const broker = aedes()
t.teardown(broker.close.bind(broker))
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.inStream.write({
cmd: 'unsubscribe',
messageId: 43,
unsubscriptions: ['hello']
})
subscriber.outStream.once('data', function (packet) {
t.same(packet, {
cmd: 'unsuback',
messageId: 43,
dup: false,
length: 2,
qos: 0,
retain: false
}, 'packet matches')
subscriber.inStream.end()
const publisher = connect(setup(broker))
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (packet) {
t.equal(packet.sessionPresent, false, 'session present is set to false')
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 43
}, function () {
subscriber.inStream.end()
})
subscriber.outStream.once('data', function (packet) {
t.fail('publish received')
})
})
subscriber.outStream.once('data', function (packet) {
t.fail('publish received')
})
})
})
})
test('upgrade a QoS 0 subscription to QoS 1', function (t) {
t.plan(8)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
length: 14,
retain: false,
dup: false
}
subscribe(t, s, 'hello', 0, function () {
subscribe(t, s, 'hello', 1, function () {
s.outStream.once('data', function (packet) {
t.ok(packet.messageId, 'has messageId')
delete packet.messageId
t.same(packet, expected, 'packet matches')
})
s.broker.publish({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1
})
})
})
})
test('downgrade QoS 0 publish on QoS 1 subsciption', function (t) {
t.plan(4)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
length: 12,
retain: false,
dup: false
}
subscribe(t, s, 'hello', 1, function () {
s.outStream.once('data', function (packet) {
t.same(packet, expected, 'packet matches')
})
s.broker.publish({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 0
})
})
})
test('subscribe and publish QoS 1 in parallel', function (t) {
t.plan(5)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = connect(setup(broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
dup: false,
length: 14,
retain: false
}
broker.on('clientError', function (client, err) {
console.log(err.stack)
// t.fail('no client error')
})
s.outStream.once('data', function (packet) {
t.equal(packet.cmd, 'puback')
t.equal(packet.messageId, 42, 'messageId must match')
s.outStream.on('data', function (packet) {
if (packet.cmd === 'suback') {
t.same(packet.granted, [1])
t.equal(packet.messageId, 24)
}
if (packet.cmd === 'publish') {
s.inStream.write({
cmd: 'puback',
messageId: packet.messageId
})
delete packet.messageId
t.same(packet, expected, 'packet must match')
}
})
})
s.inStream.write({
cmd: 'subscribe',
messageId: 24,
subscriptions: [{
topic: 'hello',
qos: 1
}]
})
s.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42
})
})
+686
View File
@@ -0,0 +1,686 @@
'use strict'
const { test } = require('tap')
const concat = require('concat-stream')
const { setup, connect, subscribe } = require('./helper')
const aedes = require('../')
function publish (t, s, packet, done) {
const msgId = packet.messageId
s.inStream.write(packet)
s.outStream.once('data', function (packet) {
t.same(packet, {
cmd: 'pubrec',
messageId: msgId,
length: 2,
dup: false,
retain: false,
qos: 0
}, 'pubrec must match')
s.inStream.write({
cmd: 'pubrel',
messageId: msgId
})
s.outStream.once('data', function (packet) {
t.same(packet, {
cmd: 'pubcomp',
messageId: msgId,
length: 2,
dup: false,
retain: false,
qos: 0
}, 'pubcomp must match')
if (done) {
done()
}
})
})
}
function receive (t, subscriber, expected, done) {
subscriber.outStream.once('data', function (packet) {
t.not(packet.messageId, expected.messageId, 'messageId must differ')
const msgId = packet.messageId
delete packet.messageId
delete expected.messageId
t.same(packet, expected, 'packet must match')
subscriber.inStream.write({
cmd: 'pubrec',
messageId: msgId
})
subscriber.outStream.once('data', function (packet) {
subscriber.inStream.write({
cmd: 'pubcomp',
messageId: msgId
})
t.same(packet, {
cmd: 'pubrel',
messageId: msgId,
length: 2,
qos: 1,
retain: false,
dup: false
}, 'pubrel must match')
if (done) {
done()
}
})
})
}
test('publish QoS 2', function (t) {
t.plan(2)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
const packet = {
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 2,
messageId: 42
}
publish(t, s, packet)
})
test('subscribe QoS 2', function (t) {
t.plan(8)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker))
const subscriber = connect(setup(broker))
const toPublish = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
messageId: 42,
dup: false,
length: 14,
retain: false
}
subscribe(t, subscriber, 'hello', 2, function () {
publish(t, publisher, toPublish)
receive(t, subscriber, toPublish)
})
})
test('publish QoS 2 throws error on write', function (t) {
t.plan(1)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
s.broker.on('client', function (client) {
client.connected = false
client.connecting = false
s.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 2,
messageId: 42
})
})
s.broker.on('clientError', function (client, err) {
t.equal(err.message, 'connection closed', 'throws error')
})
})
test('pubrec handler calls done when outgoingUpdate fails (clean=false)', function (t) {
t.plan(1)
const s = connect(setup(), { clean: false })
t.teardown(s.broker.close.bind(s.broker))
const handle = require('../lib/handlers/pubrec.js')
s.broker.persistence.outgoingUpdate = function (client, pubrel, done) {
done(Error('throws error'))
}
handle(s.client, { messageId: 42 }, function done () {
t.pass('calls done on error')
})
})
test('client.publish with clean=true subscribption QoS 2', function (t) {
t.plan(8)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const toPublish = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
messageId: 42,
dup: false,
length: 14,
retain: false
}
let brokerClient = null
broker.on('client', function (client) {
brokerClient = client
brokerClient.on('error', function (err) {
t.error(err)
})
})
const subscriber = connect(setup(broker), { clean: true })
subscribe(t, subscriber, 'hello', 2, function () {
t.pass('subscribed')
receive(t, subscriber, toPublish)
brokerClient.publish(toPublish, function (err) {
t.error(err)
})
})
})
test('call published method with client with QoS 2', function (t) {
t.plan(9)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker))
const subscriber = connect(setup(broker))
const toPublish = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
messageId: 42,
dup: false,
length: 14,
retain: false
}
broker.published = function (packet, client, cb) {
// Client is null for all server publishes
if (packet.topic.split('/')[0] !== '$SYS') {
t.ok(client, 'client must be passed to published method')
cb()
}
}
subscribe(t, subscriber, 'hello', 2, function () {
publish(t, publisher, toPublish)
receive(t, subscriber, toPublish)
})
})
;[true, false].forEach(function (cleanSession) {
test(`authorized forward publish packets in QoS 2 [clean=${cleanSession}]`, function (t) {
t.plan(9)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const opts = { clean: cleanSession }
const publisher = connect(setup(broker), { clientId: 'my-client-xyz-8' })
const subscriber = connect(setup(broker), { ...opts, clientId: 'abcde' })
const forwarded = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
retain: false,
dup: false,
messageId: undefined,
clientId: 'my-client-xyz-8'
}
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
retain: false,
length: 14,
dup: false
}
broker.authorizeForward = function (client, packet) {
forwarded.brokerId = broker.id
forwarded.brokerCounter = broker.counter
delete packet.nl
t.same(packet, forwarded, 'forwarded packet must match')
return packet
}
subscribe(t, subscriber, 'hello', 2, function () {
subscriber.outStream.once('data', function (packet) {
t.not(packet.messageId, 42)
delete packet.messageId
t.same(packet, expected, 'packet must match')
})
publish(t, publisher, {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
retain: false,
messageId: 42,
dup: false
}, function () {
const stream = broker.persistence.outgoingStream({ id: 'abcde' })
stream.pipe(concat(function (list) {
if (cleanSession) {
t.equal(list.length, 0, 'should have empty item in queue')
} else {
t.equal(list.length, 1, 'should have one item in queue')
}
}))
})
})
})
})
;[true, false].forEach(function (cleanSession) {
test(`unauthorized forward publish packets in QoS 2 [clean=${cleanSession}]`, function (t) {
t.plan(6)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const opts = { clean: cleanSession }
const publisher = connect(setup(broker))
const subscriber = connect(setup(broker), { ...opts, clientId: 'abcde' })
broker.authorizeForward = function (client, packet) {
}
subscribe(t, subscriber, 'hello', 2, function () {
subscriber.outStream.once('data', function (packet) {
t.fail('should not receive any packets')
})
publish(t, publisher, {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
retain: false,
messageId: 42,
dup: false
}, function () {
const stream = broker.persistence.outgoingStream({ id: 'abcde' })
stream.pipe(concat(function (list) {
t.equal(list.length, 0, 'should empty in queue')
}))
})
})
})
})
test('subscribe QoS 0, but publish QoS 2', function (t) {
t.plan(6)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker))
const subscriber = connect(setup(broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
dup: false,
length: 12,
retain: false
}
subscribe(t, subscriber, 'hello', 0, function () {
subscriber.outStream.once('data', function (packet) {
t.same(packet, expected, 'packet must match')
})
publish(t, publisher, {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
retain: false,
messageId: 42,
dup: false
})
})
})
test('subscribe QoS 1, but publish QoS 2', function (t) {
t.plan(6)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker))
const subscriber = connect(setup(broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
dup: false,
length: 14,
retain: false
}
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.outStream.once('data', function (packet) {
delete packet.messageId
t.same(packet, expected, 'packet must match')
})
publish(t, publisher, {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
retain: false,
messageId: 42,
dup: false
})
})
})
test('restore QoS 2 subscriptions not clean', function (t) {
t.plan(9)
const broker = aedes()
t.teardown(broker.close.bind(broker))
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
dup: false,
length: 14,
messageId: 42,
retain: false
}
subscribe(t, subscriber, 'hello', 2, function () {
subscriber.inStream.end()
const publisher = connect(setup(broker))
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
t.equal(connect.sessionPresent, true, 'session present is set to true')
publish(t, publisher, expected)
})
receive(t, subscriber, expected)
})
})
test('resend publish on non-clean reconnect QoS 2', function (t) {
t.plan(8)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const opts = { clean: false, clientId: 'abcde' }
let subscriber = connect(setup(broker), opts)
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
dup: false,
length: 14,
messageId: 42,
retain: false
}
subscribe(t, subscriber, 'hello', 2, function () {
subscriber.inStream.end()
const publisher = connect(setup(broker))
publish(t, publisher, expected, function () {
subscriber = connect(setup(broker), opts)
receive(t, subscriber, expected)
})
})
})
test('resend pubrel on non-clean reconnect QoS 2', function (t) {
t.plan(9)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const opts = { clean: false, clientId: 'abcde' }
let subscriber = connect(setup(broker), opts)
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
dup: false,
length: 14,
messageId: 42,
retain: false
}
subscribe(t, subscriber, 'hello', 2, function () {
subscriber.inStream.end()
const publisher = connect(setup(broker))
publish(t, publisher, expected, function () {
subscriber = connect(setup(broker), opts)
subscriber.outStream.once('data', function (packet) {
t.not(packet.messageId, expected.messageId, 'messageId must differ')
const msgId = packet.messageId
delete packet.messageId
delete expected.messageId
t.same(packet, expected, 'packet must match')
subscriber.inStream.write({
cmd: 'pubrec',
messageId: msgId
})
subscriber.outStream.once('data', function (packet) {
t.same(packet, {
cmd: 'pubrel',
messageId: msgId,
length: 2,
qos: 1,
retain: false,
dup: false
}, 'pubrel must match')
subscriber.inStream.end()
subscriber = connect(setup(broker), opts)
subscriber.outStream.once('data', function (packet) {
t.same(packet, {
cmd: 'pubrel',
messageId: msgId,
length: 2,
qos: 1,
retain: false,
dup: false
}, 'pubrel must match')
subscriber.inStream.write({
cmd: 'pubcomp',
messageId: msgId
})
})
})
})
})
})
})
test('publish after disconnection', function (t) {
t.plan(10)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker))
const subscriber = connect(setup(broker))
const toPublish = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
messageId: 42,
dup: false,
length: 14,
retain: false
}
const toPublish2 = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('worl2'),
qos: 2,
messageId: 43,
dup: false,
length: 14,
retain: false
}
subscribe(t, subscriber, 'hello', 2, function () {
publish(t, publisher, toPublish)
receive(t, subscriber, toPublish, function () {
publish(t, publisher, toPublish2)
})
})
})
test('multiple publish and store one', function (t) {
t.plan(2)
const broker = aedes()
const sid = {
id: 'abcde'
}
const s = connect(setup(broker), { clientId: sid.id })
const toPublish = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 2,
retain: false,
dup: false,
messageId: 42
}
let count = 5
while (count--) {
s.inStream.write(toPublish)
}
let recvcnt = 0
s.outStream.on('data', function (packet) {
if (++recvcnt < 5) return
broker.close(function () {
broker.persistence.incomingGetPacket(sid, toPublish, function (err, origPacket) {
delete origPacket.brokerId
delete origPacket.brokerCounter
t.same(origPacket, toPublish, 'packet must match')
t.error(err)
})
})
})
})
test('packet is written to stream after being stored', function (t) {
const s = connect(setup())
const broker = s.broker
t.teardown(broker.close.bind(s.broker))
let packetStored = false
const fn = broker.persistence.incomingStorePacket.bind(broker.persistence)
s.broker.persistence.incomingStorePacket = function (client, packet, done) {
packetStored = true
t.pass('packet stored')
fn(client, packet, done)
}
const packet = {
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 2,
messageId: 42
}
publish(t, s, packet)
s.outStream.once('data', function (packet) {
t.equal(packet.cmd, 'pubrec', 'pubrec received')
t.equal(packetStored, true, 'after packet store')
t.end()
})
})
test('not send pubrec when persistence fails to store packet', function (t) {
t.plan(2)
const s = connect(setup())
const broker = s.broker
t.teardown(broker.close.bind(s.broker))
s.broker.persistence.incomingStorePacket = function (client, packet, done) {
t.pass('packet stored')
done(new Error('store error'))
}
s.broker.on('clientError', function (client, err) {
t.equal(err.message, 'store error')
})
const packet = {
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 2,
messageId: 42
}
s.inStream.write(packet)
s.outStream.once('data', function (packet) {
t.fail('should not have pubrec')
})
})
+34
View File
@@ -0,0 +1,34 @@
'use strict'
const { test } = require('tap')
const { setup, connect } = require('./helper')
test('after an error, outstanding packets are discarded', function (t) {
t.plan(1)
const s = connect(setup(), {
keepalive: 1000
})
t.teardown(s.broker.close.bind(s.broker))
const packet = {
cmd: 'publish',
topic: 'hello',
payload: 'world'
}
s.broker.mq.on('hello', function (msg, cb) {
t.pass('first msg received')
s.inStream.destroy(new Error('something went wrong'))
cb()
setImmediate(() => {
packet.topic = 'foo'
s.inStream.write(packet)
s.inStream.write(packet)
})
})
s.broker.mq.on('foo', function (msg, cb) {
t.fail('msg received')
})
s.inStream.write(packet)
})
+713
View File
@@ -0,0 +1,713 @@
'use strict'
const { test } = require('tap')
const { through } = require('../lib/utils')
const Faketimers = require('@sinonjs/fake-timers')
const { setup, connect, subscribe, noError } = require('./helper')
const aedes = require('../')
// [MQTT-3.3.1-9]
test('live retain packets', function (t) {
t.plan(5)
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
retain: false,
dup: false,
length: 12,
qos: 0
}
const s = noError(connect(setup()), t)
t.teardown(s.broker.close.bind(s.broker))
subscribe(t, s, 'hello', 0, function () {
s.outStream.on('data', function (packet) {
t.same(packet, expected)
})
s.broker.publish({
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
retain: true,
dup: false,
length: 12,
qos: 0
}, function () {
t.pass('publish finished')
})
})
})
test('retain messages', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker))
const subscriber = connect(setup(broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
dup: false,
length: 12,
retain: true
}
broker.subscribe('hello', function (packet, cb) {
cb()
// defer this or it will receive the message which
// is being published
setImmediate(function () {
subscribe(t, subscriber, 'hello', 0, function () {
subscriber.outStream.once('data', function (packet) {
t.same(packet, expected, 'packet must match')
})
})
})
})
publisher.inStream.write(expected)
})
test('retain messages propagates through broker subscriptions', function (t) {
t.plan(1)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
dup: false,
retain: true
}
const subscriberFunc = function (packet, cb) {
packet = Object.assign({}, packet)
delete packet.brokerId
delete packet.brokerCounter
cb()
setImmediate(function () {
t.same(packet, expected, 'packet must not have been modified')
})
}
broker.subscribe('hello', subscriberFunc, function () {
broker.publish(expected)
})
})
test('avoid wrong deduping of retain messages', function (t) {
t.plan(7)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker))
const subscriber = connect(setup(broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
dup: false,
length: 12,
retain: true
}
broker.subscribe('hello', function (packet, cb) {
cb()
// subscribe and publish another topic
subscribe(t, subscriber, 'hello2', 0, function () {
cb()
publisher.inStream.write({
cmd: 'publish',
topic: 'hello2',
payload: Buffer.from('world'),
qos: 0,
dup: false
})
subscriber.outStream.once('data', function (packet) {
subscribe(t, subscriber, 'hello', 0, function () {
subscriber.outStream.once('data', function (packet) {
t.same(packet, expected, 'packet must match')
})
})
})
})
})
publisher.inStream.write(expected)
})
test('reconnected subscriber will not receive retained messages when QoS 0 and clean', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker), { clean: true })
let subscriber = connect(setup(broker), { clean: true })
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
retain: false,
dup: false,
length: 12
}
subscribe(t, subscriber, 'hello', 0, function () {
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 0,
retain: false
})
subscriber.outStream.once('data', function (packet) {
t.same(packet, expected, 'packet must match')
subscriber.inStream.end()
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'foo',
qos: 0,
retain: true
})
subscriber = connect(setup(broker), { clean: true })
subscriber.outStream.on('data', function (packet) {
t.fail('should not received retain message')
})
})
})
})
test('subscriber will not receive retained messages when QoS is 128', function (t) {
t.plan(3)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const pubPacket = {
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
retain: true,
messageId: 42
}
broker.authorizeSubscribe = function (client, sub, callback) {
if (sub.topic === pubPacket.topic) {
callback(null, null)
} else {
callback(null, sub)
}
}
const publisher = connect(setup(broker), { clean: true })
publisher.inStream.write(pubPacket)
publisher.outStream.on('data', function (packet) {
const subscriber = connect(setup(broker), { clean: true })
subscribe(t, subscriber, pubPacket.topic, 128, function () {
subscriber.outStream.on('data', function (packet) {
t.fail('should not received retain message')
})
})
})
})
// [MQTT-3.3.1-6]
test('new QoS 0 subscribers receive QoS 0 retained messages when clean', function (t) {
t.plan(9)
const clock = Faketimers.createClock()
const broker = aedes()
t.teardown(function () {
clock.reset()
broker.close()
})
const publisher = connect(setup(broker), { clean: true })
const expected = {
cmd: 'publish',
topic: 'hello/world',
payload: Buffer.from('big big world'),
qos: 0,
retain: true,
dup: false,
length: 26
}
publisher.inStream.write({
cmd: 'publish',
topic: 'hello/world',
payload: 'big big world',
qos: 0,
retain: true
})
const subscriber1 = connect(setup(broker), { clean: true })
subscribe(t, subscriber1, 'hello/world', 0, function () {
subscriber1.outStream.on('data', function (packet) {
t.same(packet, expected, 'packet must match')
clock.tick(100)
})
})
const subscriber2 = connect(setup(broker), { clean: true })
subscribe(t, subscriber2, 'hello/+', 0, function () {
subscriber2.outStream.on('data', function (packet) {
t.same(packet, expected, 'packet must match')
clock.tick(100)
})
})
clock.setTimeout(() => {
t.equal(broker.counter, 9)
}, 200)
})
// [MQTT-3.3.1-5]
test('new QoS 0 subscribers receive downgraded QoS 1 retained messages when clean', function (t) {
t.plan(6)
const broker = aedes()
const publisher = connect(setup(broker), { clean: true })
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
retain: true,
dup: false,
length: 12
}
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
retain: true,
messageId: 42
})
publisher.outStream.on('data', function (packet) {
const subscriber = connect(setup(broker), { clean: true })
subscribe(t, subscriber, 'hello', 0, function () {
subscriber.outStream.on('data', function (packet) {
t.not(packet.messageId, 42, 'messageId should not be the same')
delete packet.messageId
t.same(packet, expected, 'packet must match')
broker.close()
})
})
})
broker.on('closed', function () {
t.equal(broker.counter, 8)
})
})
// [MQTT-3.3.1-10]
test('clean retained messages', function (t) {
t.plan(3)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker), { clean: true })
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 0,
retain: true
})
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: '',
qos: 0,
retain: true
})
const subscriber = connect(setup(broker), { clean: true })
subscribe(t, subscriber, 'hello', 0, function () {
subscriber.outStream.once('data', function (packet) {
t.fail('should not received retain message')
})
})
})
// [MQTT-3.3.1-11]
test('broker not store zero-byte retained messages', function (t) {
t.plan(0)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = connect(setup(broker))
s.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: '',
retain: true
})
s.broker.on('publish', function (packet, client) {
if (packet.topic.startsWith('$SYS/')) {
return
}
const stream = s.broker.persistence.createRetainedStream(packet.topic)
stream.pipe(through(function sendRetained (packet, enc, cb) {
t.fail('not store zero-byte retained messages')
}))
})
})
test('fail to clean retained messages without retain flag', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker), { clean: true })
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
retain: true,
dup: false,
length: 12
}
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 0,
retain: true
})
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: '',
qos: 0,
retain: false
})
const subscriber = connect(setup(broker), { clean: true })
subscribe(t, subscriber, 'hello', 0, function () {
subscriber.outStream.on('data', function (packet) {
t.same(packet, expected, 'packet must match')
})
})
})
test('only get the last retained messages in same topic', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker), { clean: true })
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('foo'),
qos: 0,
retain: true,
dup: false,
length: 10
}
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 0,
retain: true
})
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'foo',
qos: 0,
retain: true
})
const subscriber = connect(setup(broker), { clean: true })
subscribe(t, subscriber, 'hello', 0, function () {
subscriber.outStream.on('data', function (packet) {
t.same(packet, expected, 'packet must match')
})
})
})
test('deliver QoS 1 retained messages to new subscriptions', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker))
const subscriber = connect(setup(broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
dup: false,
length: 14,
retain: true
}
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42,
retain: true
})
publisher.outStream.on('data', function (packet) {
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.outStream.once('data', function (packet) {
delete packet.messageId
t.same(packet, expected, 'packet must match')
})
})
})
})
test('deliver QoS 1 retained messages to established subscriptions', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker))
const subscriber = connect(setup(broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
dup: false,
length: 14,
retain: false
}
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.outStream.once('data', function (packet) {
delete packet.messageId
t.same(packet, expected, 'packet must match')
})
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42,
retain: true
})
})
})
test('deliver QoS 0 retained message with QoS 1 subscription', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker))
const subscriber = connect(setup(broker))
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
dup: false,
length: 12,
retain: true
}
broker.mq.on('hello', function (msg, cb) {
cb()
// defer this or it will receive the message which
// is being published
setImmediate(function () {
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.outStream.once('data', function (packet) {
t.same(packet, expected, 'packet must match')
})
})
})
})
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
messageId: 42,
retain: true
})
})
test('disconnect and retain messages with QoS 1 [clean=false]', function (t) {
t.plan(7)
const broker = aedes()
t.teardown(broker.close.bind(broker))
let subscriber = noError(connect(setup(broker), { clean: false, clientId: 'abcde' }), t)
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
dup: false,
length: 14,
retain: true
}
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.inStream.write({
cmd: 'disconnect'
})
subscriber.outStream.on('data', function (packet) {
console.log('original', packet)
})
const publisher = noError(connect(setup(broker)), t)
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 42,
retain: true
})
publisher.outStream.once('data', function (packet) {
t.equal(packet.cmd, 'puback')
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
t.equal(connect.sessionPresent, true, 'session present is set to true')
})
subscriber.outStream.once('data', function (packet) {
// receive any queued messages (no matter they are retained messages) at the disconnected time
t.not(packet.messageId, 42, 'messageId must differ')
delete packet.messageId
packet.length = 14
t.same(packet, expected, 'packet must match')
// there should be no messages come from restored subscriptions
subscriber.outStream.once('data', function (packet) {
t.fail('should not receive any more messages')
})
})
})
})
})
test('disconnect and two retain messages with QoS 1 [clean=false]', function (t) {
t.plan(15)
const broker = aedes()
t.teardown(broker.close.bind(broker))
let subscriber = noError(connect(setup(broker), { clean: false, clientId: 'abcde' }), t)
const expected = {
cmd: 'publish',
topic: 'hello',
qos: 1,
dup: false,
length: 14,
retain: true
}
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.inStream.write({
cmd: 'disconnect'
})
subscriber.outStream.on('data', function (packet) {
console.log('original', packet)
})
const publisher = noError(connect(setup(broker)), t)
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world',
qos: 1,
messageId: 41,
retain: true
})
publisher.outStream.once('data', function (packet) {
t.equal(packet.cmd, 'puback')
publisher.inStream.write({
cmd: 'publish',
topic: 'hello',
payload: 'world2',
qos: 1,
messageId: 42,
retain: true
})
publisher.outStream.once('data', function (packet) {
t.equal(packet.cmd, 'puback')
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
t.equal(connect.sessionPresent, true, 'session present is set to true')
})
subscriber.outStream.once('data', function (packet) {
// receive any queued messages (included retained messages) at the disconnected time
t.not(packet.messageId, 41, 'messageId must differ')
delete packet.messageId
packet.length = 14
expected.payload = Buffer.from('world')
t.same(packet, expected, 'packet must match')
// receive any queued messages (included retained messages) at the disconnected time
subscriber.outStream.once('data', function (packet) {
t.not(packet.messageId, 42, 'messageId must differ')
delete packet.messageId
packet.length = 14
expected.payload = Buffer.from('world2')
t.same(packet, expected, 'packet must match')
// should get the last retained message when we do a subscribe
subscribe(t, subscriber, 'hello', 1, function () {
subscriber.outStream.on('data', function (packet) {
t.not(packet.messageId, 42, 'messageId must differ')
delete packet.messageId
packet.length = 14
expected.payload = Buffer.from('world2')
t.same(packet, expected, 'packet must match')
})
})
})
})
})
})
})
})
+302
View File
@@ -0,0 +1,302 @@
'use strict'
const { test } = require('tap')
const { setup, connect, subscribe } = require('./helper')
const aedes = require('../')
const { validateTopic } = require('../lib/utils')
test('validation of `null` topic', function (t) {
// issue #780
t.plan(1)
const err = validateTopic(null, 'SUBSCRIBE')
t.equal(err.message, 'impossible to SUBSCRIBE to an empty topic')
})
// [MQTT-4.7.1-3]
test('Single-level wildcard should match empty level', function (t) {
t.plan(4)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
subscribe(t, s, 'a/+/b', 0, function () {
s.outStream.once('data', function (packet) {
t.pass('ok')
})
s.inStream.write({
cmd: 'publish',
topic: 'a//b',
payload: 'world'
})
})
})
// [MQTT-4.7.3-1]
test('publish empty topic', function (t) {
t.plan(4)
const s = connect(setup())
subscribe(t, s, '#', 0, function () {
s.outStream.once('data', function (packet) {
t.fail('no packet')
})
s.inStream.write({
cmd: 'publish',
topic: '',
payload: 'world'
})
s.broker.close(function () {
t.equal(s.broker.connectedClients, 0, 'no connected clients')
})
})
})
test('publish invalid topic with #', function (t) {
t.plan(4)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
subscribe(t, s, '#', 0, function () {
s.outStream.once('data', function (packet) {
t.fail('no packet')
})
s.inStream.write({
cmd: 'publish',
topic: 'hello/#',
payload: 'world'
})
})
s.broker.on('clientError', function () {
t.pass('raise an error')
})
})
test('publish invalid topic with +', function (t) {
t.plan(4)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
subscribe(t, s, '#', 0, function () {
s.outStream.once('data', function (packet) {
t.fail('no packet')
})
s.inStream.write({
cmd: 'publish',
topic: 'hello/+/eee',
payload: 'world'
})
})
s.broker.on('clientError', function () {
t.pass('raise an error')
})
})
;['base/#/sub', 'base/#sub', 'base/sub#', 'base/xyz+/sub', 'base/+xyz/sub', ''].forEach(function (topic) {
test('subscribe to invalid topic with "' + topic + '"', function (t) {
t.plan(1)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
s.broker.on('clientError', function () {
t.pass('raise an error')
})
s.inStream.write({
cmd: 'subscribe',
messageId: 24,
subscriptions: [{
topic,
qos: 0
}]
})
})
test('unsubscribe to invalid topic with "' + topic + '"', function (t) {
t.plan(1)
const s = connect(setup())
t.teardown(s.broker.close.bind(s.broker))
s.broker.on('clientError', function () {
t.pass('raise an error')
})
s.inStream.write({
cmd: 'unsubscribe',
messageId: 24,
unsubscriptions: [topic]
})
})
})
test('topics are case-sensitive', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const publisher = connect(setup(broker), { clean: true })
const subscriber = connect(setup(broker), { clean: true })
const expected = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
dup: false,
length: 12,
retain: false
}
subscribe(t, subscriber, 'hello', 0, function () {
subscriber.outStream.on('data', function (packet) {
t.same(packet, expected, 'packet mush match')
})
;['hello', 'HELLO', 'heLLo', 'HELLO/#', 'hello/+'].forEach(function (topic) {
publisher.inStream.write({
cmd: 'publish',
topic,
payload: 'world',
qos: 0,
retain: false
})
})
})
})
function subscribeMultipleTopics (t, broker, qos, subscriber, subscriptions, done) {
const publisher = connect(setup(broker))
subscriber.inStream.write({
cmd: 'subscribe',
messageId: 24,
subscriptions
})
subscriber.outStream.once('data', function (packet) {
t.equal(packet.cmd, 'suback')
t.same(packet.granted, subscriptions.map(obj => obj.qos))
t.equal(packet.messageId, 24)
publisher.inStream.write({
cmd: 'publish',
topic: 'hello/world',
payload: 'world',
qos,
messageId: 42
})
if (done) {
done(null, packet)
}
})
}
test('Overlapped topics with same QoS', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const subscriber = connect(setup(broker))
const expected = {
cmd: 'publish',
topic: 'hello/world',
payload: Buffer.from('world'),
qos: 1,
dup: false,
length: 20,
retain: false
}
const sub = [
{ topic: 'hello/world', qos: 1 },
{ topic: 'hello/#', qos: 1 }]
subscribeMultipleTopics(t, broker, 1, subscriber, sub, function () {
subscriber.outStream.on('data', function (packet) {
delete packet.messageId
t.same(packet, expected, 'packet must match')
})
})
})
// [MQTT-3.3.5-1]
test('deliver overlapped topics respecting the maximum QoS of all the matching subscriptions - QoS 0 publish', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const subscriber = connect(setup(broker))
const expected = {
cmd: 'publish',
topic: 'hello/world',
payload: Buffer.from('world'),
qos: 0,
dup: false,
length: 18,
retain: false
}
const sub = [
{ topic: 'hello/world', qos: 0 },
{ topic: 'hello/#', qos: 2 }]
subscribeMultipleTopics(t, broker, 0, subscriber, sub, function () {
subscriber.outStream.on('data', function (packet) {
delete packet.messageId
t.same(packet, expected, 'packet must match')
})
})
})
// [MQTT-3.3.5-1]
test('deliver overlapped topics respecting the maximum QoS of all the matching subscriptions - QoS 2 publish', function (t) {
t.plan(3)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const subscriber = connect(setup(broker))
const sub = [
{ topic: 'hello/world', qos: 0 },
{ topic: 'hello/#', qos: 2 }]
subscribeMultipleTopics(t, broker, 2, subscriber, sub, function () {
subscriber.outStream.on('data', function () {
t.fail('should receive messages with the maximum QoS')
})
})
})
test('Overlapped topics with QoS downgrade', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const subscriber = connect(setup(broker))
const expected = {
cmd: 'publish',
topic: 'hello/world',
payload: Buffer.from('world'),
qos: 0,
dup: false,
length: 18,
retain: false
}
const sub = [
{ topic: 'hello/world', qos: 1 },
{ topic: 'hello/#', qos: 1 }]
subscribeMultipleTopics(t, broker, 0, subscriber, sub, function () {
subscriber.outStream.on('data', function (packet) {
t.same(packet, expected, 'packet must match')
})
})
})
+224
View File
@@ -0,0 +1,224 @@
import { IncomingMessage } from 'node:http'
import { Socket } from 'node:net'
import type {
Brokers,
AuthenticateError,
Client,
Connection
} from '../../aedes'
import Aedes, { AedesOptions, createBroker } from '../../aedes'
import type { AedesPublishPacket, ConnackPacket, ConnectPacket, PingreqPacket, PublishPacket, PubrelPacket, Subscription, SubscribePacket, UnsubscribePacket } from '../../types/packet'
import { expectType } from 'tsd'
// Test for createBroker function
expectType<(options?: AedesOptions) => Aedes>(createBroker)
// Aedes server
let broker = createBroker()
expectType<Aedes>(broker)
broker = new Aedes({
id: 'aedes',
concurrency: 100,
heartbeatInterval: 60000,
connectTimeout: 30000,
maxClientsIdLength: 23,
keepaliveLimit: 0,
preConnect: (client: Client, packet: ConnectPacket, callback) => {
if (client.req) {
callback(new Error('not websocket stream'), false)
}
if (client.conn instanceof Socket && client.conn.remoteAddress === '::1') {
callback(null, true)
} else {
callback(new Error('connection error'), false)
}
},
authenticate: (
client: Client,
username: Readonly<string | undefined>,
password: Readonly<Buffer | undefined>,
callback
) => {
if (
username === 'test' &&
password === Buffer.from('test') &&
client.version === 4
) {
callback(null, true)
} else {
const error = new Error() as AuthenticateError
error.returnCode = 1
callback(error, false)
}
},
authorizePublish: (
client: Client | null,
packet: PublishPacket,
callback
) => {
if (packet.topic === 'aaaa') {
return callback(new Error('wrong topic'))
}
if (packet.topic === 'bbb') {
packet.payload = Buffer.from('overwrite packet payload')
}
callback(null)
},
authorizeSubscribe: (client: Client, sub: Subscription, callback) => {
if (sub.topic === 'aaaa') {
return callback(new Error('wrong topic'))
}
if (sub.topic === 'bbb') {
// overwrites subscription
sub.qos = 2
}
callback(null, sub)
},
authorizeForward: (client: Client, packet: AedesPublishPacket) => {
if (packet.topic === 'aaaa' && client.id === 'I should not see this') {
return null
// also works with return undefined
} else if (
packet.topic === 'aaaa' &&
client.id === 'I should not see this either'
) {
return
}
if (packet.topic === 'bbb') {
packet.payload = Buffer.from('overwrite packet payload')
}
return packet
},
published: (packet: AedesPublishPacket, client: Client, callback) => {
callback(null)
callback(new Error())
}
})
expectType<Aedes>(broker)
expectType<Readonly<Brokers>>(broker.brokers)
expectType<Aedes>(broker.on('closed', () => {}))
expectType<Aedes>(broker.on('client', (client: Client) => {}))
expectType<Aedes>(broker.on('clientReady', (client: Client) => {}))
expectType<Aedes>(broker.on('clientDisconnect', (client: Client) => {}))
expectType<Aedes>(broker.on('keepaliveTimeout', (client: Client) => {}))
expectType<Aedes>(
broker.on('clientError', (client: Client, error: Error) => {})
)
expectType<Aedes>(
broker.on('connectionError', (client: Client, error: Error) => {})
)
expectType<Aedes>(
broker.on('connackSent', (packet: ConnackPacket, client: Client) => {})
)
expectType<Aedes>(
broker.on('ping', (packet: PingreqPacket, client: Client) => {})
)
expectType<Aedes>(
broker.on(
'publish',
(packet: AedesPublishPacket, client: Client | null) => {}
)
)
expectType<Aedes>(
broker.on('ack', (packet: PublishPacket | PubrelPacket, client: Client) => {})
)
expectType<Aedes>(
broker.on('subscribe', (subscriptions: Subscription[], client: Client) => {})
)
expectType<Aedes>(
broker.on('unsubscribe', (unsubscriptions: string[], client: Client) => {})
)
expectType<void>(
broker.publish({} as PublishPacket, (error?: Error) => {
if (error) {
console.error(error)
}
})
)
expectType<void>(
broker.subscribe(
'topic',
(packet: AedesPublishPacket, callback: () => void) => {},
() => {}
)
)
expectType<void>(
broker.unsubscribe(
'topic',
(packet: AedesPublishPacket, callback: () => void) => {},
() => {}
)
)
expectType<void>(broker.close())
expectType<void>(broker.close(() => {}))
// Aedes client
const client = broker.handle({} as Connection, {} as IncomingMessage)
const client2 = broker.handle({} as Connection)
expectType<Client>(client)
expectType<Client>(client2)
expectType<Connection>(client.conn)
expectType<IncomingMessage>(client.req!)
expectType<Client>(client.on('connected', () => {}))
expectType<Client>(
client.on('error', (error: Error) => {
if (error) {
console.error(error)
}
})
)
expectType<void>(
client.publish({} as PublishPacket, (error?: Error) => {
if (error) {
console.error(error)
}
})
)
expectType<void>(client.publish({} as PublishPacket))
expectType<void>(
client.subscribe({} as Subscription, (error?: Error) => {
if (error) {
console.error(error)
}
})
)
expectType<void>(client.subscribe({} as Subscription))
expectType<void>(client.subscribe([] as Subscription[]))
expectType<void>(client.subscribe({} as SubscribePacket))
expectType<void>(
client.unsubscribe({} as Subscription, (error?: Error) => {
if (error) {
console.error(error)
}
})
)
expectType<void>(client.unsubscribe({} as Subscription))
expectType<void>(client.unsubscribe([] as Subscription[]))
expectType<void>(client.unsubscribe({} as UnsubscribePacket))
expectType<void>(client.emptyOutgoingQueue())
expectType<void>(client.emptyOutgoingQueue(() => {}))
expectType<void>(client.close())
expectType<void>(client.close(() => {}))
+601
View File
@@ -0,0 +1,601 @@
'use strict'
const { test } = require('tap')
const memory = require('aedes-persistence')
const Faketimers = require('@sinonjs/fake-timers')
const { setup, connect, noError } = require('./helper')
const aedes = require('../')
function willConnect (s, opts, connected) {
opts = opts || {}
opts.will = {
topic: 'mywill',
payload: Buffer.from('last will'),
qos: 0,
retain: false
}
return connect(s, opts, connected)
}
test('delivers a will', function (t) {
t.plan(4)
const opts = {}
// willConnect populates opts with a will
const s = willConnect(setup(),
opts,
function () {
s.conn.destroy()
}
)
t.teardown(s.broker.close.bind(s.broker))
s.broker.mq.on('mywill', function (packet, cb) {
t.equal(packet.topic, opts.will.topic, 'topic matches')
t.same(packet.payload, opts.will.payload, 'payload matches')
t.equal(packet.qos, opts.will.qos, 'qos matches')
t.equal(packet.retain, opts.will.retain, 'retain matches')
cb()
})
})
test('calling close two times should not deliver two wills', function (t) {
t.plan(4)
const opts = {}
const broker = aedes()
t.teardown(broker.close.bind(broker))
broker.on('client', function (client) {
client.close()
client.close()
})
broker.mq.on('mywill', onWill)
// willConnect populates opts with a will
willConnect(setup(broker), opts)
function onWill (packet, cb) {
broker.mq.removeListener('mywill', onWill, function () {
broker.mq.on('mywill', function (packet) {
t.fail('the will must be delivered only once')
})
})
t.equal(packet.topic, opts.will.topic, 'topic matches')
t.same(packet.payload, opts.will.payload, 'payload matches')
t.equal(packet.qos, opts.will.qos, 'qos matches')
t.equal(packet.retain, opts.will.retain, 'retain matches')
cb()
}
})
test('delivers old will in case of a crash', function (t) {
t.plan(8)
const persistence = memory()
const will = {
topic: 'mywill',
payload: Buffer.from('last will'),
qos: 0,
retain: false
}
persistence.broker = {
id: 'anotherBroker'
}
persistence.putWill({
id: 'myClientId42'
}, will, function (err) {
t.error(err, 'no error')
let authorized = false
const interval = 10 // ms, so that the will check happens fast!
const broker = aedes({
persistence,
heartbeatInterval: interval,
authorizePublish: function (client, packet, callback) {
t.strictSame(client, null, 'client must be null')
authorized = true
callback(null)
}
})
t.teardown(broker.close.bind(broker))
const start = Date.now()
broker.mq.on('mywill', check)
function check (packet, cb) {
broker.mq.removeListener('mywill', check, function () {
broker.mq.on('mywill', function (packet) {
t.fail('the will must be delivered only once')
})
})
t.ok(Date.now() - start >= 3 * interval, 'the will needs to be emitted after 3 heartbeats')
t.equal(packet.topic, will.topic, 'topic matches')
t.same(packet.payload, will.payload, 'payload matches')
t.equal(packet.qos, will.qos, 'qos matches')
t.equal(packet.retain, will.retain, 'retain matches')
t.equal(authorized, true, 'authorization called')
cb()
}
})
})
test('deliver old will without authorization in case of a crash', function (t) {
t.plan(2)
const persistence = memory()
const will = {
topic: 'mywill',
payload: Buffer.from('last will'),
qos: 0,
retain: false
}
persistence.broker = {
id: 'anotherBroker'
}
persistence.putWill({
id: 'myClientId42'
}, will, function (err) {
t.error(err, 'no error')
const interval = 10 // ms, so that the will check happens fast!
const broker = aedes({
persistence,
heartbeatInterval: interval,
authorizePublish: function (client, packet, callback) {
t.strictSame(client, null, 'client must be null')
callback(new Error())
}
})
t.teardown(broker.close.bind(broker))
broker.mq.on('mywill', check)
function check (packet, cb) {
t.fail('received will without authorization')
cb()
}
})
})
test('delete old broker', function (t) {
t.plan(1)
const clock = Faketimers.install()
const heartbeatInterval = 100
const broker = aedes({
heartbeatInterval
})
t.teardown(broker.close.bind(broker))
const brokerId = 'dummyBroker'
broker.brokers[brokerId] = Date.now() - heartbeatInterval * 3.5
setTimeout(() => {
t.equal(broker.brokers[brokerId], undefined, 'Broker deleted')
}, heartbeatInterval * 4)
clock.tick(heartbeatInterval * 4)
clock.uninstall()
})
test('store the will in the persistence', function (t) {
t.plan(5)
const opts = {
clientId: 'abcde'
}
// willConnect populates opts with a will
const s = willConnect(setup(), opts)
t.teardown(s.broker.close.bind(s.broker))
s.broker.on('client', function () {
// this is connack
s.broker.persistence.getWill({
id: opts.clientId
}, function (err, packet) {
t.error(err, 'no error')
t.same(packet.topic, opts.will.topic, 'will topic matches')
t.same(packet.payload, opts.will.payload, 'will payload matches')
t.same(packet.qos, opts.will.qos, 'will qos matches')
t.same(packet.retain, opts.will.retain, 'will retain matches')
})
})
})
test('delete the will in the persistence after publish', function (t) {
t.plan(2)
const opts = {
clientId: 'abcde'
}
const broker = aedes()
t.teardown(broker.close.bind(broker))
broker.on('client', function (client) {
setImmediate(function () {
client.close()
})
})
broker.mq.on('mywill', check)
// willConnect populates opts with a will
willConnect(setup(broker), opts)
function check (packet, cb) {
broker.mq.removeListener('mywill', check, function () {
broker.persistence.getWill({
id: opts.clientId
}, function (err, p) {
t.error(err, 'no error')
t.notOk(p, 'packet is empty')
})
})
cb()
}
})
test('delivers a will with authorization', function (t) {
t.plan(6)
let authorized = false
const opts = {}
// willConnect populates opts with a will
const s = willConnect(
setup(aedes({
authorizePublish: (client, packet, callback) => {
authorized = true
callback(null)
}
})),
opts,
function () {
s.conn.destroy()
}
)
t.teardown(s.broker.close.bind(s.broker))
s.broker.on('clientDisconnect', function (client) {
t.equal(client.connected, false)
})
s.broker.mq.on('mywill', function (packet, cb) {
t.equal(packet.topic, opts.will.topic, 'topic matches')
t.same(packet.payload, opts.will.payload, 'payload matches')
t.equal(packet.qos, opts.will.qos, 'qos matches')
t.equal(packet.retain, opts.will.retain, 'retain matches')
t.equal(authorized, true, 'authorization called')
cb()
})
})
test('delivers a will waits for authorization', function (t) {
t.plan(6)
let authorized = false
const opts = {}
// willConnect populates opts with a will
const s = willConnect(
setup(aedes({
authorizePublish: (client, packet, callback) => {
authorized = true
setImmediate(() => { callback(null) })
}
})),
opts,
function () {
s.conn.destroy()
}
)
t.teardown(s.broker.close.bind(s.broker))
s.broker.on('clientDisconnect', function () {
t.pass('client is disconnected')
})
s.broker.mq.on('mywill', function (packet, cb) {
t.equal(packet.topic, opts.will.topic, 'topic matches')
t.same(packet.payload, opts.will.payload, 'payload matches')
t.equal(packet.qos, opts.will.qos, 'qos matches')
t.equal(packet.retain, opts.will.retain, 'retain matches')
t.equal(authorized, true, 'authorization called')
cb()
})
})
test('does not deliver a will without authorization', function (t) {
t.plan(1)
let authorized = false
const opts = {}
// willConnect populates opts with a will
const s = willConnect(
setup(aedes({
authorizePublish: (username, packet, callback) => {
authorized = true
callback(new Error())
}
})),
opts,
function () {
s.conn.destroy()
}
)
t.teardown(s.broker.close.bind(s.broker))
s.broker.on('clientDisconnect', function () {
t.equal(authorized, true, 'authorization called')
})
s.broker.mq.on('mywill', function (packet, cb) {
t.fail('received will without authorization')
cb()
})
})
test('does not deliver a will without authentication', function (t) {
t.plan(1)
let authenticated = false
const opts = {}
// willConnect populates opts with a will
const s = willConnect(
setup(aedes({
authenticate: (client, username, password, callback) => {
authenticated = true
callback(new Error(), false)
}
})),
opts
)
t.teardown(s.broker.close.bind(s.broker))
s.broker.on('clientError', function () {
t.equal(authenticated, true, 'authentication called')
t.end()
})
s.broker.mq.on('mywill', function (packet, cb) {
t.fail('received will without authentication')
cb()
})
})
test('does not deliver will if broker is closed during authentication', function (t) {
t.plan(0)
const opts = { keepalive: 1 }
const broker = aedes({
authenticate: function (client, username, password, callback) {
setTimeout(function () {
callback(null, true)
})
broker.close()
}
})
broker.on('keepaliveTimeout', function () {
t.fail('keepalive timer shoud not be set')
})
broker.mq.on('mywill', function (packet, cb) {
t.fail('Received will when it was not expected')
cb()
})
willConnect(setup(broker), opts)
})
// [MQTT-3.14.4-3]
test('does not deliver will when client sends a DISCONNECT', function (t) {
t.plan(0)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = noError(willConnect(setup(broker), {}, function () {
s.inStream.end({
cmd: 'disconnect'
})
}), t)
s.broker.mq.on('mywill', function (packet, cb) {
t.fail(packet)
cb()
})
})
test('deletes from persistence on DISCONNECT', function (t) {
t.plan(2)
const opts = {
clientId: 'abcde'
}
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = noError(willConnect(setup(broker), opts, function () {
s.inStream.end({
cmd: 'disconnect'
})
}), t)
s.broker.persistence.getWill({
id: opts.clientId
}, function (err, packet) {
t.error(err, 'no error')
t.notOk(packet)
})
})
test('does not store multiple will with same clientid', function (t) {
t.plan(4)
const opts = { clientId: 'abcde' }
const broker = aedes()
let s = noError(willConnect(setup(broker), opts, function () {
// gracefully close client so no will is sent
s.inStream.end({
cmd: 'disconnect'
})
}), t)
broker.on('clientDisconnect', function (client) {
// reconnect same client with will
s = willConnect(setup(broker), opts, function () {
// check that there are not 2 will messages for the same clientid
s.broker.persistence.delWill({ id: opts.clientId }, function (err, packet) {
t.error(err, 'no error')
t.equal(packet.clientId, opts.clientId, 'will packet found')
s.broker.persistence.delWill({ id: opts.clientId }, function (err, packet) {
t.error(err, 'no error')
t.equal(!!packet, false, 'no duplicated packets')
broker.close()
})
})
})
})
})
test('don\'t delivers a will if broker alive', function (t) {
const persistence = memory()
const will = {
topic: 'mywill',
payload: Buffer.from('last will'),
qos: 0,
retain: false
}
const oldBroker = 'broker1'
persistence.broker = {
id: oldBroker
}
persistence.putWill({
id: 'myClientId42'
}, will, function (err) {
t.error(err, 'no error')
const opts = {
persistence,
heartbeatInterval: 10
}
let count = 0
const broker = aedes(opts)
t.teardown(broker.close.bind(broker))
const streamWill = persistence.streamWill
persistence.streamWill = function () {
// don't pass broker.brokers to streamWill
return streamWill.call(persistence)
}
broker.mq.on('mywill', function (packet, cb) {
t.fail('Will received')
cb()
})
broker.mq.on('$SYS/+/heartbeat', function () {
t.pass('Heartbeat received')
broker.brokers[oldBroker] = Date.now()
if (++count === 5) {
t.end()
}
})
})
})
test('handle will publish error', function (t) {
t.plan(2)
const persistence = memory()
const will = {
topic: 'mywill',
payload: Buffer.from('last will'),
qos: 0,
retain: false
}
persistence.broker = {
id: 'broker1'
}
persistence.putWill({
id: 'myClientId42'
}, will, function (err) {
t.error(err, 'no error')
const opts = {
persistence,
heartbeatInterval: 10
}
persistence.delWill = function (client, cb) {
cb(new Error('Throws error'))
}
const broker = aedes(opts)
t.teardown(broker.close.bind(broker))
broker.once('error', function (err) {
t.equal('Throws error', err.message, 'throws error')
})
})
})
test('handle will publish error 2', function (t) {
t.plan(2)
const persistence = memory()
const will = {
topic: 'mywill',
payload: Buffer.from('last will'),
qos: 0,
retain: true
}
persistence.broker = {
id: 'broker1'
}
persistence.putWill({
id: 'myClientId42'
}, will, function (err) {
t.error(err, 'no error')
const opts = {
persistence,
heartbeatInterval: 10
}
persistence.storeRetained = function (packet, cb) {
cb(new Error('Throws error'))
}
const broker = aedes(opts)
t.teardown(broker.close.bind(broker))
broker.once('error', function (err) {
t.equal('Throws error', err.message, 'throws error')
})
})
})