@flowfuse/nr-mqtt-nodes 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +21 -0
- package/.github/dependabot.yml +15 -0
- package/.github/workflows/project-automation.yml +10 -0
- package/.github/workflows/publish.yml +51 -0
- package/.github/workflows/release-publish.yml +21 -0
- package/CHANGELOG.md +3 -0
- package/LICENSE +178 -0
- package/README.md +29 -0
- package/nodes/ff-mqtt.html +488 -0
- package/nodes/ff-mqtt.js +1826 -0
- package/nodes/icons/ff-logo.svg +6 -0
- package/nodes/lib/TeamBrokerApi.js +83 -0
- package/nodes/lib/proxyHelper.js +301 -0
- package/nodes/lib/util.js +112 -0
- package/nodes/locales/en-US/ff-mqtt.html +121 -0
- package/nodes/locales/en-US/ff-mqtt.json +136 -0
- package/npmignore +5 -0
- package/package.json +54 -0
- package/test/unit/ff-mqtt_spec.js +920 -0
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This is a fork of the MQTT nodes from Node-RED.
|
|
3
|
+
The original code can be found at:
|
|
4
|
+
https://github.com/node-red/node-red/blob/f43d4e946523a797e7489deb46fed9ca78b70c6d/test/nodes/core/network/21-mqtt_spec.js
|
|
5
|
+
### Changes:
|
|
6
|
+
- lint errors fixed up
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict'
|
|
10
|
+
const should = require('should')
|
|
11
|
+
const sinon = require('sinon')
|
|
12
|
+
const helper = require('node-red-node-test-helper')
|
|
13
|
+
const TeamBrokerApi = require('../../nodes/lib/TeamBrokerApi.js')
|
|
14
|
+
const mqttNodes = require('../../nodes/ff-mqtt.js')
|
|
15
|
+
const BROKER_HOST = process.env.MQTT_BROKER_SERVER || 'mqtt://localhost'
|
|
16
|
+
const BROKER_PORT = process.env.MQTT_BROKER_PORT || 1883
|
|
17
|
+
// By default, MQTT tests are disabled. Set ENV VAR NR_MQTT_TESTS to "1" or "true" to enable
|
|
18
|
+
const skipTests = process.env.NR_MQTT_TESTS !== 'true' && process.env.NR_MQTT_TESTS !== '1'
|
|
19
|
+
// const testUtils = require('nr-test-utils')
|
|
20
|
+
const RED = require('node-red/lib/red')
|
|
21
|
+
|
|
22
|
+
describe('FF MQTT Nodes', function () {
|
|
23
|
+
before(function (done) {
|
|
24
|
+
helper.startServer(done)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
beforeEach(function () {
|
|
28
|
+
setupDefaultForgeSettings({ forgeUrl: 'http://localhost:3000' })
|
|
29
|
+
mockTeamBrokerApi(RED, null, { forgeURL: 'http://localhost:3000' })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
after(function (done) {
|
|
33
|
+
helper.stopServer(done)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
afterEach(function () {
|
|
37
|
+
sinon.restore()
|
|
38
|
+
TeamBrokerApi.TeamBrokerApi = TeamBrokerApi._teamBrokerApi
|
|
39
|
+
delete TeamBrokerApi._teamBrokerApi
|
|
40
|
+
const mqttIn = helper.getNode('mqtt.in')
|
|
41
|
+
const mqttOut = helper.getNode('mqtt.out')
|
|
42
|
+
const brokerConn = mqttIn?.brokerConn || mqttOut?.brokerConn
|
|
43
|
+
if (brokerConn) {
|
|
44
|
+
clearInterval(brokerConn.linkMonitorInterval)
|
|
45
|
+
brokerConn.disconnect()
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
helper.unload()
|
|
49
|
+
} catch (error) { }
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
function mockTeamBrokerApi (RED, gotClient, { forgeURL, teamId, instanceType = 'application', instanceId = 'application-id', token, API_VERSION = 'v1' } = {}) {
|
|
53
|
+
TeamBrokerApi._teamBrokerApi = TeamBrokerApi.TeamBrokerApi
|
|
54
|
+
TeamBrokerApi.TeamBrokerApi = sinon.stub().callsFake(function (RED, gotClient, settings) {
|
|
55
|
+
const api = {
|
|
56
|
+
link: sinon.stub().callsFake(async function (password) {
|
|
57
|
+
return { }
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
return api
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function setupDefaultForgeSettings ({ forgeURL, teamID, applicationID, projectID, deviceID, useSharedSubscriptions, broker } = {}) {
|
|
65
|
+
RED.settings.flowforge = {
|
|
66
|
+
forgeURL: forgeURL || 'http://localhost:3000',
|
|
67
|
+
teamID: teamID || 'team-id',
|
|
68
|
+
applicationID: applicationID || 'application-id',
|
|
69
|
+
projectID: projectID || 'project-id',
|
|
70
|
+
deviceID: deviceID || null
|
|
71
|
+
}
|
|
72
|
+
RED.settings.flowforge.projectLink = {
|
|
73
|
+
useSharedSubscriptions: useSharedSubscriptions || false,
|
|
74
|
+
broker: broker || {
|
|
75
|
+
url: BROKER_HOST,
|
|
76
|
+
username: null,
|
|
77
|
+
password: null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
it('should not be loaded without FF settings', async function () {
|
|
83
|
+
this.timeout = 2000
|
|
84
|
+
const { flow } = buildBasicMQTTSendRecvFlow({ id: 'mqtt.in', topic: 'in_topic' }, { id: 'mqtt.out', topic: 'out_topic' })
|
|
85
|
+
|
|
86
|
+
RED.settings.flowforge = null // no settings
|
|
87
|
+
should(helper.load(mqttNodes, flow)).be.rejectedWith('FlowFuse MQTT nodes cannot be loaded outside of an FlowFuse EE environment')
|
|
88
|
+
|
|
89
|
+
setupDefaultForgeSettings()
|
|
90
|
+
RED.settings.flowforge.forgeURL = null // no url
|
|
91
|
+
should(helper.load(mqttNodes, flow)).be.rejectedWith('FlowFuse MQTT nodes cannot be loaded outside of an FlowFuse EE environment')
|
|
92
|
+
|
|
93
|
+
setupDefaultForgeSettings()
|
|
94
|
+
RED.settings.flowforge.teamID = null // no teamID
|
|
95
|
+
should(helper.load(mqttNodes, flow)).be.rejectedWith('FlowFuse MQTT nodes cannot be loaded outside of an FlowFuse EE environment')
|
|
96
|
+
|
|
97
|
+
setupDefaultForgeSettings()
|
|
98
|
+
RED.settings.flowforge.broker = null
|
|
99
|
+
should(helper.load(mqttNodes, flow)).be.rejectedWith('FlowFuse MQTT nodes cannot be loaded without a broker configured in FlowFuse EE settings')
|
|
100
|
+
|
|
101
|
+
setupDefaultForgeSettings()
|
|
102
|
+
RED.settings.flowforge.projectID = null // no projectID
|
|
103
|
+
should(helper.load(mqttNodes, flow)).be.rejectedWith('FlowFuse MQTT nodes cannot be loaded due to missing instance information')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should be loaded and have default values', async function () {
|
|
107
|
+
this.timeout = 2000
|
|
108
|
+
const { flow } = buildBasicMQTTSendRecvFlow({ id: 'mqtt.in', topic: 'in_topic' }, { id: 'mqtt.out', topic: 'out_topic' })
|
|
109
|
+
await helper.load(mqttNodes, flow)
|
|
110
|
+
|
|
111
|
+
const mqttIn = helper.getNode('mqtt.in')
|
|
112
|
+
const mqttOut = helper.getNode('mqtt.out')
|
|
113
|
+
|
|
114
|
+
should(mqttIn).be.type('object', 'mqtt in node should be an object')
|
|
115
|
+
mqttIn.should.have.property('datatype', 'utf8') // default: 'utf8'
|
|
116
|
+
mqttIn.should.have.property('isDynamic', false) // default: false
|
|
117
|
+
mqttIn.should.have.property('inputs', 0) // default: 0
|
|
118
|
+
mqttIn.should.have.property('qos', 2) // default: 2
|
|
119
|
+
mqttIn.should.have.property('topic', 'in_topic')
|
|
120
|
+
mqttIn.should.have.property('wires', [['helper.node']])
|
|
121
|
+
mqttIn.should.have.property('brokerConn').and.be.type('object')
|
|
122
|
+
|
|
123
|
+
should(mqttOut).be.type('object', 'mqtt out node should be an object')
|
|
124
|
+
mqttOut.should.have.property('topic', 'out_topic')
|
|
125
|
+
mqttOut.should.have.property('brokerConn').and.be.type('object')
|
|
126
|
+
|
|
127
|
+
mqttOut.brokerConn.should.equal(mqttIn.brokerConn) // should be the same broker connection
|
|
128
|
+
const mqttBroker = mqttOut.brokerConn
|
|
129
|
+
clearInterval(mqttBroker.linkMonitorInterval) // clear the link monitor interval so the test can exit
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if (skipTests) {
|
|
133
|
+
it('skipping MQTT tests. Set env var "NR_MQTT_TESTS=true" to enable. Requires a v5 capable broker running on localhost:1883.', function (done) {
|
|
134
|
+
done()
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
// Conditional test runner (only run if skipTests=false)
|
|
138
|
+
function itConditional (title, test) {
|
|
139
|
+
return !skipTests ? it(title, test) : it.skip(title, test)
|
|
140
|
+
}
|
|
141
|
+
itConditional.skip = it.skip
|
|
142
|
+
// eslint-disable-next-line no-only-tests/no-only-tests
|
|
143
|
+
itConditional.only = it.only
|
|
144
|
+
|
|
145
|
+
// #region ################### BASIC TESTS ################### #//
|
|
146
|
+
|
|
147
|
+
itConditional('basic send and receive tests', function (done) {
|
|
148
|
+
if (skipTests) { return this.skip() }
|
|
149
|
+
this.timeout = 2000
|
|
150
|
+
const options = {}
|
|
151
|
+
options.sendMsg = {
|
|
152
|
+
topic: nextTopic(),
|
|
153
|
+
payload: 'hello',
|
|
154
|
+
qos: 0
|
|
155
|
+
}
|
|
156
|
+
options.expectMsg = Object.assign({}, options.sendMsg)
|
|
157
|
+
testSendRecv({}, { datatype: 'auto', topicType: 'static' }, {}, options, { done })
|
|
158
|
+
})
|
|
159
|
+
// Prior to V3, "auto" mode would only parse to string or buffer.
|
|
160
|
+
itConditional('should send JSON and receive string (auto mode)', function (done) {
|
|
161
|
+
if (skipTests) { return this.skip() }
|
|
162
|
+
this.timeout = 2000
|
|
163
|
+
const options = {}
|
|
164
|
+
options.sendMsg = {
|
|
165
|
+
topic: nextTopic(),
|
|
166
|
+
payload: '{"prop":"value1", "num":1}',
|
|
167
|
+
qos: 1
|
|
168
|
+
}
|
|
169
|
+
options.expectMsg = Object.assign({}, options.sendMsg)
|
|
170
|
+
testSendRecv({}, { datatype: 'auto', topicType: 'static' }, {}, options, { done })
|
|
171
|
+
})
|
|
172
|
+
// In V3, "auto" mode should try to parse JSON, then string and fall back to buffer
|
|
173
|
+
itConditional('should send JSON and receive object (auto-detect mode)', function (done) {
|
|
174
|
+
if (skipTests) { return this.skip() }
|
|
175
|
+
this.timeout = 2000
|
|
176
|
+
const options = {}
|
|
177
|
+
options.sendMsg = {
|
|
178
|
+
topic: nextTopic(),
|
|
179
|
+
payload: '{"prop":"value1", "num":1}',
|
|
180
|
+
qos: 1
|
|
181
|
+
}
|
|
182
|
+
options.expectMsg = Object.assign({}, options.sendMsg)
|
|
183
|
+
options.expectMsg.payload = JSON.parse(options.sendMsg.payload)
|
|
184
|
+
testSendRecv({}, { datatype: 'auto-detect', topicType: 'static' }, {}, options, { done })
|
|
185
|
+
})
|
|
186
|
+
itConditional('should send invalid JSON and receive string (auto mode)', function (done) {
|
|
187
|
+
if (skipTests) { return this.skip() }
|
|
188
|
+
this.timeout = 2000
|
|
189
|
+
const options = {}
|
|
190
|
+
options.sendMsg = {
|
|
191
|
+
topic: nextTopic(),
|
|
192
|
+
payload: '{prop:"value3", "num":3}'// send invalid JSON ...
|
|
193
|
+
}
|
|
194
|
+
options.expectMsg = Object.assign({}, options.sendMsg)// expect same payload
|
|
195
|
+
testSendRecv({}, { datatype: 'auto', topicType: 'static' }, {}, options, { done })
|
|
196
|
+
})
|
|
197
|
+
itConditional('should send invalid JSON and receive string (auto-detect mode)', function (done) {
|
|
198
|
+
if (skipTests) { return this.skip() }
|
|
199
|
+
this.timeout = 2000
|
|
200
|
+
const options = {}
|
|
201
|
+
options.sendMsg = {
|
|
202
|
+
topic: nextTopic(),
|
|
203
|
+
payload: '{prop:"value3", "num":3}'// send invalid JSON ...
|
|
204
|
+
}
|
|
205
|
+
options.expectMsg = Object.assign({}, options.sendMsg)// expect same payload
|
|
206
|
+
testSendRecv({}, { datatype: 'auto-detect', topicType: 'static' }, {}, options, { done })
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
itConditional('should send JSON and receive string (utf8 mode)', function (done) {
|
|
210
|
+
if (skipTests) { return this.skip() }
|
|
211
|
+
this.timeout = 2000
|
|
212
|
+
const options = {}
|
|
213
|
+
options.sendMsg = {
|
|
214
|
+
topic: nextTopic(),
|
|
215
|
+
payload: '{"prop":"value2", "num":2}',
|
|
216
|
+
qos: 2
|
|
217
|
+
}
|
|
218
|
+
options.expectMsg = Object.assign({}, options.sendMsg)
|
|
219
|
+
testSendRecv({}, { datatype: 'utf8', topicType: 'static' }, {}, options, { done })
|
|
220
|
+
})
|
|
221
|
+
itConditional('should send JSON and receive Object (json mode)', function (done) {
|
|
222
|
+
if (skipTests) { return this.skip() }
|
|
223
|
+
this.timeout = 2000
|
|
224
|
+
const options = {}
|
|
225
|
+
options.sendMsg = {
|
|
226
|
+
topic: nextTopic(),
|
|
227
|
+
payload: '{"prop":"value3", "num":3}'// send a string ...
|
|
228
|
+
}
|
|
229
|
+
options.expectMsg = Object.assign({}, options.sendMsg, { payload: { prop: 'value3', num: 3 } })// expect an object
|
|
230
|
+
testSendRecv({}, { datatype: 'json', topicType: 'static' }, {}, options, { done })
|
|
231
|
+
})
|
|
232
|
+
itConditional('should send invalid JSON and raise error (json mode)', function (done) {
|
|
233
|
+
if (skipTests) { return this.skip() }
|
|
234
|
+
this.timeout = 2000
|
|
235
|
+
const options = {}
|
|
236
|
+
options.sendMsg = {
|
|
237
|
+
topic: nextTopic(),
|
|
238
|
+
payload: '{prop:"value3", "num":3}' // send invalid JSON ...
|
|
239
|
+
}
|
|
240
|
+
const hooks = { done: null, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
241
|
+
hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => {
|
|
242
|
+
helperNode.on('input', function (msg) {
|
|
243
|
+
try {
|
|
244
|
+
msg.should.have.a.property('error').type('object')
|
|
245
|
+
msg.error.should.have.a.property('source').type('object')
|
|
246
|
+
msg.error.source.should.have.a.property('id', mqttIn.id)
|
|
247
|
+
done()
|
|
248
|
+
} catch (err) {
|
|
249
|
+
done(err)
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
return true // handled
|
|
253
|
+
}
|
|
254
|
+
testSendRecv({}, { datatype: 'json', topicType: 'static' }, {}, options, hooks)
|
|
255
|
+
})
|
|
256
|
+
itConditional('should send String and receive Buffer (buffer mode)', function (done) {
|
|
257
|
+
if (skipTests) { return this.skip() }
|
|
258
|
+
this.timeout = 2000
|
|
259
|
+
const options = {}
|
|
260
|
+
options.sendMsg = {
|
|
261
|
+
topic: nextTopic(),
|
|
262
|
+
payload: 'a b c' // send string ...
|
|
263
|
+
}
|
|
264
|
+
options.expectMsg = Object.assign({}, options.sendMsg, { payload: Buffer.from(options.sendMsg.payload) })// expect Buffer.from(msg.payload)
|
|
265
|
+
testSendRecv({}, { datatype: 'buffer', topicType: 'static' }, {}, options, { done })
|
|
266
|
+
})
|
|
267
|
+
itConditional('should send utf8 Buffer and receive String (auto mode)', function (done) {
|
|
268
|
+
if (skipTests) { return this.skip() }
|
|
269
|
+
this.timeout = 2000
|
|
270
|
+
const options = {}
|
|
271
|
+
options.sendMsg = {
|
|
272
|
+
topic: nextTopic(),
|
|
273
|
+
payload: Buffer.from([0x78, 0x20, 0x79, 0x20, 0x7a]) // "x y z"
|
|
274
|
+
}
|
|
275
|
+
options.expectMsg = Object.assign({}, options.sendMsg, { payload: 'x y z' })// set expected payload to "x y z"
|
|
276
|
+
testSendRecv({}, { datatype: 'auto', topicType: 'static' }, {}, options, { done })
|
|
277
|
+
})
|
|
278
|
+
itConditional('should send non utf8 Buffer and receive Buffer (auto mode)', function (done) {
|
|
279
|
+
if (skipTests) { return this.skip() }
|
|
280
|
+
this.timeout = 2000
|
|
281
|
+
const options = {}
|
|
282
|
+
const hooks = { done, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
283
|
+
options.sendMsg = {
|
|
284
|
+
topic: nextTopic(),
|
|
285
|
+
payload: Buffer.from([0xC0, 0xC1, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF]) // non valid UTF8
|
|
286
|
+
}
|
|
287
|
+
options.expectMsg = Object.assign({}, options.sendMsg, { payload: Buffer.from([0xC0, 0xC1, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF]) })
|
|
288
|
+
testSendRecv({}, { datatype: 'auto', topicType: 'static' }, {}, options, hooks)
|
|
289
|
+
})
|
|
290
|
+
itConditional('should send/receive all v5 flags and settings', function (done) {
|
|
291
|
+
if (skipTests) { return this.skip() }
|
|
292
|
+
this.timeout = 2000
|
|
293
|
+
const t = nextTopic()
|
|
294
|
+
const options = {}
|
|
295
|
+
const hooks = { done, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
296
|
+
options.sendMsg = {
|
|
297
|
+
topic: t + '/command',
|
|
298
|
+
payload: Buffer.from('{"version":"v5"}'),
|
|
299
|
+
qos: 1,
|
|
300
|
+
retain: true,
|
|
301
|
+
responseTopic: t + '/response',
|
|
302
|
+
userProperties: { prop1: 'val1' },
|
|
303
|
+
contentType: 'text/plain',
|
|
304
|
+
correlationData: Buffer.from([1, 2, 3]),
|
|
305
|
+
payloadFormatIndicator: true,
|
|
306
|
+
messageExpiryInterval: 2000
|
|
307
|
+
}
|
|
308
|
+
options.expectMsg = Object.assign({}, options.sendMsg)
|
|
309
|
+
options.expectMsg.payload = options.expectMsg.payload.toString() // auto mode + payloadFormatIndicator + contentType: "text/plain" should make a string
|
|
310
|
+
delete options.expectMsg.payloadFormatIndicator // Seems mqtt.js only publishes payloadFormatIndicator the will msg
|
|
311
|
+
const inOptions = {
|
|
312
|
+
datatype: 'auto',
|
|
313
|
+
topicType: 'static',
|
|
314
|
+
qos: 1,
|
|
315
|
+
nl: false,
|
|
316
|
+
rap: true,
|
|
317
|
+
rh: 1
|
|
318
|
+
}
|
|
319
|
+
testSendRecv({ protocolVersion: 5 }, inOptions, {}, options, hooks)
|
|
320
|
+
})
|
|
321
|
+
itConditional('should send regular string with v5 media type "text/plain" and receive a string (auto mode)', function (done) {
|
|
322
|
+
if (skipTests) { return this.skip() }
|
|
323
|
+
this.timeout = 2000
|
|
324
|
+
const options = {}
|
|
325
|
+
const hooks = { done, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
326
|
+
options.sendMsg = {
|
|
327
|
+
topic: nextTopic(), payload: 'abc', contentType: 'text/plain'
|
|
328
|
+
}
|
|
329
|
+
options.expectMsg = Object.assign({}, options.sendMsg)
|
|
330
|
+
testSendRecv({ protocolVersion: 5 }, { datatype: 'auto', topicType: 'static' }, {}, options, hooks)
|
|
331
|
+
})
|
|
332
|
+
itConditional('should send JSON with v5 media type "text/plain" and receive a string (auto mode)', function (done) {
|
|
333
|
+
if (skipTests) { return this.skip() }
|
|
334
|
+
this.timeout = 2000
|
|
335
|
+
const options = {}
|
|
336
|
+
const hooks = { done, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
337
|
+
options.sendMsg = {
|
|
338
|
+
topic: nextTopic(), payload: '{"prop":"val"}', contentType: 'text/plain'
|
|
339
|
+
}
|
|
340
|
+
options.expectMsg = Object.assign({}, options.sendMsg)
|
|
341
|
+
testSendRecv({ protocolVersion: 5 }, { datatype: 'auto', topicType: 'static' }, {}, options, hooks)
|
|
342
|
+
})
|
|
343
|
+
itConditional('should send JSON with v5 media type "text/plain" and receive a string (auto-detect mode)', function (done) {
|
|
344
|
+
if (skipTests) { return this.skip() }
|
|
345
|
+
this.timeout = 2000
|
|
346
|
+
const options = {}
|
|
347
|
+
const hooks = { done, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
348
|
+
options.sendMsg = {
|
|
349
|
+
topic: nextTopic(), payload: '{"prop":"val"}', contentType: 'text/plain'
|
|
350
|
+
}
|
|
351
|
+
options.expectMsg = Object.assign({}, options.sendMsg)
|
|
352
|
+
testSendRecv({ protocolVersion: 5 }, { datatype: 'auto-detect', topicType: 'static' }, {}, options, hooks)
|
|
353
|
+
})
|
|
354
|
+
itConditional('should send JSON with v5 media type "application/json" and receive an object (auto-detect mode)', function (done) {
|
|
355
|
+
if (skipTests) { return this.skip() }
|
|
356
|
+
this.timeout = 2000
|
|
357
|
+
const options = {}
|
|
358
|
+
const hooks = { done, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
359
|
+
options.sendMsg = {
|
|
360
|
+
topic: nextTopic(), payload: '{"prop":"val"}', contentType: 'application/json'
|
|
361
|
+
}
|
|
362
|
+
options.expectMsg = Object.assign({}, options.sendMsg, { payload: JSON.parse(options.sendMsg.payload) })
|
|
363
|
+
testSendRecv({ protocolVersion: 5 }, { datatype: 'auto-detect', topicType: 'static' }, {}, options, hooks)
|
|
364
|
+
})
|
|
365
|
+
itConditional('should send invalid JSON with v5 media type "application/json" and raise an error (auto mode)', function (done) {
|
|
366
|
+
if (skipTests) { return this.skip() }
|
|
367
|
+
this.timeout = 2000
|
|
368
|
+
const options = {}
|
|
369
|
+
options.sendMsg = {
|
|
370
|
+
topic: nextTopic(),
|
|
371
|
+
payload: '{prop:"value3", "num":3}',
|
|
372
|
+
contentType: 'application/json' // send invalid JSON ...
|
|
373
|
+
}
|
|
374
|
+
const hooks = { done: null, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
375
|
+
hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => {
|
|
376
|
+
helperNode.on('input', function (msg) {
|
|
377
|
+
try {
|
|
378
|
+
msg.should.have.a.property('error').type('object')
|
|
379
|
+
msg.error.should.have.a.property('source').type('object')
|
|
380
|
+
msg.error.source.should.have.a.property('id', mqttIn.id)
|
|
381
|
+
done()
|
|
382
|
+
} catch (err) {
|
|
383
|
+
done(err)
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
return true // handled
|
|
387
|
+
}
|
|
388
|
+
testSendRecv({ protocolVersion: 5 }, { datatype: 'auto', topicType: 'static' }, {}, options, hooks)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
itConditional('should send buffer with v5 media type "application/json" and receive an object (auto-detect mode)', function (done) {
|
|
392
|
+
if (skipTests) { return this.skip() }
|
|
393
|
+
this.timeout = 2000
|
|
394
|
+
const options = {}
|
|
395
|
+
const hooks = { done, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
396
|
+
options.sendMsg = {
|
|
397
|
+
topic: nextTopic(), payload: Buffer.from([0x7b, 0x22, 0x70, 0x72, 0x6f, 0x70, 0x22, 0x3a, 0x22, 0x76, 0x61, 0x6c, 0x22, 0x7d]), contentType: 'application/json'
|
|
398
|
+
}
|
|
399
|
+
options.expectMsg = Object.assign({}, options.sendMsg, { payload: { prop: 'val' } })
|
|
400
|
+
testSendRecv({ protocolVersion: 5 }, { datatype: 'auto-detect', topicType: 'static' }, {}, options, hooks)
|
|
401
|
+
})
|
|
402
|
+
itConditional('should send buffer with v5 media type "text/plain" and receive a string (auto mode)', function (done) {
|
|
403
|
+
if (skipTests) { return this.skip() }
|
|
404
|
+
this.timeout = 2000
|
|
405
|
+
const options = {}
|
|
406
|
+
const hooks = { done, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
407
|
+
options.sendMsg = {
|
|
408
|
+
topic: nextTopic(), payload: Buffer.from([0x7b, 0x22, 0x70, 0x72, 0x6f, 0x70, 0x22, 0x3a, 0x22, 0x76, 0x61, 0x6c, 0x22, 0x7d]), contentType: 'text/plain'
|
|
409
|
+
}
|
|
410
|
+
options.expectMsg = Object.assign({}, options.sendMsg, { payload: '{"prop":"val"}' })
|
|
411
|
+
testSendRecv({ protocolVersion: 5 }, { datatype: 'auto', topicType: 'static' }, {}, options, hooks)
|
|
412
|
+
})
|
|
413
|
+
itConditional('should send buffer with v5 media type "application/zip" and receive a buffer (auto mode)', function (done) {
|
|
414
|
+
if (skipTests) { return this.skip() }
|
|
415
|
+
this.timeout = 2000
|
|
416
|
+
const options = {}
|
|
417
|
+
const hooks = { done, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
418
|
+
options.sendMsg = {
|
|
419
|
+
topic: nextTopic(), payload: Buffer.from([0x7b, 0x22, 0x70, 0x72, 0x6f, 0x70, 0x22, 0x3a, 0x22, 0x76, 0x61, 0x6c, 0x22, 0x7d]), contentType: 'application/zip'
|
|
420
|
+
}
|
|
421
|
+
options.expectMsg = Object.assign({}, options.sendMsg, { payload: Buffer.from([0x7b, 0x22, 0x70, 0x72, 0x6f, 0x70, 0x22, 0x3a, 0x22, 0x76, 0x61, 0x6c, 0x22, 0x7d]) })
|
|
422
|
+
testSendRecv({ protocolVersion: 5 }, { datatype: 'auto', topicType: 'static' }, {}, options, hooks)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
itConditional('should subscribe dynamically via action', function (done) {
|
|
426
|
+
if (skipTests) { return this.skip() }
|
|
427
|
+
this.timeout = 2000
|
|
428
|
+
const options = {}
|
|
429
|
+
const hooks = { done, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
430
|
+
options.sendMsg = {
|
|
431
|
+
topic: nextTopic(), payload: 'abc'
|
|
432
|
+
}
|
|
433
|
+
options.expectMsg = Object.assign({}, options.sendMsg)
|
|
434
|
+
testSendRecv({ protocolVersion: 5 }, { datatype: 'utf8', topicType: 'dynamic' }, {}, options, hooks)
|
|
435
|
+
})
|
|
436
|
+
// #endregion BASIC TESTS
|
|
437
|
+
|
|
438
|
+
// #region ################### ADVANCED TESTS ################### #//
|
|
439
|
+
// next test is skipped because broker options are not settible for ff-mqtt nodes (always auto connects)
|
|
440
|
+
itConditional.skip('should connect via "connect" action', function (done) {
|
|
441
|
+
if (skipTests) { return this.skip() }
|
|
442
|
+
this.timeout = 2000
|
|
443
|
+
const options = {}
|
|
444
|
+
const hooks = { done: null, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
445
|
+
hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => {
|
|
446
|
+
mqttBroker.should.have.property('autoConnect', false)
|
|
447
|
+
mqttBroker.should.have.property('connecting', false)// should not attempt to connect (autoConnect:false)
|
|
448
|
+
mqttIn.receive({ action: 'connect' }) // now request connect action
|
|
449
|
+
return true // handled
|
|
450
|
+
}
|
|
451
|
+
hooks.afterConnect = (helperNode, mqttBroker, mqttIn, mqttOut) => {
|
|
452
|
+
done()// if we got here, it connected :)
|
|
453
|
+
return true
|
|
454
|
+
}
|
|
455
|
+
testSendRecv({ protocolVersion: 5, autoConnect: false }, { datatype: 'utf8', topicType: 'dynamic' }, {}, options, hooks)
|
|
456
|
+
})
|
|
457
|
+
itConditional('should disconnect via "disconnect" action', function (done) {
|
|
458
|
+
if (skipTests) { return this.skip() }
|
|
459
|
+
this.timeout = 2000
|
|
460
|
+
const options = {}
|
|
461
|
+
const hooks = { beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
462
|
+
hooks.beforeLoad = (flow) => { // add a status node pointed at MQTT Out node (to watch for connection status change)
|
|
463
|
+
flow.push({ id: 'status.node', type: 'status', name: 'status_node', scope: ['mqtt.out'], wires: [['helper.node']] })// add status node to watch mqtt_out
|
|
464
|
+
}
|
|
465
|
+
hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => {
|
|
466
|
+
mqttBroker.should.have.property('autoConnect', true)
|
|
467
|
+
mqttBroker.should.have.property('connecting', true)// should be trying to connect (autoConnect:true)
|
|
468
|
+
return true // handled
|
|
469
|
+
}
|
|
470
|
+
hooks.afterConnect = (helperNode, mqttBroker, mqttIn, mqttOut) => {
|
|
471
|
+
// connected - now add the "on" handler then send "disconnect" action
|
|
472
|
+
helperNode.on('input', function (msg) {
|
|
473
|
+
try {
|
|
474
|
+
msg.should.have.property('status')
|
|
475
|
+
msg.status.should.have.property('text')
|
|
476
|
+
msg.status.text.should.containEql('disconnect')
|
|
477
|
+
done() // it disconnected - yey!
|
|
478
|
+
} catch (error) {
|
|
479
|
+
done(error)
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
mqttOut.receive({ action: 'disconnect' })
|
|
483
|
+
return true // handed
|
|
484
|
+
}
|
|
485
|
+
testSendRecv({ protocolVersion: 5 }, null, {}, options, hooks)
|
|
486
|
+
})
|
|
487
|
+
// next test is skipped because broker options are not settible for ff-mqtt nodes (always auto connects)
|
|
488
|
+
itConditional.skip('should publish birth message', function (done) {
|
|
489
|
+
if (skipTests) { return this.skip() }
|
|
490
|
+
this.timeout = 2000
|
|
491
|
+
const baseTopic = nextTopic()
|
|
492
|
+
const brokerOptions = {
|
|
493
|
+
autoConnect: false,
|
|
494
|
+
protocolVersion: 4,
|
|
495
|
+
birthTopic: baseTopic + '/birth',
|
|
496
|
+
birthPayload: 'broker birth',
|
|
497
|
+
birthQos: 2
|
|
498
|
+
}
|
|
499
|
+
const expectMsg = {
|
|
500
|
+
topic: brokerOptions.birthTopic,
|
|
501
|
+
payload: brokerOptions.birthPayload,
|
|
502
|
+
qos: brokerOptions.birthQos
|
|
503
|
+
}
|
|
504
|
+
const options = { }
|
|
505
|
+
const hooks = { }
|
|
506
|
+
hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => {
|
|
507
|
+
helperNode.on('input', function (msg) {
|
|
508
|
+
try {
|
|
509
|
+
compareMsgToExpected(msg, expectMsg)
|
|
510
|
+
done()
|
|
511
|
+
} catch (error) {
|
|
512
|
+
done(error)
|
|
513
|
+
}
|
|
514
|
+
})
|
|
515
|
+
mqttIn.receive({ action: 'connect' }) // now request connect action
|
|
516
|
+
return true // handled
|
|
517
|
+
}
|
|
518
|
+
testSendRecv(brokerOptions, { topic: brokerOptions.birthTopic }, {}, options, hooks)
|
|
519
|
+
})
|
|
520
|
+
// next test is skipped because broker options are not settible for ff-mqtt nodes (always auto connects)
|
|
521
|
+
itConditional.skip('should safely discard bad birth topic', function (done) {
|
|
522
|
+
if (skipTests) { return this.skip() }
|
|
523
|
+
this.timeout = 2000
|
|
524
|
+
const baseTopic = nextTopic()
|
|
525
|
+
const brokerOptions = {
|
|
526
|
+
protocolVersion: 4,
|
|
527
|
+
birthTopic: baseTopic + '#', // a publish topic should never have a wildcard
|
|
528
|
+
birthPayload: 'broker connected',
|
|
529
|
+
birthQos: 2
|
|
530
|
+
}
|
|
531
|
+
const options = {}
|
|
532
|
+
const hooks = { done: null, beforeLoad: null, afterLoad: null, afterConnect: null }
|
|
533
|
+
hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => {
|
|
534
|
+
helperNode.on('input', function (msg) {
|
|
535
|
+
try {
|
|
536
|
+
msg.should.have.a.property('error').type('object')
|
|
537
|
+
msg.error.should.have.a.property('source').type('object')
|
|
538
|
+
msg.error.source.should.have.a.property('id', mqttIn.id)
|
|
539
|
+
done()
|
|
540
|
+
} catch (err) {
|
|
541
|
+
done(err)
|
|
542
|
+
}
|
|
543
|
+
})
|
|
544
|
+
return true // handled
|
|
545
|
+
}
|
|
546
|
+
options.expectMsg = null
|
|
547
|
+
try {
|
|
548
|
+
testSendRecv(brokerOptions, { topic: brokerOptions.birthTopic }, {}, options, hooks)
|
|
549
|
+
done()
|
|
550
|
+
} catch (error) {
|
|
551
|
+
done(error)
|
|
552
|
+
}
|
|
553
|
+
})
|
|
554
|
+
// next test is skipped because broker options are not settible for ff-mqtt nodes (always auto connects)
|
|
555
|
+
itConditional.skip('should publish close message', function (done) {
|
|
556
|
+
if (skipTests) { return this.skip() }
|
|
557
|
+
this.timeout = 2000
|
|
558
|
+
const baseTopic = nextTopic()
|
|
559
|
+
const broker1Options = { id: 'mqtt.broker1' }// Broker 1 - stays connected to receive the close message
|
|
560
|
+
const broker2Options = { id: 'mqtt.broker2', closeTopic: baseTopic + '/close', closePayload: '{"msg":"close"}', closeQos: 1 }// Broker 2 - connects to same broker but has a LWT message.
|
|
561
|
+
const { flow } = buildBasicMQTTSendRecvFlow(broker1Options, { broker: broker1Options.id, topic: broker2Options.closeTopic, datatype: 'json' }, { broker: broker2Options.id })
|
|
562
|
+
flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, BROKER_HOST, BROKER_PORT, broker2Options)) // add second broker
|
|
563
|
+
helper.load(mqttNodes, flow, function () {
|
|
564
|
+
const helperNode = helper.getNode('helper.node')
|
|
565
|
+
const mqttOut = helper.getNode('mqtt.out')
|
|
566
|
+
const mqttBroker1 = helper.getNode('mqtt.broker1')
|
|
567
|
+
const mqttBroker2 = helper.getNode('mqtt.broker2')
|
|
568
|
+
waitBrokerConnect([mqttBroker1, mqttBroker2])
|
|
569
|
+
.then(() => {
|
|
570
|
+
// connected - add the on handler and call to disconnect
|
|
571
|
+
helperNode.on('input', function (msg) {
|
|
572
|
+
try {
|
|
573
|
+
msg.should.have.property('topic', broker2Options.closeTopic)
|
|
574
|
+
msg.should.have.property('payload', JSON.parse(broker2Options.closePayload))
|
|
575
|
+
msg.should.have.property('qos', broker2Options.closeQos)
|
|
576
|
+
done()
|
|
577
|
+
} catch (error) {
|
|
578
|
+
done(error)
|
|
579
|
+
}
|
|
580
|
+
})
|
|
581
|
+
mqttOut.receive({ action: 'disconnect' })// close broker2
|
|
582
|
+
})
|
|
583
|
+
.catch(done)
|
|
584
|
+
})
|
|
585
|
+
})
|
|
586
|
+
// next test is skipped because broker options are not settible for ff-mqtt nodes (always auto connects)
|
|
587
|
+
itConditional.skip('should publish will message', function (done) {
|
|
588
|
+
if (skipTests) { return this.skip() }
|
|
589
|
+
this.timeout = 2000
|
|
590
|
+
const baseTopic = nextTopic()
|
|
591
|
+
const broker1Options = { id: 'mqtt.broker1' }// Broker 1 - stays connected to receive the will message
|
|
592
|
+
const broker2Options = { id: 'mqtt.broker2', willTopic: baseTopic + '/will', willPayload: '{"msg":"will"}', willQos: 2 }// Broker 2 - connects to same broker but has a LWT message.
|
|
593
|
+
const { flow } = buildBasicMQTTSendRecvFlow(broker1Options, { broker: broker1Options.id, topic: broker2Options.willTopic, datatype: 'utf8' }, { broker: broker2Options.id })
|
|
594
|
+
flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, BROKER_HOST, BROKER_PORT, broker2Options)) // add second broker
|
|
595
|
+
|
|
596
|
+
helper.load(mqttNodes, flow, function () {
|
|
597
|
+
const helperNode = helper.getNode('helper.node')
|
|
598
|
+
const mqttBroker1 = helper.getNode('mqtt.broker1')
|
|
599
|
+
const mqttBroker2 = helper.getNode('mqtt.broker2')
|
|
600
|
+
waitBrokerConnect([mqttBroker1, mqttBroker2])
|
|
601
|
+
.then(() => {
|
|
602
|
+
// connected - add the on handler and call to disconnect
|
|
603
|
+
helperNode.on('input', function (msg) {
|
|
604
|
+
try {
|
|
605
|
+
msg.should.have.property('topic', broker2Options.willTopic)
|
|
606
|
+
msg.should.have.property('payload', broker2Options.willPayload)
|
|
607
|
+
msg.should.have.property('qos', broker2Options.willQos)
|
|
608
|
+
done()
|
|
609
|
+
} catch (error) {
|
|
610
|
+
done(error)
|
|
611
|
+
}
|
|
612
|
+
})
|
|
613
|
+
mqttBroker2.client.end(true) // force closure
|
|
614
|
+
})
|
|
615
|
+
.catch(done)
|
|
616
|
+
})
|
|
617
|
+
})
|
|
618
|
+
// next test is skipped because broker options are not settible for ff-mqtt nodes (always auto connects)
|
|
619
|
+
itConditional.skip('should publish will message with V5 properties', function (done) {
|
|
620
|
+
if (skipTests) { return this.skip() }
|
|
621
|
+
// return this.skip(); //Issue receiving v5 props on will msg. Issue raised here: https://github.com/mqttjs/MQTT.js/issues/1455
|
|
622
|
+
this.timeout = 2000
|
|
623
|
+
const baseTopic = nextTopic()
|
|
624
|
+
// Broker 1 - stays connected to receive the will message when broker 2 is killed
|
|
625
|
+
const broker1Options = { id: 'mqtt.broker1', name: 'mqtt_broker1', protocolVersion: 5, datatype: 'utf8' }
|
|
626
|
+
// Broker 2 - connects to same broker but has a LWT message. Broker 2 gets killed shortly after connection so that the will message is sent from broker
|
|
627
|
+
const broker2Options = {
|
|
628
|
+
id: 'mqtt.broker2',
|
|
629
|
+
name: 'mqtt_broker2',
|
|
630
|
+
protocolVersion: 5,
|
|
631
|
+
willTopic: baseTopic + '/will',
|
|
632
|
+
willPayload: '{"msg":"will"}',
|
|
633
|
+
willQos: 2,
|
|
634
|
+
willMsg: {
|
|
635
|
+
contentType: 'application/json',
|
|
636
|
+
userProps: { will: 'value' },
|
|
637
|
+
respTopic: baseTopic + '/resp',
|
|
638
|
+
correl: Buffer.from('abc'),
|
|
639
|
+
expiry: 2000,
|
|
640
|
+
payloadFormatIndicator: true
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const expectMsg = {
|
|
644
|
+
topic: broker2Options.willTopic,
|
|
645
|
+
payload: broker2Options.willPayload,
|
|
646
|
+
qos: broker2Options.willQos,
|
|
647
|
+
contentType: broker2Options.willMsg.contentType,
|
|
648
|
+
userProperties: broker2Options.willMsg.userProps,
|
|
649
|
+
responseTopic: broker2Options.willMsg.respTopic,
|
|
650
|
+
correlationData: broker2Options.willMsg.correl,
|
|
651
|
+
messageExpiryInterval: broker2Options.willMsg.expiry
|
|
652
|
+
// payloadFormatIndicator: broker2Options.willMsg.payloadFormatIndicator,
|
|
653
|
+
}
|
|
654
|
+
const { flow, nodes } = buildBasicMQTTSendRecvFlow(broker1Options, { broker: broker1Options.id, topic: broker2Options.willTopic, datatype: 'utf8' }, { broker: broker2Options.id })
|
|
655
|
+
flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, nodes.mqtt_broker1.broker, nodes.mqtt_broker1.port, broker2Options)) // add second broker with will msg set
|
|
656
|
+
helper.load(mqttNodes, flow, function () {
|
|
657
|
+
const helperNode = helper.getNode('helper.node')
|
|
658
|
+
const mqttBroker1 = helper.getNode('mqtt.broker1')
|
|
659
|
+
const mqttBroker2 = helper.getNode('mqtt.broker2')
|
|
660
|
+
waitBrokerConnect([mqttBroker1, mqttBroker2])
|
|
661
|
+
.then(() => {
|
|
662
|
+
// connected - add the on handler and call to disconnect
|
|
663
|
+
helperNode.on('input', function (msg) {
|
|
664
|
+
try {
|
|
665
|
+
compareMsgToExpected(msg, expectMsg)
|
|
666
|
+
done()
|
|
667
|
+
} catch (error) {
|
|
668
|
+
done(error)
|
|
669
|
+
}
|
|
670
|
+
})
|
|
671
|
+
mqttBroker2.client.end(true) // force closure
|
|
672
|
+
})
|
|
673
|
+
.catch(done)
|
|
674
|
+
})
|
|
675
|
+
})
|
|
676
|
+
// #endregion ADVANCED TESTS
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
// #region ################### HELPERS ################### #//
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* A basic unit test that builds a flow containing 1 broker, 1 mqtt-in, one mqtt-out and a helper.
|
|
683
|
+
* It performs the following steps: builds flow, loads flow, waits for connection, sends `sendMsg`,
|
|
684
|
+
* waits for msg then compares `sendMsg` to `expectMsg`, and finally calls `done`
|
|
685
|
+
* @param {object} brokerOptions anything that can be set in an MQTTBrokerNode (e.g. id, name, url, broker, server, port, protocolVersion, ...)
|
|
686
|
+
* @param {object} inNodeOptions anything that can be set in an MQTTInNode (e.g. id, name, broker, topic, rh, nl, rap, ... )
|
|
687
|
+
* @param {object} outNodeOptions anything that can be set in an MQTTOutNode (e.g. id, name, broker, ...)
|
|
688
|
+
* @param {object} options an object for passing in test properties like `sendMsg` and `expectMsg`
|
|
689
|
+
* @param {object} hooks an object containing hook functions...
|
|
690
|
+
* * [fn] `done()` - the tests done function. If excluded, an error will be thrown upon test error
|
|
691
|
+
* * [fn] `beforeLoad(flow)` - provides opportunity to adjust the flow JSON before loading into runtime
|
|
692
|
+
* * [fn] `afterLoad(helperNode, mqttBroker, mqttIn, mqttOut)` - called before connection attempt
|
|
693
|
+
* * [fn] `afterConnect(helperNode, mqttBroker, mqttIn, mqttOut)` - called before connection attempt
|
|
694
|
+
*/
|
|
695
|
+
function testSendRecv (brokerOptions, inNodeOptions, outNodeOptions, options, hooks) {
|
|
696
|
+
options = options || {}
|
|
697
|
+
brokerOptions = brokerOptions || {}
|
|
698
|
+
inNodeOptions = inNodeOptions || {}
|
|
699
|
+
outNodeOptions = outNodeOptions || {}
|
|
700
|
+
const sendMsg = options.sendMsg || {}
|
|
701
|
+
sendMsg.topic = sendMsg.topic || nextTopic()
|
|
702
|
+
const expectMsg = options.expectMsg || Object.assign({}, sendMsg)
|
|
703
|
+
expectMsg.payload = inNodeOptions.payload === undefined ? expectMsg.payload : inNodeOptions.payload
|
|
704
|
+
if (inNodeOptions.topicType !== 'dynamic') {
|
|
705
|
+
inNodeOptions.topic = inNodeOptions.topic || sendMsg.topic
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const { flow, nodes } = buildBasicMQTTSendRecvFlow(inNodeOptions, outNodeOptions)
|
|
709
|
+
if (hooks.beforeLoad) { hooks.beforeLoad(flow) }
|
|
710
|
+
helper.load(mqttNodes, flow, function () {
|
|
711
|
+
let finished = false
|
|
712
|
+
try {
|
|
713
|
+
const helperNode = helper.getNode('helper.node')
|
|
714
|
+
const mqttIn = helper.getNode(nodes.mqtt_in.id)
|
|
715
|
+
const mqttOut = helper.getNode(nodes.mqtt_out.id)
|
|
716
|
+
const mqttBroker = mqttIn.brokerConn || mqttOut.brokerConn
|
|
717
|
+
let afterLoadHandled = false
|
|
718
|
+
if (hooks.afterLoad) {
|
|
719
|
+
afterLoadHandled = hooks.afterLoad(helperNode, mqttBroker, mqttIn, mqttOut)
|
|
720
|
+
}
|
|
721
|
+
if (!afterLoadHandled) {
|
|
722
|
+
helperNode.on('input', function (msg) {
|
|
723
|
+
finished = true
|
|
724
|
+
try {
|
|
725
|
+
compareMsgToExpected(msg, expectMsg)
|
|
726
|
+
if (hooks.done) { hooks.done() }
|
|
727
|
+
} catch (err) {
|
|
728
|
+
if (hooks.done) { hooks.done(err) } else { throw err }
|
|
729
|
+
}
|
|
730
|
+
})
|
|
731
|
+
}
|
|
732
|
+
waitBrokerConnect(mqttBroker)
|
|
733
|
+
.then(() => {
|
|
734
|
+
// finally, connected!
|
|
735
|
+
if (hooks.afterConnect) {
|
|
736
|
+
const handled = hooks.afterConnect(helperNode, mqttBroker, mqttIn, mqttOut)
|
|
737
|
+
if (handled) { return }
|
|
738
|
+
}
|
|
739
|
+
if (sendMsg.topic) {
|
|
740
|
+
if (mqttIn.isDynamic) {
|
|
741
|
+
mqttIn.receive({ action: 'subscribe', topic: sendMsg.topic })
|
|
742
|
+
}
|
|
743
|
+
mqttOut.receive(sendMsg)
|
|
744
|
+
}
|
|
745
|
+
})
|
|
746
|
+
.catch((e) => {
|
|
747
|
+
if (finished) { return }
|
|
748
|
+
if (hooks.done) { hooks.done(e) } else { throw e }
|
|
749
|
+
})
|
|
750
|
+
} catch (err) {
|
|
751
|
+
if (finished) { return }
|
|
752
|
+
if (hooks.done) { hooks.done(err) } else { throw err }
|
|
753
|
+
}
|
|
754
|
+
})
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Builds a flow containing 2 parts.
|
|
759
|
+
* * 1: MQTT Out node (with broker configured).
|
|
760
|
+
* * 2: MQTT In node (with broker configured) --> helper node `id:helper.node`
|
|
761
|
+
*/
|
|
762
|
+
function buildBasicMQTTSendRecvFlow (inOptions, outOptions) {
|
|
763
|
+
const inNode = buildMQTTInNode(inOptions.id, inOptions.name, inOptions.topic, inOptions, ['helper.node'])
|
|
764
|
+
const outNode = buildMQTTOutNode(outOptions.id, outOptions.name, outOptions.topic, outOptions)
|
|
765
|
+
const helper = buildNode('helper', 'helper.node', 'helper_node', {})
|
|
766
|
+
const catchNode = buildNode('catch', 'catch.node', 'catch_node', { scope: ['mqtt.in'] }, ['helper.node'])
|
|
767
|
+
return {
|
|
768
|
+
nodes: {
|
|
769
|
+
[inNode.name]: inNode,
|
|
770
|
+
[outNode.name]: outNode,
|
|
771
|
+
[helper.name]: helper,
|
|
772
|
+
[catchNode.name]: catchNode
|
|
773
|
+
},
|
|
774
|
+
flow: [inNode, outNode, helper, catchNode]
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function buildMQTTBrokerNode (id, name, brokerHost, brokerPort, options) {
|
|
779
|
+
// url,broker,port,clientid,autoConnect,usetls,usews,verifyservercert,compatmode,protocolVersion,keepalive,
|
|
780
|
+
// cleansession,sessionExpiry,topicAliasMaximum,maximumPacketSize,receiveMaximum,userProperties,userPropertiesType,autoUnsubscribe
|
|
781
|
+
options = options || {}
|
|
782
|
+
const node = buildNode('mqtt-broker', id || 'mqtt.broker', name || 'mqtt_broker', options)
|
|
783
|
+
node.url = options.url
|
|
784
|
+
node.broker = brokerHost || options.broker || BROKER_HOST
|
|
785
|
+
node.port = brokerPort || options.port || BROKER_PORT
|
|
786
|
+
node.clientid = options.clientid || ''
|
|
787
|
+
node.cleansession = String(options.cleansession) !== 'false'
|
|
788
|
+
node.autoUnsubscribe = String(options.autoUnsubscribe) !== 'false'
|
|
789
|
+
node.autoConnect = String(options.autoConnect) !== 'false'
|
|
790
|
+
node.sessionExpiry = options.sessionExpiry ? options.sessionExpiry : undefined
|
|
791
|
+
|
|
792
|
+
if (options.birthTopic) {
|
|
793
|
+
node.birthTopic = options.birthTopic
|
|
794
|
+
node.birthQos = options.birthQos || '0'
|
|
795
|
+
node.birthPayload = options.birthPayload || ''
|
|
796
|
+
}
|
|
797
|
+
if (options.closeTopic) {
|
|
798
|
+
node.closeTopic = options.closeTopic
|
|
799
|
+
node.closeQos = options.closeQos || '0'
|
|
800
|
+
node.closePayload = options.closePayload || ''
|
|
801
|
+
}
|
|
802
|
+
if (options.willTopic) {
|
|
803
|
+
node.willTopic = options.willTopic
|
|
804
|
+
node.willQos = options.willQos || '0'
|
|
805
|
+
node.willPayload = options.willPayload || ''
|
|
806
|
+
}
|
|
807
|
+
updateNodeOptions(options, node)
|
|
808
|
+
return node
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function buildMQTTInNode (id, name, topic, options, wires) {
|
|
812
|
+
// { "id": "mqtt.in", "type": "mqtt in", "name": "mqtt_in", "topic": "test/in", "qos": "2", "datatype": "auto", "broker": "mqtt.broker", "nl": false, "rap": true, "rh": 0, "inputs": 0, "wires": [["mqtt.out"]] }
|
|
813
|
+
options = options || {}
|
|
814
|
+
const node = buildNode('ff-mqtt-in', id || 'mqtt.in', name || 'mqtt_in', options)
|
|
815
|
+
node.topic = topic || ''
|
|
816
|
+
node.topicType = options.topicType === 'dynamic' ? 'dynamic' : 'static'
|
|
817
|
+
node.inputs = options.topicType === 'dynamic' ? 1 : 0
|
|
818
|
+
updateNodeOptions(node, options, wires)
|
|
819
|
+
return node
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function buildMQTTOutNode (id, name, topic, options) {
|
|
823
|
+
// { "id": "mqtt.out", "type": "mqtt out", "name": "mqtt_out", "topic": "test/out", "qos": "", "retain": "", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": brokerId, "wires": [] },
|
|
824
|
+
options = options || {}
|
|
825
|
+
options.broker = options.broker || 'mqtt.broker'
|
|
826
|
+
const node = buildNode('ff-mqtt-out', id || 'mqtt.out', name || 'mqtt_out', options)
|
|
827
|
+
node.topic = topic || ''
|
|
828
|
+
updateNodeOptions(node, options, null)
|
|
829
|
+
return node
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function buildNode (type, id, name, options, wires) {
|
|
833
|
+
// { "id": "mqtt.in", "type": "mqtt in", "name": "mqtt_in", "topic": "test/in", "qos": "2", "datatype": "auto", "broker": "mqtt.broker", "nl": false, "rap": true, "rh": 0, "inputs": 0, "wires": [["mqtt.out"]] }
|
|
834
|
+
options = options || {}
|
|
835
|
+
const node = {
|
|
836
|
+
id: id || (type.replace(/[\W]/g, '.')),
|
|
837
|
+
type,
|
|
838
|
+
name: name || (type.replace(/[\W]/g, '_')),
|
|
839
|
+
wires: []
|
|
840
|
+
}
|
|
841
|
+
if (node.id.indexOf('.') === -1) { node.id += '.node' }
|
|
842
|
+
updateNodeOptions(node, options, wires)
|
|
843
|
+
return node
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function updateNodeOptions (node, options, wires) {
|
|
847
|
+
const keys = Object.keys(options)
|
|
848
|
+
for (let index = 0; index < keys.length; index++) {
|
|
849
|
+
const key = keys[index]
|
|
850
|
+
const val = options[key]
|
|
851
|
+
if (node[key] === undefined) {
|
|
852
|
+
node[key] = val
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (wires && Array.isArray(wires)) {
|
|
856
|
+
node.wires[0] = [...wires]
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function compareMsgToExpected (msg, expectMsg) {
|
|
861
|
+
msg.should.have.property('topic', expectMsg.topic)
|
|
862
|
+
msg.should.have.property('payload', expectMsg.payload)
|
|
863
|
+
if (hasProperty(expectMsg, 'retain')) { msg.retain.should.eql(expectMsg.retain) }
|
|
864
|
+
if (hasProperty(expectMsg, 'qos')) {
|
|
865
|
+
msg.qos.should.eql(expectMsg.qos)
|
|
866
|
+
} else {
|
|
867
|
+
msg.qos.should.eql(0)
|
|
868
|
+
}
|
|
869
|
+
if (hasProperty(expectMsg, 'userProperties')) { msg.should.have.property('userProperties', expectMsg.userProperties) }
|
|
870
|
+
if (hasProperty(expectMsg, 'contentType')) { msg.should.have.property('contentType', expectMsg.contentType) }
|
|
871
|
+
if (hasProperty(expectMsg, 'correlationData')) { msg.should.have.property('correlationData', expectMsg.correlationData) }
|
|
872
|
+
if (hasProperty(expectMsg, 'responseTopic')) { msg.should.have.property('responseTopic', expectMsg.responseTopic) }
|
|
873
|
+
if (hasProperty(expectMsg, 'payloadFormatIndicator')) { msg.should.have.property('payloadFormatIndicator', expectMsg.payloadFormatIndicator) }
|
|
874
|
+
if (hasProperty(expectMsg, 'messageExpiryInterval')) { msg.should.have.property('messageExpiryInterval', expectMsg.messageExpiryInterval) }
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function waitBrokerConnect (broker, timeLimit) {
|
|
878
|
+
const waitConnected = (broker, timeLimit) => {
|
|
879
|
+
const brokers = Array.isArray(broker) ? broker : [broker]
|
|
880
|
+
timeLimit = timeLimit || 1000
|
|
881
|
+
return new Promise((resolve, reject) => {
|
|
882
|
+
let timer; let resolved = false
|
|
883
|
+
timer = wait()
|
|
884
|
+
function wait () {
|
|
885
|
+
if (brokers.every(e => e.connected === true)) {
|
|
886
|
+
resolved = true
|
|
887
|
+
clearTimeout(timer)
|
|
888
|
+
resolve()
|
|
889
|
+
} else {
|
|
890
|
+
timeLimit = timeLimit - 15
|
|
891
|
+
if (timeLimit <= 0) {
|
|
892
|
+
if (!resolved) {
|
|
893
|
+
// eslint-disable-next-line prefer-promise-reject-errors
|
|
894
|
+
reject('Timeout waiting broker connect')
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
timer = setTimeout(wait, 15)
|
|
898
|
+
return timer
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
})
|
|
902
|
+
}
|
|
903
|
+
return waitConnected(broker, timeLimit)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function hasProperty (obj, propName) {
|
|
907
|
+
return Object.prototype.hasOwnProperty.call(obj, propName)
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const baseTopic = 'nr' + Date.now().toString() + '/'
|
|
911
|
+
let topicNo = 0
|
|
912
|
+
function nextTopic (topic) {
|
|
913
|
+
topicNo++
|
|
914
|
+
if (!topic) { topic = 'unittest' }
|
|
915
|
+
if (topic.startsWith('/')) { topic = topic.substring(1) }
|
|
916
|
+
if (topic.startsWith(baseTopic)) { return topic + String(topicNo) }
|
|
917
|
+
return (baseTopic + topic + String(topicNo))
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// #endregion HELPERS
|