From 25f4fbf2bbab8b7c0129ba54e1188bce9fa5292e Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Mon, 21 Feb 2022 20:03:25 +0000 Subject: [PATCH 01/20] undo regression to tcp-in node To fix #3454 --- packages/node_modules/@node-red/nodes/core/network/31-tcpin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/network/31-tcpin.js b/packages/node_modules/@node-red/nodes/core/network/31-tcpin.js index 24e6abf7e..9e81d5056 100644 --- a/packages/node_modules/@node-red/nodes/core/network/31-tcpin.js +++ b/packages/node_modules/@node-red/nodes/core/network/31-tcpin.js @@ -229,7 +229,7 @@ module.exports = function(RED) { buffer = buffer+data; var parts = buffer.split(node.newline); for (var i = 0; i Date: Fri, 25 Feb 2022 16:06:44 +0000 Subject: [PATCH 02/20] undo regression in tcp-in node (missed one) and add test --- .../node_modules/@node-red/nodes/core/network/31-tcpin.js | 2 +- test/nodes/core/network/31-tcpin_spec.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/network/31-tcpin.js b/packages/node_modules/@node-red/nodes/core/network/31-tcpin.js index 9e81d5056..a2f033fa3 100644 --- a/packages/node_modules/@node-red/nodes/core/network/31-tcpin.js +++ b/packages/node_modules/@node-red/nodes/core/network/31-tcpin.js @@ -135,7 +135,7 @@ module.exports = function(RED) { buffer = buffer+data; var parts = buffer.split(node.newline); for (var i = 0; i Date: Sat, 26 Feb 2022 18:10:17 +0800 Subject: [PATCH 03/20] fix html label mistake (#3459) --- packages/node_modules/@node-red/nodes/core/parsers/70-JSON.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/parsers/70-JSON.html b/packages/node_modules/@node-red/nodes/core/parsers/70-JSON.html index 6599cf0e0..82e1ccd0d 100644 --- a/packages/node_modules/@node-red/nodes/core/parsers/70-JSON.html +++ b/packages/node_modules/@node-red/nodes/core/parsers/70-JSON.html @@ -21,7 +21,7 @@
-
From 97ebe33d688c7f59b5099f4a8feeaf275b24a85c Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 28 Feb 2022 18:15:13 +0000 Subject: [PATCH 04/20] Dont show 1st tab if hidden when loading fixes #3455 --- .../@node-red/editor-client/src/js/red.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 2b1c8f2e4..afcdbdb1a 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -252,8 +252,21 @@ var RED = (function() { if (/^#flow\/.+$/.test(currentHash)) { RED.workspaces.show(currentHash.substring(6),true); } - if (RED.workspaces.active() === 0 && RED.workspaces.count() > 0) { - RED.workspaces.show(RED.nodes.getWorkspaceOrder()[0]) + if (RED.workspaces.count() > 0) { + const hiddenTabs = JSON.parse(RED.settings.getLocal("hiddenTabs")||"{}"); + const workspaces = RED.nodes.getWorkspaceOrder(); + if (RED.workspaces.active() === 0) { + for (let index = 0; index < workspaces.length; index++) { + const ws = workspaces[index]; + if (!hiddenTabs[ws]) { + RED.workspaces.show(ws); + break; + } + } + } + if (RED.workspaces.active() === 0) { + RED.workspaces.show(workspaces[0]); + } } } catch(err) { console.warn(err); From cf1424976fd822cf4170c2bb4cde927ec4951719 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Fri, 4 Mar 2022 16:08:29 +0000 Subject: [PATCH 05/20] Improve scroll into view - if a node is behind scrollbar, it is not scrolled into view - jQuery `.width()` & `.width()` actually includes the scroll bar. - using native `clientWidth` and `clientHeight` fixes this --- packages/node_modules/@node-red/editor-client/src/js/ui/view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index a3c838ce1..57e233eda 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -5621,7 +5621,7 @@ RED.view = (function() { node.dirty = true; RED.workspaces.show(node.z); - var screenSize = [chart.width()/scaleFactor,chart.height()/scaleFactor]; + var screenSize = [chart[0].clientWidth/scaleFactor,chart[0].clientHeight/scaleFactor]; var scrollPos = [chart.scrollLeft()/scaleFactor,chart.scrollTop()/scaleFactor]; var cx = node.x; var cy = node.y; From 10b18de3e0311810e84079573a35ec67d8938dc9 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Sat, 5 Mar 2022 11:24:25 +0000 Subject: [PATCH 06/20] fix: ensure mqtt v5 props can be set false fixes #3471 --- packages/node_modules/@node-red/nodes/core/network/10-mqtt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js index bdf2b0e5b..1249181a6 100644 --- a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js +++ b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js @@ -103,7 +103,7 @@ module.exports = function(RED) { if(src[propName] === "true" || src[propName] === true) { dst[propName] = true; } else if(src[propName] === "false" || src[propName] === false) { - dst[propName] = true; + dst[propName] = false; } } else { if(def != undefined) dst[propName] = def; From a49927f1735c9408e436a0e6ee8fba50d739132b Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 8 Mar 2022 14:07:03 +0000 Subject: [PATCH 07/20] do JSON comparison of old value/new value fixes #3475 --- .../editor-client/src/js/ui/editors/panes/properties.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js index f3a5960e0..39aa63ed1 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js @@ -107,7 +107,7 @@ newValue = ""; } } - if (node[d] != newValue) { + if (!JSON.stringify(node[d]) === JSON.stringify(newValue)) { if (node._def.defaults[d].type) { // Change to a related config node var configNode = RED.nodes.node(node[d]); From 84a9cf7adf4bd0ba7df9b862476133fdecd915ae Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 8 Mar 2022 14:20:12 +0000 Subject: [PATCH 08/20] handle errors by circ refs, undefined, BigInt etc --- .../src/js/ui/editors/panes/properties.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js index 39aa63ed1..84505fb91 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js @@ -107,7 +107,7 @@ newValue = ""; } } - if (!JSON.stringify(node[d]) === JSON.stringify(newValue)) { + if (!isEqual(node[d], newValue)) { if (node._def.defaults[d].type) { // Change to a related config node var configNode = RED.nodes.node(node[d]); @@ -138,6 +138,22 @@ } } }); + /** + * Compares `v1` with `v2` for equality + * @param {*} v1 variable 1 + * @param {*} v2 variable 2 + * @returns {boolean} true if variable 1 equals variable 2, otherwise false + */ + function isEqual(v1, v2) { + try { + if(v1 === v2) { + return true; + } + return JSON.stringify(v1) === JSON.stringify(v2); + } catch (err) { + return false; + } + } /** * Update the node credentials from the edit form From 10f77fdf1a9d9f87576f901ab523040078008395 Mon Sep 17 00:00:00 2001 From: Stephen McLaughlin <44235289+Steve-Mcl@users.noreply.github.com> Date: Tue, 8 Mar 2022 23:13:41 +0000 Subject: [PATCH 09/20] permit non strict comparison of string or number --- .../src/js/ui/editors/panes/properties.js | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js index 84505fb91..5f704cc65 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js @@ -139,17 +139,23 @@ } }); /** - * Compares `v1` with `v2` for equality - * @param {*} v1 variable 1 - * @param {*} v2 variable 2 - * @returns {boolean} true if variable 1 equals variable 2, otherwise false + * Compares `newValue` with `originalValue` for equality. + * NOTES: + * * If `newValue` is a string or number, comparison is not strict + * * If `newValue` is anything else, comparison is strict + * @param {*} originalValue Original value + * @param {*} newValue New value + * @returns {boolean} true if originalValue equals newValue, otherwise false */ - function isEqual(v1, v2) { + function isEqual(originalValue, newValue) { try { - if(v1 === v2) { - return true; + if (typeof newValue === "string" || typeof newValue === "number") { + return originalValue == newValue; } - return JSON.stringify(v1) === JSON.stringify(v2); + if (typeof newValue === "boolean") { + return originalValue === newValue; + } + return JSON.stringify(originalValue) === JSON.stringify(newValue); } catch (err) { return false; } From a8579fa68a1272f46cc3be47527d0dc774bb55d1 Mon Sep 17 00:00:00 2001 From: Bruno Henriques Date: Wed, 9 Mar 2022 10:39:47 +0000 Subject: [PATCH 10/20] fix nodes losing their wires when in an iframe --- packages/node_modules/@node-red/editor-client/src/js/nodes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index ab7fae401..7ef65c445 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -2135,7 +2135,7 @@ RED.nodes = (function() { n = new_nodes[i]; if (n.wires) { for (var w1=0;w1 Date: Thu, 10 Mar 2022 11:22:59 +0000 Subject: [PATCH 11/20] backward compatible equality testing of immutables - make non object equality tests non strict - this aligns with prior condition --- .../src/js/ui/editors/panes/properties.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js index 5f704cc65..cfa72be10 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js @@ -138,22 +138,17 @@ } } }); + /** * Compares `newValue` with `originalValue` for equality. - * NOTES: - * * If `newValue` is a string or number, comparison is not strict - * * If `newValue` is anything else, comparison is strict * @param {*} originalValue Original value * @param {*} newValue New value * @returns {boolean} true if originalValue equals newValue, otherwise false */ - function isEqual(originalValue, newValue) { + function isEqual(originalValue, newValue) { try { - if (typeof newValue === "string" || typeof newValue === "number") { - return originalValue == newValue; - } - if (typeof newValue === "boolean") { - return originalValue === newValue; + if(originalValue == newValue) { + return true; } return JSON.stringify(originalValue) === JSON.stringify(newValue); } catch (err) { From 497d63e67ee1dbf090a74d37dd102328eee1fe61 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Sat, 19 Mar 2022 17:29:31 +0000 Subject: [PATCH 12/20] Add unit tests for MQTT nodes --- test/nodes/core/network/21-mqtt_spec.js | 693 ++++++++++++++++++++++++ 1 file changed, 693 insertions(+) create mode 100644 test/nodes/core/network/21-mqtt_spec.js diff --git a/test/nodes/core/network/21-mqtt_spec.js b/test/nodes/core/network/21-mqtt_spec.js new file mode 100644 index 000000000..d6fd74027 --- /dev/null +++ b/test/nodes/core/network/21-mqtt_spec.js @@ -0,0 +1,693 @@ +/** + * Copyright JS Foundation and other contributors, mqtt://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +/* These tests are only supposed to be executed at development time (for now)*/ + +const should = require("should"); +const helper = require("node-red-node-test-helper"); +const mqttNodes = require("nr-test-utils").require("@node-red/nodes/core/network/10-mqtt.js"); +const server = process.env.MQTT_BROKER_SERVER || "localhost"; +const port = process.env.MQTT_BROKER_PORT || 1883; +const skipTests = process.env.MQTT_SKIP_TESTS == "true"; + +describe('MQTT Nodes', function () { + + before(function (done) { + helper.startServer(done); + }); + + after(function (done) { + helper.stopServer(done); + }); + + afterEach(function () { + try { + helper.unload(); + } catch (error) { + } + }); + + it('should be loaded and have default values', function (done) { + this.timeout = 1000; + const brokerName = "mqtt_broker", brokerId = "mqtt.broker", brokerOptions = { autoConnect: false }; + const inName = "mqtt_in", inId = "mqtt.in"; + const outName = "mqtt_out", outId = "mqtt.out"; + const flow = [ + buildMQTTInNode(inId, inName, brokerId, "test/in", {}, [outId]), + buildMQTTOutNode(outId, outName, brokerId, "test/out", {}), + buildMQTTBrokerNode(brokerId, brokerName, server, port, brokerOptions), + ]; + helper.load(mqttNodes, flow, function () { + try { + const mqttIn = helper.getNode(inId); + const mqttOut = helper.getNode(outId); + const mqttBroker = helper.getNode(brokerId); + + should(mqttIn).be.type("object", "mqtt in node should be an object") + mqttIn.should.have.property('broker', brokerId); + mqttIn.should.have.property('datatype', 'utf8'); //default: 'utf8' + mqttIn.should.have.property('isDynamic', false); //default: false + mqttIn.should.have.property('inputs', 0); //default: 0 + mqttIn.should.have.property('qos', 2); //default: 2 + mqttIn.should.have.property('wires', [[outId]]); + + should(mqttOut).be.type("object", "mqtt out node should be an object") + mqttOut.should.have.property('broker', brokerId); + + should(mqttBroker).be.type("object", "mqtt broker node should be an object") + mqttBroker.should.have.property('broker', server); + mqttBroker.should.have.property('port', port); + mqttBroker.should.have.property('brokerurl'); + // mqttBroker.should.have.property('autoUnsubscribe', true);//default: true + mqttBroker.should.have.property('autoConnect', false);//Set "autoConnect:false" in brokerOptions + mqttBroker.should.have.property('options'); + mqttBroker.options.should.have.property('clean', true); + mqttBroker.options.should.have.property('clientId'); + mqttBroker.options.clientId.should.containEql('nodered_'); + mqttBroker.options.should.have.property('keepalive').type("number"); + mqttBroker.options.should.have.property('reconnectPeriod').type("number"); + done(); + } catch (error) { + done(error) + } + }); + }); + + if (skipTests) { + it('should skip following MQTT tests (no broker available)', function (done) { + done(); + }); + } + + /** Conditional test runner (only run if skipTests=false) */ + function itConditional(title, test) { + return !skipTests ? it(title, test) : it.skip(title, test); + } + + //#region ################### BASIC TESTS ################### #// + itConditional('should send and receive string (auto)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const msg = { + topic: nextTopic(), + payload: "hello", + qos: 0 + } + const expectMsg = Object.assign({}, msg); + testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, msg, expectMsg, done); + }); + itConditional('should send JSON and receive string (auto)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const msg = { + topic: nextTopic(), + payload: '{"prop":"value1", "num":1}', + qos: 1 + } + const expectMsg = Object.assign({}, msg); + testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, msg, expectMsg, done); + }); + itConditional('should send JSON and receive string (utf8)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const msg = { + topic: nextTopic(), + payload: '{"prop":"value2", "num":2}', + qos: 2 + } + const expectMsg = Object.assign({}, msg); + testSendRecv({}, { datatype: "utf8", topicType: "static" }, {}, msg, expectMsg, done); + }); + itConditional('should send JSON and receive Object (json)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const msg = { + topic: nextTopic(), + payload: '{"prop":"value3", "num":3}'// send a string ... + } + const expectMsg = Object.assign({}, msg, {payload: {"prop":"value3", "num":3}});//expect an object + testSendRecv({}, { datatype: "json", topicType: "static" }, {}, msg, expectMsg, done); + }); + itConditional('should send String and receive Buffer (buffer)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const msg = { + topic: nextTopic(), + payload: "a b c" //send string ... + } + const expectMsg = Object.assign({}, msg, {payload: Buffer.from(msg.payload)});//expect Buffer.from(msg.payload) + testSendRecv({}, { datatype: "buffer", topicType: "static"}, {}, msg, expectMsg, done); + }); + itConditional('should send utf8 Buffer and receive String (auto)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const msg = { + topic: nextTopic(), + payload: Buffer.from([0x78, 0x20, 0x79, 0x20, 0x7a]) // "x y z" + } + const expectMsg = Object.assign({}, msg, {payload: "x y z"});//set expected payload to "x y z" + testSendRecv({}, { datatype: "auto", topicType: "static"}, {}, msg, expectMsg, done); + }); + itConditional('should send non utf8 Buffer and receive Buffer (auto)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const msg = { + topic: nextTopic(), + payload: Buffer.from([0xC0, 0xC1, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF]) //non valid UTF8 + } + const expectMsg = Object.assign({}, msg); + testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, msg, expectMsg, done); + }); + itConditional('should send/receive all v5 flags and settings', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const t = nextTopic(); + const msg = { + topic: t + "/command", + payload: Buffer.from("v5"), + qos: 1, + retain: true, + responseTopic: t + "/response", + userProperties: { prop1: "val1" }, + contentType: "application/json", + correlationData: Buffer.from([1, 2, 3]), + payloadFormatIndicator: true, + messageExpiryInterval: 2000, + } + const expectMsg = Object.assign({}, msg); + expectMsg.payload = expectMsg.payload.toString(); //auto mode + payloadFormatIndicator should make a string + delete expectMsg.payloadFormatIndicator; //Seems mqtt.js only publishes payloadFormatIndicator the will msg + const inOptions = { + datatype: "auto", topicType: "static", + qos: 1, nl: false, rap: true, rh: 1, + subscriptionIdentifier: 333 + } + testSendRecv({ protocolVersion: 5 }, inOptions, {}, msg, expectMsg, done); + }); + itConditional('should subscribe dynamically via action', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const msg = { + topic: nextTopic(), + payload: "abc" + } + const expectMsg = Object.assign({}, msg); + testSendRecv({ protocolVersion: 5 }, { datatype: "utf8", topicType: "dynamic" }, {}, msg, expectMsg, done); + }); + //#endregion BASIC TESTS + + //#region ################### ADVANCED TESTS ################### #// + itConditional('should connect via "connect" action', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const {flow, nodes} = buildBasicMQTTSendRecvFlow("mqtt.broker", "mqtt_broker", { autoConnect: false }, "mqtt.in", "mqtt_in", {}, null, null, null, {}); + flow.push({ "id": "status_node", "type": "status", "name": "status-node", "scope": ["mqtt.in"], "wires": [["helper.node"]] }); + + helper.load(mqttNodes, flow, function () { + const helperNode = helper.getNode("helper.node"); + const mqttIn = helper.getNode("mqtt.in"); + const mqttBroker = helper.getNode("mqtt.broker"); + try { + mqttBroker.should.have.property("autoConnect", false); + mqttBroker.should.have.property("connecting", false);//should not attempt to connect (autoConnect:false) + } catch (error) { + done(error) + } + mqttIn.receive({ "action": "connect" }); + waitConnection(); + function waitConnection() { + if (!mqttBroker.connected) { + setTimeout(waitConnection, 15); + return; + } + done();//if we got here, it connected! + } + }); + }); + itConditional('should disconnect via "disconnect" action', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const {flow, nodes} = buildBasicMQTTSendRecvFlow("mqtt.broker", "mqtt_broker", {}, null, null, null, "mqtt.out", "mqtt_out", {}, {}); + flow.push({ "id": "statusnode", "type": "status", "name": "status-node", "scope": ["mqtt.out"], "wires": [["helper.node"]] });//add status node to watch mqtt_out + + helper.load(mqttNodes, flow, function () { + const helperNode = helper.getNode("helper.node"); + const mqttOut = helper.getNode("mqtt.out"); + const mqttBroker = helper.getNode("mqtt.broker"); + try { + mqttBroker.should.have.property("autoConnect", true); + mqttBroker.should.have.property("connecting", true);//should be trying to connect (autoConnect:true) + } catch (error) { + done(error) + } + waitConnection(); + function waitConnection() { + if (!mqttBroker.connected) { + setTimeout(waitConnection, 15); + return; + } + //connected - add the on handler and call to disconnect + helperNode.on("input", function (msg) { + try { + msg.status.should.have.property("text"); + msg.status.text.should.containEql('disconnect'); + done(); + } catch (error) { + done(error) + } + }) + mqttOut.receive({ "action": "disconnect" }); + } + }); + }); + itConditional('should publish birth message', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const baseTopic = nextTopic(); + const brokerOptions = { + protocolVersion: 4, + birthTopic: baseTopic + "/birth", + birthPayload: "broker connected", + birthQos: 2, + } + const msg = { topic: baseTopic + "/ignoreme"} + const expectMsg ={ + topic: brokerOptions.birthTopic, + payload: brokerOptions.birthPayload, + qos: brokerOptions.birthQos + }; + testSendRecv(brokerOptions, { topic: brokerOptions.birthTopic }, {}, msg, expectMsg, done); + }); + itConditional('should publish close message', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const baseTopic = nextTopic(); + const broker1Options = { id: "mqtt.broker1", name: "mqtt_broker1" } + const broker2Options = { id: "mqtt.broker2", name: "mqtt_broker2", closeTopic: baseTopic + "/close", closePayload: "broker disconnected", closeQos: 2,} + + const {flow} = buildBasicMQTTSendRecvFlow( + broker1Options.id, broker1Options.name || "mqtt_broker", broker1Options, + "mqtt.in", "mqtt_in", {broker: broker1Options.id, topic: broker2Options.closeTopic}, //should receive close msg of broker2 + "mqtt.out", "mqtt_out", {broker: broker2Options.id}, + ) + flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, server, port, broker2Options)) + + helper.load(mqttNodes, flow, function () { + const helperNode = helper.getNode("helper.node"); + const mqttOut = helper.getNode("mqtt.out"); + const mqttIn = helper.getNode("mqtt.in"); + const mqttBroker1 = helper.getNode("mqtt.broker1"); + const mqttBroker2 = helper.getNode("mqtt.broker2"); + waitConnection(); + function waitConnection() { + if (!mqttBroker1.connected || !mqttBroker2.connected) { + setTimeout(waitConnection, 15); + return; + } + //connected - add the on handler and call to disconnect + helperNode.on("input", function (msg) { + try { + msg.should.have.property("topic", broker2Options.closeTopic); + msg.should.have.property('payload', broker2Options.closePayload); + msg.should.have.property('qos', broker2Options.closeQos); + done(); + } catch (error) { + done(error) + } + }) + mqttOut.receive({ "action": "disconnect" }); + } + }); + }); + itConditional('should publish will message', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 1000; + const baseTopic = nextTopic(); + //Broker 1 - stays connected to receive the will message when broker 2 is killed + const broker1Options = { id: "mqtt.broker1", name: "mqtt_broker1" } + //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 + const broker2Options = { id: "mqtt.broker2", name: "mqtt_broker2", willTopic: baseTopic + "/will", willPayload: '{"msg":"will"}', willQos: 1,} + const expectMsg ={ + topic: broker2Options.willTopic, + payload: JSON.parse(broker2Options.willPayload), + qos: broker2Options.willQos + }; + + const {flow} = buildBasicMQTTSendRecvFlow( + broker1Options.id, broker1Options.name || "mqtt_broker", broker1Options, + "mqtt.in", "mqtt_in", {broker: broker1Options.id, topic: broker2Options.willTopic, datatype: "json"}, //should receive will msg of broker2 + "mqtt.out", "mqtt_out", {broker: broker2Options.id}, + ) + //add second broker + flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, server, port, broker2Options)) + + helper.load(mqttNodes, flow, function () { + const helperNode = helper.getNode("helper.node"); + const mqttBroker1 = helper.getNode("mqtt.broker1"); + const mqttBroker2 = helper.getNode("mqtt.broker2"); + waitConnection(); + function waitConnection() { + if (!mqttBroker1.connected || !mqttBroker2.connected) { + setTimeout(waitConnection, 15); + return; + } + //connected - add the on handler and call to disconnect + helperNode.on("input", function (msg) { + try { + compareMsgToExpected(msg, expectMsg); + done(); + } catch (error) { + done(error) + } + }); + mqttBroker2.client.end(true); //force closure + } + }); + }); + itConditional('should publish will message with V5 properties', function (done) { + if (skipTests) { return this.skip() } + // return this.skip(); //Issue receiving v5 props on will msg. Issue raised here: https://github.com/mqttjs/MQTT.js/issues/1455 + this.timeout = 1000; + const baseTopic = nextTopic(); + //Broker 1 - stays connected to receive the will message when broker 2 is killed + const broker1Options = { id: "mqtt.broker1", name: "mqtt_broker1", protocolVersion: 5, datatype: "utf8"} + //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 + const broker2Options = { + id: "mqtt.broker2", name: "mqtt_broker2", protocolVersion: 5, + willTopic: baseTopic + "/will", + willPayload: '{"msg":"will"}', + willQos: 2, + willMsg: { + contentType: 'application/json' , + userProps: {"will":"value"}, + respTopic: baseTopic+"/resp", + correl: Buffer.from("abc"), + expiry: 2000, + payloadFormatIndicator: true + } + } + const expectMsg ={ + topic: broker2Options.willTopic, + payload: broker2Options.willPayload, + qos: broker2Options.willQos, + contentType: broker2Options.willMsg.contentType, + userProperties: broker2Options.willMsg.userProps, + responseTopic: broker2Options.willMsg.respTopic, + correlationData: broker2Options.willMsg.correl, + messageExpiryInterval: broker2Options.willMsg.expiry, + // payloadFormatIndicator: broker2Options.willMsg.payloadFormatIndicator, + }; + const {flow} = buildBasicMQTTSendRecvFlow( + broker1Options.id, broker1Options.name || "mqtt_broker", broker1Options, + "mqtt.in", "mqtt_in", {broker: broker1Options.id, topic: broker2Options.willTopic}, //should receive will msg of broker2 + "mqtt.out", "mqtt_out", {broker: broker2Options.id}, + ) + //add second broker with will msg set + flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, server, port, broker2Options)) + + helper.load(mqttNodes, flow, function () { + const helperNode = helper.getNode("helper.node"); + const mqttBroker1 = helper.getNode("mqtt.broker1"); + const mqttBroker2 = helper.getNode("mqtt.broker2"); + waitConnection(); + function waitConnection() { + if (!mqttBroker1.connected || !mqttBroker2.connected) { + setTimeout(waitConnection, 15); + return; + } + //connected - add the on handler and call to disconnect + helperNode.on("input", function (msg) { + try { + compareMsgToExpected(msg, expectMsg); + done(); + } catch (error) { + done(error) + } + }); + mqttBroker2.client.end(true); //force closure of broker 2 to cause will msg + } + }); + }); + //#endregion ADVANCED TESTS +}); + +//#region ################### HELPERS ################### #// + +/** + * A basic unit test that builds a flow containg 1 broker, 1 mqtt-in, one mqtt-out and a helper. + * It performs the following steps: builds flow, loads flow, waits for connection, sends `sendMsg`, + * waits for msg then compares `sendMsg` to `expectMsg`, and finally calls `done` + * @param {object} brokerOptions anything that can be set in an MQTTBrokerNode (e.g. id, name, url, broker, server, port, protocolVersion, ...) + * @param {object} inNodeOptions anything that can be set in an MQTTInNode (e.g. id, name, broker, topic, rh, nl, rap, ... ) + * @param {object} outNodeOptions anything that can be set in an MQTTOutNode (e.g. id, name, broker, ...) + * @param {object} sendMsg the msg to send to broker + * @param {object} expectMsg the msg to send to broker + * @param {function} done the test runner `done` callback + */ +function testSendRecv(brokerOptions, inNodeOptions, outNodeOptions, sendMsg, expectMsg, done, customTests) { + sendMsg = sendMsg || {}; + brokerOptions = brokerOptions || {}; + inNodeOptions = inNodeOptions || {}; + outNodeOptions = outNodeOptions || {}; + brokerOptions.id = brokerOptions.id || "mqtt.broker"; + inNodeOptions.id = inNodeOptions.id || "mqtt.in"; + outNodeOptions.id = outNodeOptions.id || "mqtt.out"; + inNodeOptions.brokerId = inNodeOptions.brokerId || brokerOptions.id; + outNodeOptions.id = outNodeOptions.id || brokerOptions.id; + sendMsg.topic = sendMsg.topic || nextTopic(); + + if(inNodeOptions.topicType != "dynamic" ) { + inNodeOptions.topic = inNodeOptions.topic || sendMsg.topic; + } + outNodeOptions.topic = outNodeOptions.topic ? outNodeOptions.topic : sendMsg.topic; + + const {flow} = buildBasicMQTTSendRecvFlow( + brokerOptions.id, brokerOptions.name || "mqtt_broker", brokerOptions, + inNodeOptions.id, inNodeOptions.name, inNodeOptions, + outNodeOptions.id, outNodeOptions.name, outNodeOptions, + ) + + expectMsg = expectMsg || Object.assign({}, sendMsg); + expectMsg.payload = inNodeOptions.payload === undefined ? expectMsg.payload : inNodeOptions.payload; + + helper.load(mqttNodes, flow, function () { + try { + const helperNode = helper.getNode("helper.node"); + const mqttBroker = helper.getNode(brokerOptions.id); + const mqttIn = helper.getNode(inNodeOptions.id); + const mqttOut = helper.getNode(outNodeOptions.id); + + helperNode.on("input", function (msg) { + try { + if (customTests) { + customTests(msg, helperNode, mqttBroker, mqttIn, mqttOut) + } else { + compareMsgToExpected(msg, expectMsg); + } + done(); + } catch (err) { + done(err); + } + }); + waitConnection(); + function waitConnection() { + if (!mqttBroker.connected) { + setTimeout(waitConnection, 15); + return; + } + //finally, connected! + if (mqttIn.isDynamic) { + mqttIn.receive({ "action": "subscribe", "topic": sendMsg.topic }) + } + mqttOut.receive(sendMsg); + } + } catch (error) { + done(error) + } + }); +} + +/** + * Builds a flow from an array of {type:string, id:string, name:string, options:object, wires:[string]} + * @param {[{type:string, id:string, name:string, options:object, wires:[string]}]} nodes + * @returns {{[flow: [], nodes: {}]}} Returns `{[flow: [], nodes: {}]}` + */ +function buildFlow(nodes) { + const result = {flow: [], nodes: {}}; + nodes.forEach(node => { + //const flow = [ { "id": "helper.node", "type": "helper", "wires": [] } ]; + node.options = node.options || {}; + switch (node.type) { + case "mqtt-broker": + result.nodes[node.id] = buildMQTTBrokerNode(node.id, node.name, node.options.server, node.options.port, node.options); + break; + case "mqtt in": + result.nodes[node.id] = buildMQTTInNode(node.id, node.name, node.options.brokerId, node.options.topic, node.options); + break; + case "mqtt out": + result.nodes[node.id] = buildMQTTOutNode(node.id, node.name, node.options.brokerId, node.options.topic, node.options); + break; + default: + result.nodes[node.id] = buildNode(node.type, node.id, node.name, node.options); + break; + } + if (node.wires && Array.isArray(node.wires)) { + result.nodes[node.id].wires[0] = [...node.wires]; + } + result.flow.push(result.nodes[node.id]); + }) + return result; +} + +/** + * Builds a flow containing 2 parts. Part1: MQTT Out node. Part2: MQTT In node --> helper node + * If inXxx is excluded, there will be no in node + * If outXxx is excluded, there will be no out node +*/ +function buildBasicMQTTSendRecvFlow(brokerId, brokerName, brokerOptions, inId, inName, inOptions, outId, outName, outOptions) { + var nodes = []; + brokerOptions = brokerOptions || {}; + outOptions = outOptions || {}; + inOptions = inOptions || {}; + + brokerOptions.server = brokerOptions.server || server; + brokerOptions.port = brokerOptions.port || port; + brokerOptions.autoConnect = String(brokerOptions.autoConnect) == "false" ? false : true; + + outOptions.broker = outOptions.broker || brokerId; + inOptions.broker = inOptions.broker || brokerId; + + nodes.push({type:"mqtt-broker", id: brokerId, name: brokerName, options: brokerOptions}); + if(inId) { nodes.push({type:"mqtt in", id: inId, name: inName, options: inOptions, wires: ["helper.node"]}); } + if(outId) { nodes.push({type:"mqtt out", id: outId, name: outName, options: outOptions}); } + nodes.push({type:"helper", id: "helper.node", name: "helper_node", options: {}}); + return buildFlow(nodes); +} + +function buildMQTTBrokerNode(id, name, server, port, options) { + // url,broker,port,clientid,autoConnect,usetls,usews,verifyservercert,compatmode,protocolVersion,keepalive, + //cleansession,sessionExpiry,topicAliasMaximum,maximumPacketSize,receiveMaximum,userProperties,userPropertiesType,autoUnsubscribe + options = options || {}; + const node = buildNode("mqtt-broker", id, name, options); + node.broker = server; + node.port = port; + node.url = options.url; + node.clientid = options.clientid || ""; + node.cleansession = String(options.cleansession) == "false" ? false : true; + node.autoUnsubscribe = String(options.autoUnsubscribe) == "false" ? false : true; + node.autoConnect = String(options.autoConnect) == "false" ? false : true; + + if (options.birthTopic) { + node.birthTopic = options.birthTopic; + node.birthQos = options.birthQos || "0"; + node.birthPayload = options.birthPayload || ""; + } + if (options.closeTopic) { + node.closeTopic = options.closeTopic; + node.closeQos = options.closeQos || "0"; + node.closePayload = options.closePayload || ""; + } + if (options.willTopic) { + node.willTopic = options.willTopic; + node.willQos = options.willQos || "0"; + node.willPayload = options.willPayload || ""; + } + updateNodeOptions(options, node); + return node; +} + +function buildMQTTInNode(id, name, brokerId, topic, options, wires) { + //{ "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"]] } + options = options || {}; + const node = buildNode("mqtt in", id, name, options); + node.topic = topic || ""; + node.broker = brokerId; + node.topicType = options.topicType == "dynamic" ? "dynamic" : "static", + node.inputs = options.topicType == "dynamic" ? 1 : 0, + updateNodeOptions(node, options, wires); + return node; +} + +function buildMQTTOutNode(id, name, brokerId, topic, options) { + //{ "id": "mqtt.out", "type": "mqtt out", "name": "mqtt_out", "topic": "test/out", "qos": "", "retain": "", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": brokerId, "wires": [] }, + options = options || {}; + const node = buildNode("mqtt out", id, name, options); + node.topic = topic || ""; + node.broker = brokerId; + updateNodeOptions(node, options, null); + return node; +} + +function buildNode(type, id, name, options, wires) { + //{ "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"]] } + options = options || {}; + const ts = String(Date.now()); + const node = { + "id": id || (type + "." + ts), + "type": type, + "name": name || (type.replace(/[\W]/g,"_") + "_" + ts), + "wires": [] + } + updateNodeOptions(node, options, wires); + return node; +} + +function updateNodeOptions(node, options, wires) { + let keys = Object.keys(options); + for (let index = 0; index < keys.length; index++) { + const key = keys[index]; + const val = options[key]; + if (node[key] === undefined) { + node[key] = val; + } + } + if (wires && Array.isArray(wires)) { + node.wires[0] = [...wires]; + } +} + +function compareMsgToExpected(msg, expectMsg) { + msg.should.have.property("topic", expectMsg.topic); + msg.should.have.property("payload", expectMsg.payload); + if (hasProperty(expectMsg, "retain")) { msg.retain.should.eql(expectMsg.retain); } + if (hasProperty(expectMsg, "qos")) { + msg.qos.should.eql(expectMsg.qos); + } else { + msg.qos.should.eql(0); + } + if (hasProperty(expectMsg, "userProperties")) { msg.should.have.property("userProperties", expectMsg.userProperties); } + if (hasProperty(expectMsg, "contentType")) { msg.should.have.property("contentType", expectMsg.contentType); } + if (hasProperty(expectMsg, "correlationData")) { msg.should.have.property("correlationData", expectMsg.correlationData); } + if (hasProperty(expectMsg, "responseTopic")) { msg.should.have.property("responseTopic", expectMsg.responseTopic); } + if (hasProperty(expectMsg, "payloadFormatIndicator")) { msg.should.have.property("payloadFormatIndicator", expectMsg.payloadFormatIndicator); } + if (hasProperty(expectMsg, "messageExpiryInterval")) { msg.should.have.property("messageExpiryInterval", expectMsg.messageExpiryInterval); } +} + +function hasProperty(obj, propName) { + return Object.prototype.hasOwnProperty.call(obj, propName); +} + +const base_topic = "nr" + Date.now().toString() + "/"; +let topicNo = 0; +function nextTopic(topic) { + topicNo++; + if (!topic) { topic = "unittest" } + if (topic.startsWith("/")) { topic = topic.substring(1); } + if (topic.startsWith(base_topic)) { return topic + String(topicNo) } + return (base_topic + topic + String(topicNo)); +} + +//#endregion HELPERS \ No newline at end of file From 8b6678a45323ce7e53ad021ab8a1d0e414890c37 Mon Sep 17 00:00:00 2001 From: Mauricio Bonani Date: Sun, 20 Mar 2022 15:40:52 -0400 Subject: [PATCH 13/20] Add the ability to customize diff colors even more --- .../@node-red/editor-client/src/sass/colors.scss | 2 ++ .../node_modules/@node-red/editor-client/src/sass/diff.scss | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss index 40dbeea06..92231b735 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss @@ -151,6 +151,8 @@ $popover-button-border-color-hover: #666; $diff-text-header-color: $secondary-text-color; $diff-text-header-background: #ffd; $diff-text-header-background-hover: #ffc; +$diff-state-color: $primary-text-color; +$diff-state-prefix-color: $secondary-text-color; $diff-state-added: #009900; $diff-state-deleted: #f80000; $diff-state-changed: #f89406; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/diff.scss b/packages/node_modules/@node-red/editor-client/src/sass/diff.scss index 2a8bf689c..38fd36252 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/diff.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/diff.scss @@ -562,7 +562,7 @@ ul.red-ui-deploy-dialog-confirm-list { width: 30px; display: inline-block; text-align: center; - color: $secondary-text-color; + color: $diff-state-prefix-color; } &.added { @@ -577,9 +577,11 @@ ul.red-ui-deploy-dialog-confirm-list { } td.added { background: $diff-state-added-background; + color: $diff-state-color; } td.removed { background: $diff-state-deleted-background; + color: $diff-state-color; } tr.mergeHeader td { color: $diff-merge-header-color; @@ -652,7 +654,7 @@ ul.red-ui-deploy-dialog-confirm-list { font-family: $monospace-font; padding: 5px 10px; text-align: left; - color: $secondary-text-color; + color: $diff-text-header-color; background: $diff-text-header-background; height: 30px; vertical-align: middle; From fad13254277217984aa15b1e0909b6864fc42add Mon Sep 17 00:00:00 2001 From: ralphwetzel Date: Sun, 20 Mar 2022 23:59:24 +0100 Subject: [PATCH 14/20] Fix: Sidebar "Configuration" filter button tooltip "Configuration" sidebar shows wrong tooltip for filter button "All". Fixed. --- .../@node-red/editor-client/src/js/ui/tab-config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js index 492600f91..4b37d8536 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js @@ -346,7 +346,7 @@ RED.sidebar.config = (function() { refreshConfigNodeList(); } }); - RED.popover.tooltip($('#red-ui-sidebar-config-filter-all'), RED._("sidebar.config.showAllUnusedConfigNodes")); + RED.popover.tooltip($('#red-ui-sidebar-config-filter-all'), RED._("sidebar.config.showAllConfigNodes")); RED.popover.tooltip($('#red-ui-sidebar-config-filter-unused'), RED._("sidebar.config.showAllUnusedConfigNodes")); } From a6696733fa22b3553b78e0627cd3b3b416d4d528 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 21 Mar 2022 13:50:24 +0000 Subject: [PATCH 15/20] tidy up code --- test/nodes/core/network/21-mqtt_spec.js | 541 +++++++++++------------- 1 file changed, 236 insertions(+), 305 deletions(-) diff --git a/test/nodes/core/network/21-mqtt_spec.js b/test/nodes/core/network/21-mqtt_spec.js index d6fd74027..f86924c91 100644 --- a/test/nodes/core/network/21-mqtt_spec.js +++ b/test/nodes/core/network/21-mqtt_spec.js @@ -16,11 +16,12 @@ /* These tests are only supposed to be executed at development time (for now)*/ +"use strict"; const should = require("should"); const helper = require("node-red-node-test-helper"); const mqttNodes = require("nr-test-utils").require("@node-red/nodes/core/network/10-mqtt.js"); -const server = process.env.MQTT_BROKER_SERVER || "localhost"; -const port = process.env.MQTT_BROKER_PORT || 1883; +const BROKER_HOST = process.env.MQTT_BROKER_SERVER || "localhost"; //"broker.emqx.io";// "mqtt.eclipse.org"; //"test.mosquitto.org"; //"localhost"; +const BROKER_PORT = process.env.MQTT_BROKER_PORT || 1883; const skipTests = process.env.MQTT_SKIP_TESTS == "true"; describe('MQTT Nodes', function () { @@ -36,40 +37,34 @@ describe('MQTT Nodes', function () { afterEach(function () { try { helper.unload(); - } catch (error) { - } + } catch (error) { } }); it('should be loaded and have default values', function (done) { - this.timeout = 1000; - const brokerName = "mqtt_broker", brokerId = "mqtt.broker", brokerOptions = { autoConnect: false }; - const inName = "mqtt_in", inId = "mqtt.in"; - const outName = "mqtt_out", outId = "mqtt.out"; - const flow = [ - buildMQTTInNode(inId, inName, brokerId, "test/in", {}, [outId]), - buildMQTTOutNode(outId, outName, brokerId, "test/out", {}), - buildMQTTBrokerNode(brokerId, brokerName, server, port, brokerOptions), - ]; + this.timeout = 2000; + const { flow, nodes } = buildBasicMQTTSendRecvFlow({ id: "mqtt.broker", name: "mqtt_broker", autoConnect: false }, { id: "mqtt.in", topic: "in_topic" }, { id: "mqtt.out", topic: "out_topic" }); helper.load(mqttNodes, flow, function () { try { - const mqttIn = helper.getNode(inId); - const mqttOut = helper.getNode(outId); - const mqttBroker = helper.getNode(brokerId); + const mqttIn = helper.getNode("mqtt.in"); + const mqttOut = helper.getNode("mqtt.out"); + const mqttBroker = helper.getNode("mqtt.broker"); should(mqttIn).be.type("object", "mqtt in node should be an object") - mqttIn.should.have.property('broker', brokerId); + mqttIn.should.have.property('broker', nodes.mqtt_broker.id); //should be the id of the broker node mqttIn.should.have.property('datatype', 'utf8'); //default: 'utf8' mqttIn.should.have.property('isDynamic', false); //default: false mqttIn.should.have.property('inputs', 0); //default: 0 mqttIn.should.have.property('qos', 2); //default: 2 - mqttIn.should.have.property('wires', [[outId]]); + mqttIn.should.have.property('topic', "in_topic"); + mqttIn.should.have.property('wires', [["helper.node"]]); should(mqttOut).be.type("object", "mqtt out node should be an object") - mqttOut.should.have.property('broker', brokerId); + mqttOut.should.have.property('broker', nodes.mqtt_broker.id); //should be the id of the broker node + mqttOut.should.have.property('topic', "out_topic"); should(mqttBroker).be.type("object", "mqtt broker node should be an object") - mqttBroker.should.have.property('broker', server); - mqttBroker.should.have.property('port', port); + mqttBroker.should.have.property('broker', BROKER_HOST); + mqttBroker.should.have.property('port', BROKER_PORT); mqttBroker.should.have.property('brokerurl'); // mqttBroker.should.have.property('autoUnsubscribe', true);//default: true mqttBroker.should.have.property('autoConnect', false);//Set "autoConnect:false" in brokerOptions @@ -98,88 +93,96 @@ describe('MQTT Nodes', function () { } //#region ################### BASIC TESTS ################### #// - itConditional('should send and receive string (auto)', function (done) { + + itConditional('basic send and receive tests', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; - const msg = { + this.timeout = 2000; + const options = {} + options.sendMsg = { topic: nextTopic(), payload: "hello", qos: 0 } - const expectMsg = Object.assign({}, msg); - testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, msg, expectMsg, done); + options.expectMsg = Object.assign({}, options.sendMsg); + testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, options, { done: done }); }); itConditional('should send JSON and receive string (auto)', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; - const msg = { + this.timeout = 2000; + const options = {} + options.sendMsg = { topic: nextTopic(), payload: '{"prop":"value1", "num":1}', qos: 1 } - const expectMsg = Object.assign({}, msg); - testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, msg, expectMsg, done); - }); + options.expectMsg = Object.assign({}, options.sendMsg); + testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, options, { done: done }); + }) itConditional('should send JSON and receive string (utf8)', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; - const msg = { + this.timeout = 2000; + const options = {} + options.sendMsg = { topic: nextTopic(), payload: '{"prop":"value2", "num":2}', qos: 2 } - const expectMsg = Object.assign({}, msg); - testSendRecv({}, { datatype: "utf8", topicType: "static" }, {}, msg, expectMsg, done); + options.expectMsg = Object.assign({}, options.sendMsg); + testSendRecv({}, { datatype: "utf8", topicType: "static" }, {}, options, { done: done }); }); itConditional('should send JSON and receive Object (json)', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; - const msg = { + this.timeout = 2000; + const options = {} + options.sendMsg = { topic: nextTopic(), payload: '{"prop":"value3", "num":3}'// send a string ... } - const expectMsg = Object.assign({}, msg, {payload: {"prop":"value3", "num":3}});//expect an object - testSendRecv({}, { datatype: "json", topicType: "static" }, {}, msg, expectMsg, done); + options.expectMsg = Object.assign({}, options.sendMsg, { payload: { "prop": "value3", "num": 3 } });//expect an object + testSendRecv({}, { datatype: "json", topicType: "static" }, {}, options, { done: done }); }); itConditional('should send String and receive Buffer (buffer)', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; - const msg = { + this.timeout = 2000; + const options = {} + options.sendMsg = { topic: nextTopic(), payload: "a b c" //send string ... } - const expectMsg = Object.assign({}, msg, {payload: Buffer.from(msg.payload)});//expect Buffer.from(msg.payload) - testSendRecv({}, { datatype: "buffer", topicType: "static"}, {}, msg, expectMsg, done); + options.expectMsg = Object.assign({}, options.sendMsg, { payload: Buffer.from(options.sendMsg.payload) });//expect Buffer.from(msg.payload) + testSendRecv({}, { datatype: "buffer", topicType: "static" }, {}, options, { done: done }); }); itConditional('should send utf8 Buffer and receive String (auto)', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; - const msg = { + this.timeout = 2000; + const options = {} + options.sendMsg = { topic: nextTopic(), payload: Buffer.from([0x78, 0x20, 0x79, 0x20, 0x7a]) // "x y z" } - const expectMsg = Object.assign({}, msg, {payload: "x y z"});//set expected payload to "x y z" - testSendRecv({}, { datatype: "auto", topicType: "static"}, {}, msg, expectMsg, done); + options.expectMsg = Object.assign({}, options.sendMsg, { payload: "x y z" });//set expected payload to "x y z" + testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, options, { done: done }); }); itConditional('should send non utf8 Buffer and receive Buffer (auto)', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; - const msg = { + this.timeout = 2000; + const options = {} + const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null } + options.sendMsg = { topic: nextTopic(), payload: Buffer.from([0xC0, 0xC1, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF]) //non valid UTF8 } - const expectMsg = Object.assign({}, msg); - testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, msg, expectMsg, done); + options.expectMsg = Object.assign({}, options.sendMsg); + testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, options, hooks); }); itConditional('should send/receive all v5 flags and settings', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; + this.timeout = 2000; const t = nextTopic(); - const msg = { - topic: t + "/command", - payload: Buffer.from("v5"), - qos: 1, - retain: true, + const options = {} + const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null } + options.sendMsg = { + topic: t + "/command", payload: Buffer.from("v5"), qos: 1, retain: true, responseTopic: t + "/response", userProperties: { prop1: "val1" }, contentType: "application/json", @@ -187,95 +190,79 @@ describe('MQTT Nodes', function () { payloadFormatIndicator: true, messageExpiryInterval: 2000, } - const expectMsg = Object.assign({}, msg); - expectMsg.payload = expectMsg.payload.toString(); //auto mode + payloadFormatIndicator should make a string - delete expectMsg.payloadFormatIndicator; //Seems mqtt.js only publishes payloadFormatIndicator the will msg + options.expectMsg = Object.assign({}, options.sendMsg); + options.expectMsg.payload = options.expectMsg.payload.toString(); //auto mode + payloadFormatIndicator should make a string + delete options.expectMsg.payloadFormatIndicator; //Seems mqtt.js only publishes payloadFormatIndicator the will msg const inOptions = { datatype: "auto", topicType: "static", - qos: 1, nl: false, rap: true, rh: 1, - subscriptionIdentifier: 333 + qos: 1, nl: false, rap: true, rh: 1 } - testSendRecv({ protocolVersion: 5 }, inOptions, {}, msg, expectMsg, done); + testSendRecv({ protocolVersion: 5 }, inOptions, {}, options, hooks); }); itConditional('should subscribe dynamically via action', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; - const msg = { - topic: nextTopic(), - payload: "abc" + this.timeout = 2000; + const options = {} + const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null } + options.sendMsg = { + topic: nextTopic(), payload: "abc" } - const expectMsg = Object.assign({}, msg); - testSendRecv({ protocolVersion: 5 }, { datatype: "utf8", topicType: "dynamic" }, {}, msg, expectMsg, done); + options.expectMsg = Object.assign({}, options.sendMsg); + testSendRecv({ protocolVersion: 5 }, { datatype: "utf8", topicType: "dynamic" }, {}, options, hooks); }); //#endregion BASIC TESTS //#region ################### ADVANCED TESTS ################### #// itConditional('should connect via "connect" action', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; - const {flow, nodes} = buildBasicMQTTSendRecvFlow("mqtt.broker", "mqtt_broker", { autoConnect: false }, "mqtt.in", "mqtt_in", {}, null, null, null, {}); - flow.push({ "id": "status_node", "type": "status", "name": "status-node", "scope": ["mqtt.in"], "wires": [["helper.node"]] }); - - helper.load(mqttNodes, flow, function () { - const helperNode = helper.getNode("helper.node"); - const mqttIn = helper.getNode("mqtt.in"); - const mqttBroker = helper.getNode("mqtt.broker"); - try { - mqttBroker.should.have.property("autoConnect", false); - mqttBroker.should.have.property("connecting", false);//should not attempt to connect (autoConnect:false) - } catch (error) { - done(error) - } - mqttIn.receive({ "action": "connect" }); - waitConnection(); - function waitConnection() { - if (!mqttBroker.connected) { - setTimeout(waitConnection, 15); - return; - } - done();//if we got here, it connected! - } - }); + this.timeout = 2000; + const options = {} + const hooks = { done: null, beforeLoad: null, afterLoad: null, afterConnect: null } + hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => { + mqttBroker.should.have.property("autoConnect", false); + mqttBroker.should.have.property("connecting", false);//should not attempt to connect (autoConnect:false) + mqttIn.receive({ "action": "connect" }); //now request connect action + return true; //handled + } + hooks.afterConnect = (helperNode, mqttBroker, mqttIn, mqttOut) => { + done();//if we got here, it connected :) + return true; + } + testSendRecv({ protocolVersion: 5, autoConnect: false }, { datatype: "utf8", topicType: "dynamic" }, {}, options, hooks); }); itConditional('should disconnect via "disconnect" action', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; - const {flow, nodes} = buildBasicMQTTSendRecvFlow("mqtt.broker", "mqtt_broker", {}, null, null, null, "mqtt.out", "mqtt_out", {}, {}); - flow.push({ "id": "statusnode", "type": "status", "name": "status-node", "scope": ["mqtt.out"], "wires": [["helper.node"]] });//add status node to watch mqtt_out - - helper.load(mqttNodes, flow, function () { - const helperNode = helper.getNode("helper.node"); - const mqttOut = helper.getNode("mqtt.out"); - const mqttBroker = helper.getNode("mqtt.broker"); - try { - mqttBroker.should.have.property("autoConnect", true); - mqttBroker.should.have.property("connecting", true);//should be trying to connect (autoConnect:true) - } catch (error) { - done(error) - } - waitConnection(); - function waitConnection() { - if (!mqttBroker.connected) { - setTimeout(waitConnection, 15); - return; + this.timeout = 2000; + const options = {} + const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null } + hooks.beforeLoad = (flow) => { //add a status node pointed at MQTT Out node (to watch for connection status change) + flow.push({ "id": "status.node", "type": "status", "name": "status_node", "scope": ["mqtt.out"], "wires": [["helper.node"]] });//add status node to watch mqtt_out + } + hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => { + mqttBroker.should.have.property("autoConnect", true); + mqttBroker.should.have.property("connecting", true);//should be trying to connect (autoConnect:true) + return true; //handled + } + hooks.afterConnect = (helperNode, mqttBroker, mqttIn, mqttOut) => { + //connected - now add the "on" handler then send "disconnect" action + helperNode.on("input", function (msg) { + try { + msg.should.have.property("status"); + msg.status.should.have.property("text"); + msg.status.text.should.containEql('disconnect'); + done(); //it disconnected - yey! + } catch (error) { + done(error) } - //connected - add the on handler and call to disconnect - helperNode.on("input", function (msg) { - try { - msg.status.should.have.property("text"); - msg.status.text.should.containEql('disconnect'); - done(); - } catch (error) { - done(error) - } - }) - mqttOut.receive({ "action": "disconnect" }); - } - }); + }) + mqttOut.receive({ "action": "disconnect" }); + return true; //handed + } + testSendRecv({ protocolVersion: 5 }, null, {}, options, hooks); }); itConditional('should publish birth message', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; + this.timeout = 2000; const baseTopic = nextTopic(); const brokerOptions = { protocolVersion: 4, @@ -283,107 +270,80 @@ describe('MQTT Nodes', function () { birthPayload: "broker connected", birthQos: 2, } - const msg = { topic: baseTopic + "/ignoreme"} - const expectMsg ={ + const options = {}; + const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null }; + options.expectMsg = { topic: brokerOptions.birthTopic, payload: brokerOptions.birthPayload, qos: brokerOptions.birthQos }; - testSendRecv(brokerOptions, { topic: brokerOptions.birthTopic }, {}, msg, expectMsg, done); + testSendRecv(brokerOptions, { topic: brokerOptions.birthTopic }, {}, options, hooks); }); itConditional('should publish close message', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; + this.timeout = 2000; const baseTopic = nextTopic(); - const broker1Options = { id: "mqtt.broker1", name: "mqtt_broker1" } - const broker2Options = { id: "mqtt.broker2", name: "mqtt_broker2", closeTopic: baseTopic + "/close", closePayload: "broker disconnected", closeQos: 2,} - - const {flow} = buildBasicMQTTSendRecvFlow( - broker1Options.id, broker1Options.name || "mqtt_broker", broker1Options, - "mqtt.in", "mqtt_in", {broker: broker1Options.id, topic: broker2Options.closeTopic}, //should receive close msg of broker2 - "mqtt.out", "mqtt_out", {broker: broker2Options.id}, - ) - flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, server, port, broker2Options)) - + const broker1Options = { id: "mqtt.broker1" }//Broker 1 - stays connected to receive the close message + const broker2Options = { id: "mqtt.broker2", closeTopic: baseTopic + "/close", closePayload: '{"msg":"close"}', closeQos: 1, }//Broker 2 - connects to same broker but has a LWT message. + const { flow } = buildBasicMQTTSendRecvFlow(broker1Options, { broker: broker1Options.id, topic: broker2Options.closeTopic, datatype: "json" }, { broker: broker2Options.id }) + flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, BROKER_HOST, BROKER_PORT, broker2Options)); //add second broker helper.load(mqttNodes, flow, function () { const helperNode = helper.getNode("helper.node"); const mqttOut = helper.getNode("mqtt.out"); - const mqttIn = helper.getNode("mqtt.in"); const mqttBroker1 = helper.getNode("mqtt.broker1"); const mqttBroker2 = helper.getNode("mqtt.broker2"); - waitConnection(); - function waitConnection() { - if (!mqttBroker1.connected || !mqttBroker2.connected) { - setTimeout(waitConnection, 15); - return; - } + waitBrokerConnect([mqttBroker1, mqttBroker2], function connected() { //connected - add the on handler and call to disconnect helperNode.on("input", function (msg) { try { msg.should.have.property("topic", broker2Options.closeTopic); - msg.should.have.property('payload', broker2Options.closePayload); + msg.should.have.property('payload', JSON.parse(broker2Options.closePayload)); msg.should.have.property('qos', broker2Options.closeQos); done(); } catch (error) { done(error) } }) - mqttOut.receive({ "action": "disconnect" }); - } + mqttOut.receive({ "action": "disconnect" });//close broker2 + }) }); }); itConditional('should publish will message', function (done) { if (skipTests) { return this.skip() } - this.timeout = 1000; + this.timeout = 2000; const baseTopic = nextTopic(); - //Broker 1 - stays connected to receive the will message when broker 2 is killed - const broker1Options = { id: "mqtt.broker1", name: "mqtt_broker1" } - //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 - const broker2Options = { id: "mqtt.broker2", name: "mqtt_broker2", willTopic: baseTopic + "/will", willPayload: '{"msg":"will"}', willQos: 1,} - const expectMsg ={ - topic: broker2Options.willTopic, - payload: JSON.parse(broker2Options.willPayload), - qos: broker2Options.willQos - }; - - const {flow} = buildBasicMQTTSendRecvFlow( - broker1Options.id, broker1Options.name || "mqtt_broker", broker1Options, - "mqtt.in", "mqtt_in", {broker: broker1Options.id, topic: broker2Options.willTopic, datatype: "json"}, //should receive will msg of broker2 - "mqtt.out", "mqtt_out", {broker: broker2Options.id}, - ) - //add second broker - flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, server, port, broker2Options)) + const broker1Options = { id: "mqtt.broker1" }//Broker 1 - stays connected to receive the will message + const broker2Options = { id: "mqtt.broker2", willTopic: baseTopic + "/will", willPayload: '{"msg":"will"}', willQos: 2, }//Broker 2 - connects to same broker but has a LWT message. + const { flow } = buildBasicMQTTSendRecvFlow(broker1Options, { broker: broker1Options.id, topic: broker2Options.willTopic, datatype: "utf8" }, { broker: broker2Options.id }) + flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, BROKER_HOST, BROKER_PORT, broker2Options)); //add second broker helper.load(mqttNodes, flow, function () { const helperNode = helper.getNode("helper.node"); const mqttBroker1 = helper.getNode("mqtt.broker1"); const mqttBroker2 = helper.getNode("mqtt.broker2"); - waitConnection(); - function waitConnection() { - if (!mqttBroker1.connected || !mqttBroker2.connected) { - setTimeout(waitConnection, 15); - return; - } + waitBrokerConnect([mqttBroker1, mqttBroker2], function connected() { //connected - add the on handler and call to disconnect helperNode.on("input", function (msg) { try { - compareMsgToExpected(msg, expectMsg); + msg.should.have.property("topic", broker2Options.willTopic); + msg.should.have.property('payload', broker2Options.willPayload); + msg.should.have.property('qos', broker2Options.willQos); done(); } catch (error) { done(error) } }); mqttBroker2.client.end(true); //force closure - } + }) }); }); itConditional('should publish will message with V5 properties', function (done) { if (skipTests) { return this.skip() } // return this.skip(); //Issue receiving v5 props on will msg. Issue raised here: https://github.com/mqttjs/MQTT.js/issues/1455 - this.timeout = 1000; + this.timeout = 2000; const baseTopic = nextTopic(); //Broker 1 - stays connected to receive the will message when broker 2 is killed - const broker1Options = { id: "mqtt.broker1", name: "mqtt_broker1", protocolVersion: 5, datatype: "utf8"} + const broker1Options = { id: "mqtt.broker1", name: "mqtt_broker1", protocolVersion: 5, datatype: "utf8" } //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 const broker2Options = { id: "mqtt.broker2", name: "mqtt_broker2", protocolVersion: 5, @@ -391,15 +351,15 @@ describe('MQTT Nodes', function () { willPayload: '{"msg":"will"}', willQos: 2, willMsg: { - contentType: 'application/json' , - userProps: {"will":"value"}, - respTopic: baseTopic+"/resp", + contentType: 'application/json', + userProps: { "will": "value" }, + respTopic: baseTopic + "/resp", correl: Buffer.from("abc"), expiry: 2000, payloadFormatIndicator: true } } - const expectMsg ={ + const expectMsg = { topic: broker2Options.willTopic, payload: broker2Options.willPayload, qos: broker2Options.willQos, @@ -410,24 +370,13 @@ describe('MQTT Nodes', function () { messageExpiryInterval: broker2Options.willMsg.expiry, // payloadFormatIndicator: broker2Options.willMsg.payloadFormatIndicator, }; - const {flow} = buildBasicMQTTSendRecvFlow( - broker1Options.id, broker1Options.name || "mqtt_broker", broker1Options, - "mqtt.in", "mqtt_in", {broker: broker1Options.id, topic: broker2Options.willTopic}, //should receive will msg of broker2 - "mqtt.out", "mqtt_out", {broker: broker2Options.id}, - ) - //add second broker with will msg set - flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, server, port, broker2Options)) - + const { flow, nodes } = buildBasicMQTTSendRecvFlow(broker1Options, { broker: broker1Options.id, topic: broker2Options.willTopic, datatype: "utf8" }, { broker: broker2Options.id }) + flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, nodes.mqtt_broker1.broker, nodes.mqtt_broker1.port, broker2Options)) //add second broker with will msg set helper.load(mqttNodes, flow, function () { const helperNode = helper.getNode("helper.node"); const mqttBroker1 = helper.getNode("mqtt.broker1"); const mqttBroker2 = helper.getNode("mqtt.broker2"); - waitConnection(); - function waitConnection() { - if (!mqttBroker1.connected || !mqttBroker2.connected) { - setTimeout(waitConnection, 15); - return; - } + waitBrokerConnect([mqttBroker1, mqttBroker2], function connected() { //connected - add the on handler and call to disconnect helperNode.on("input", function (msg) { try { @@ -437,8 +386,8 @@ describe('MQTT Nodes', function () { done(error) } }); - mqttBroker2.client.end(true); //force closure of broker 2 to cause will msg - } + mqttBroker2.client.end(true); //force closure + }) }); }); //#endregion ADVANCED TESTS @@ -447,144 +396,106 @@ describe('MQTT Nodes', function () { //#region ################### HELPERS ################### #// /** - * A basic unit test that builds a flow containg 1 broker, 1 mqtt-in, one mqtt-out and a helper. + * A basic unit test that builds a flow containing 1 broker, 1 mqtt-in, one mqtt-out and a helper. * It performs the following steps: builds flow, loads flow, waits for connection, sends `sendMsg`, * waits for msg then compares `sendMsg` to `expectMsg`, and finally calls `done` * @param {object} brokerOptions anything that can be set in an MQTTBrokerNode (e.g. id, name, url, broker, server, port, protocolVersion, ...) * @param {object} inNodeOptions anything that can be set in an MQTTInNode (e.g. id, name, broker, topic, rh, nl, rap, ... ) * @param {object} outNodeOptions anything that can be set in an MQTTOutNode (e.g. id, name, broker, ...) - * @param {object} sendMsg the msg to send to broker - * @param {object} expectMsg the msg to send to broker - * @param {function} done the test runner `done` callback + * @param {object} options an object for passing in test properties like `sendMsg` and `expectMsg` + * @param {object} hooks an object containing hook functions... + * * [fn] `done()` - the tests done function. If excluded, an error will be thrown upon test error + * * [fn] `beforeLoad(flow)` - provides opportunity to adjust the flow JSON before loading into runtime + * * [fn] `afterLoad(helperNode, mqttBroker, mqttIn, mqttOut)` - called before connection attempt + * * [fn] `afterConnect(helperNode, mqttBroker, mqttIn, mqttOut)` - called before connection attempt */ -function testSendRecv(brokerOptions, inNodeOptions, outNodeOptions, sendMsg, expectMsg, done, customTests) { - sendMsg = sendMsg || {}; +function testSendRecv(brokerOptions, inNodeOptions, outNodeOptions, options, hooks) { + options = options || {}; brokerOptions = brokerOptions || {}; inNodeOptions = inNodeOptions || {}; outNodeOptions = outNodeOptions || {}; - brokerOptions.id = brokerOptions.id || "mqtt.broker"; - inNodeOptions.id = inNodeOptions.id || "mqtt.in"; - outNodeOptions.id = outNodeOptions.id || "mqtt.out"; - inNodeOptions.brokerId = inNodeOptions.brokerId || brokerOptions.id; - outNodeOptions.id = outNodeOptions.id || brokerOptions.id; + const sendMsg = options.sendMsg || {}; sendMsg.topic = sendMsg.topic || nextTopic(); - - if(inNodeOptions.topicType != "dynamic" ) { + const expectMsg = options.expectMsg || Object.assign({}, sendMsg); + expectMsg.payload = inNodeOptions.payload === undefined ? expectMsg.payload : inNodeOptions.payload; + if (inNodeOptions.topicType != "dynamic") { inNodeOptions.topic = inNodeOptions.topic || sendMsg.topic; } - outNodeOptions.topic = outNodeOptions.topic ? outNodeOptions.topic : sendMsg.topic; - - const {flow} = buildBasicMQTTSendRecvFlow( - brokerOptions.id, brokerOptions.name || "mqtt_broker", brokerOptions, - inNodeOptions.id, inNodeOptions.name, inNodeOptions, - outNodeOptions.id, outNodeOptions.name, outNodeOptions, - ) - - expectMsg = expectMsg || Object.assign({}, sendMsg); - expectMsg.payload = inNodeOptions.payload === undefined ? expectMsg.payload : inNodeOptions.payload; + const { flow, nodes } = buildBasicMQTTSendRecvFlow(brokerOptions, inNodeOptions, outNodeOptions); + if (hooks.beforeLoad) { hooks.beforeLoad(flow) } helper.load(mqttNodes, flow, function () { try { const helperNode = helper.getNode("helper.node"); const mqttBroker = helper.getNode(brokerOptions.id); - const mqttIn = helper.getNode(inNodeOptions.id); - const mqttOut = helper.getNode(outNodeOptions.id); - - helperNode.on("input", function (msg) { - try { - if (customTests) { - customTests(msg, helperNode, mqttBroker, mqttIn, mqttOut) - } else { + const mqttIn = helper.getNode(nodes.mqtt_in.id); + const mqttOut = helper.getNode(nodes.mqtt_out.id); + let afterLoadHandled = false; + if (hooks.afterLoad) { + afterLoadHandled = hooks.afterLoad(helperNode, mqttBroker, mqttIn, mqttOut) + } + if (!afterLoadHandled) { + helperNode.on("input", function (msg) { + try { compareMsgToExpected(msg, expectMsg); + if (hooks.done) { hooks.done(); } + } catch (err) { + if (hooks.done) { hooks.done(err); } + else { throw err; } } - done(); - } catch (err) { - done(err); - } - }); - waitConnection(); - function waitConnection() { - if (!mqttBroker.connected) { - setTimeout(waitConnection, 15); - return; - } + }); + } + waitBrokerConnect(mqttBroker, function () { //finally, connected! + if (hooks.afterConnect) { + let handled = hooks.afterConnect(helperNode, mqttBroker, mqttIn, mqttOut); + if (handled) { return } + } if (mqttIn.isDynamic) { mqttIn.receive({ "action": "subscribe", "topic": sendMsg.topic }) } mqttOut.receive(sendMsg); - } - } catch (error) { - done(error) + }) + } catch (err) { + if (hooks.done) { hooks.done(err); } + else { throw err; } } }); } -/** - * Builds a flow from an array of {type:string, id:string, name:string, options:object, wires:[string]} - * @param {[{type:string, id:string, name:string, options:object, wires:[string]}]} nodes - * @returns {{[flow: [], nodes: {}]}} Returns `{[flow: [], nodes: {}]}` - */ -function buildFlow(nodes) { - const result = {flow: [], nodes: {}}; - nodes.forEach(node => { - //const flow = [ { "id": "helper.node", "type": "helper", "wires": [] } ]; - node.options = node.options || {}; - switch (node.type) { - case "mqtt-broker": - result.nodes[node.id] = buildMQTTBrokerNode(node.id, node.name, node.options.server, node.options.port, node.options); - break; - case "mqtt in": - result.nodes[node.id] = buildMQTTInNode(node.id, node.name, node.options.brokerId, node.options.topic, node.options); - break; - case "mqtt out": - result.nodes[node.id] = buildMQTTOutNode(node.id, node.name, node.options.brokerId, node.options.topic, node.options); - break; - default: - result.nodes[node.id] = buildNode(node.type, node.id, node.name, node.options); - break; - } - if (node.wires && Array.isArray(node.wires)) { - result.nodes[node.id].wires[0] = [...node.wires]; - } - result.flow.push(result.nodes[node.id]); - }) - return result; -} - /** - * Builds a flow containing 2 parts. Part1: MQTT Out node. Part2: MQTT In node --> helper node - * If inXxx is excluded, there will be no in node - * If outXxx is excluded, there will be no out node + * Builds a flow containing 2 parts. + * * 1: MQTT Out node (with broker configured). + * * 2: MQTT In node (with broker configured) --> helper node `id:helper.node` */ -function buildBasicMQTTSendRecvFlow(brokerId, brokerName, brokerOptions, inId, inName, inOptions, outId, outName, outOptions) { - var nodes = []; +function buildBasicMQTTSendRecvFlow(brokerOptions, inOptions, outOptions) { brokerOptions = brokerOptions || {}; - outOptions = outOptions || {}; - inOptions = inOptions || {}; - - brokerOptions.server = brokerOptions.server || server; - brokerOptions.port = brokerOptions.port || port; + brokerOptions.broker = brokerOptions.broker || BROKER_HOST; + brokerOptions.port = brokerOptions.port || BROKER_PORT; brokerOptions.autoConnect = String(brokerOptions.autoConnect) == "false" ? false : true; - - outOptions.broker = outOptions.broker || brokerId; - inOptions.broker = inOptions.broker || brokerId; - - nodes.push({type:"mqtt-broker", id: brokerId, name: brokerName, options: brokerOptions}); - if(inId) { nodes.push({type:"mqtt in", id: inId, name: inName, options: inOptions, wires: ["helper.node"]}); } - if(outId) { nodes.push({type:"mqtt out", id: outId, name: outName, options: outOptions}); } - nodes.push({type:"helper", id: "helper.node", name: "helper_node", options: {}}); - return buildFlow(nodes); + const broker = buildMQTTBrokerNode(brokerOptions.id, brokerOptions.name, brokerOptions.broker, brokerOptions.port, brokerOptions); + const inNode = buildMQTTInNode(inOptions.id, inOptions.name, inOptions.broker || broker.id, inOptions.topic, inOptions, ["helper.node"]); + const outNode = buildMQTTOutNode(outOptions.id, outOptions.name, outOptions.broker || broker.id, outOptions.topic, outOptions); + const helper = buildNode("helper", "helper.node", "helper_node", {}); + return { + nodes: { + [broker.name]: broker, + [inNode.name]: inNode, + [outNode.name]: outNode, + [helper.name]: helper, + }, + flow: [broker, inNode, outNode, helper] + } } -function buildMQTTBrokerNode(id, name, server, port, options) { +function buildMQTTBrokerNode(id, name, brokerHost, brokerPort, options) { // url,broker,port,clientid,autoConnect,usetls,usews,verifyservercert,compatmode,protocolVersion,keepalive, //cleansession,sessionExpiry,topicAliasMaximum,maximumPacketSize,receiveMaximum,userProperties,userPropertiesType,autoUnsubscribe options = options || {}; - const node = buildNode("mqtt-broker", id, name, options); - node.broker = server; - node.port = port; + const node = buildNode("mqtt-broker", id || "mqtt.broker", name || "mqtt_broker", options); node.url = options.url; + node.broker = brokerHost || options.broker || BROKER_HOST; + node.port = brokerPort || options.port || BROKER_PORT; node.clientid = options.clientid || ""; node.cleansession = String(options.cleansession) == "false" ? false : true; node.autoUnsubscribe = String(options.autoUnsubscribe) == "false" ? false : true; @@ -612,19 +523,21 @@ function buildMQTTBrokerNode(id, name, server, port, options) { function buildMQTTInNode(id, name, brokerId, topic, options, wires) { //{ "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"]] } options = options || {}; - const node = buildNode("mqtt in", id, name, options); + options.broker = options.broker || "mqtt.broker"; + const node = buildNode("mqtt in", id || "mqtt.in", name || "mqtt_in", options); node.topic = topic || ""; node.broker = brokerId; node.topicType = options.topicType == "dynamic" ? "dynamic" : "static", - node.inputs = options.topicType == "dynamic" ? 1 : 0, - updateNodeOptions(node, options, wires); + node.inputs = options.topicType == "dynamic" ? 1 : 0, + updateNodeOptions(node, options, wires); return node; } function buildMQTTOutNode(id, name, brokerId, topic, options) { //{ "id": "mqtt.out", "type": "mqtt out", "name": "mqtt_out", "topic": "test/out", "qos": "", "retain": "", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": brokerId, "wires": [] }, options = options || {}; - const node = buildNode("mqtt out", id, name, options); + options.broker = options.broker || "mqtt.broker"; + const node = buildNode("mqtt out", id || "mqtt.out", name || "mqtt_out", options); node.topic = topic || ""; node.broker = brokerId; updateNodeOptions(node, options, null); @@ -634,13 +547,13 @@ function buildMQTTOutNode(id, name, brokerId, topic, options) { function buildNode(type, id, name, options, wires) { //{ "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"]] } options = options || {}; - const ts = String(Date.now()); const node = { - "id": id || (type + "." + ts), + "id": id || (type.replace(/[\W]/g, ".")), "type": type, - "name": name || (type.replace(/[\W]/g,"_") + "_" + ts), + "name": name || (type.replace(/[\W]/g, "_")), "wires": [] } + if (node.id.indexOf(".") == -1) { node.is += ".node" } updateNodeOptions(node, options, wires); return node; } @@ -676,6 +589,24 @@ function compareMsgToExpected(msg, expectMsg) { if (hasProperty(expectMsg, "messageExpiryInterval")) { msg.should.have.property("messageExpiryInterval", expectMsg.messageExpiryInterval); } } +function waitBrokerConnect(broker, callback, timeLimit) { + timeLimit = timeLimit || 2000; + const brokers = Array.isArray(broker) ? broker : [broker]; + wait(); + function wait() { + if (brokers.every(e => e.connected == true)) { + callback(); //yey - connected! + } else { + timeLimit = timeLimit - 15; + if (timeLimit <= 0) { + throw new Error("Timeout waiting broker connect") + } + setTimeout(wait, 15); + return; + } + } +} + function hasProperty(obj, propName) { return Object.prototype.hasOwnProperty.call(obj, propName); } From 40a9dce8698d036d66c2698f2ce2228b9f979681 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 21 Mar 2022 13:51:20 +0000 Subject: [PATCH 16/20] try broker.emqx.io --- test/nodes/core/network/21-mqtt_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/nodes/core/network/21-mqtt_spec.js b/test/nodes/core/network/21-mqtt_spec.js index f86924c91..cefc4b176 100644 --- a/test/nodes/core/network/21-mqtt_spec.js +++ b/test/nodes/core/network/21-mqtt_spec.js @@ -20,7 +20,7 @@ const should = require("should"); const helper = require("node-red-node-test-helper"); const mqttNodes = require("nr-test-utils").require("@node-red/nodes/core/network/10-mqtt.js"); -const BROKER_HOST = process.env.MQTT_BROKER_SERVER || "localhost"; //"broker.emqx.io";// "mqtt.eclipse.org"; //"test.mosquitto.org"; //"localhost"; +const BROKER_HOST = process.env.MQTT_BROKER_SERVER || "broker.emqx.io";// "mqtt.eclipse.org"; //"test.mosquitto.org"; //"localhost"; const BROKER_PORT = process.env.MQTT_BROKER_PORT || 1883; const skipTests = process.env.MQTT_SKIP_TESTS == "true"; From ecf1847dd23ea1f531ed03031d17d3b9f742e141 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 21 Mar 2022 14:59:10 +0000 Subject: [PATCH 17/20] use CI flag to skip MQTT tests --- test/nodes/core/network/21-mqtt_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/nodes/core/network/21-mqtt_spec.js b/test/nodes/core/network/21-mqtt_spec.js index cefc4b176..98286c28f 100644 --- a/test/nodes/core/network/21-mqtt_spec.js +++ b/test/nodes/core/network/21-mqtt_spec.js @@ -20,9 +20,9 @@ const should = require("should"); const helper = require("node-red-node-test-helper"); const mqttNodes = require("nr-test-utils").require("@node-red/nodes/core/network/10-mqtt.js"); -const BROKER_HOST = process.env.MQTT_BROKER_SERVER || "broker.emqx.io";// "mqtt.eclipse.org"; //"test.mosquitto.org"; //"localhost"; +const BROKER_HOST = process.env.MQTT_BROKER_SERVER || "localhost"; const BROKER_PORT = process.env.MQTT_BROKER_PORT || 1883; -const skipTests = process.env.MQTT_SKIP_TESTS == "true"; +const skipTests = process.env.CI == "true" || process.env.CI == "1"; //CI Env - skip MQTT tests describe('MQTT Nodes', function () { From 7f9f551cfead6452eb18ffdbfe0151699ed5ae94 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 21 Mar 2022 15:24:23 +0000 Subject: [PATCH 18/20] fix typo of will properties (properies) Fixes #3501 --- packages/node_modules/@node-red/nodes/core/network/10-mqtt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js index bdf2b0e5b..1796f1f2b 100644 --- a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js +++ b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js @@ -465,7 +465,7 @@ module.exports = function(RED) { }; if(hasProperty(opts, "willTopic")) { //will v5 properties must be set in the "properties" sub object - node.options.will = createLWT(opts.willTopic, opts.willPayload, opts.willQos, opts.willRetain, opts.willMsg, "properies"); + node.options.will = createLWT(opts.willTopic, opts.willPayload, opts.willQos, opts.willRetain, opts.willMsg, "properties"); }; } else { //update options From cf2e7744f37a2169fdc0e029113c7bd8cada95f0 Mon Sep 17 00:00:00 2001 From: Stephen McLaughlin <44235289+Steve-Mcl@users.noreply.github.com> Date: Wed, 23 Mar 2022 09:15:57 +0000 Subject: [PATCH 19/20] remove copyright header Co-authored-by: Nick O'Leary --- test/nodes/core/network/21-mqtt_spec.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/test/nodes/core/network/21-mqtt_spec.js b/test/nodes/core/network/21-mqtt_spec.js index 98286c28f..81c6988e3 100644 --- a/test/nodes/core/network/21-mqtt_spec.js +++ b/test/nodes/core/network/21-mqtt_spec.js @@ -1,18 +1,3 @@ -/** - * Copyright JS Foundation and other contributors, mqtt://js.foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - **/ /* These tests are only supposed to be executed at development time (for now)*/ From f660973168d08952a8a3873c73c050f20e364457 Mon Sep 17 00:00:00 2001 From: Stephen McLaughlin <44235289+Steve-Mcl@users.noreply.github.com> Date: Wed, 23 Mar 2022 10:00:36 +0000 Subject: [PATCH 20/20] Dont run MQTT tests by default on local - update skip message to inform use of how to enable test --- test/nodes/core/network/21-mqtt_spec.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/nodes/core/network/21-mqtt_spec.js b/test/nodes/core/network/21-mqtt_spec.js index 81c6988e3..56fa1abb3 100644 --- a/test/nodes/core/network/21-mqtt_spec.js +++ b/test/nodes/core/network/21-mqtt_spec.js @@ -7,7 +7,8 @@ const helper = require("node-red-node-test-helper"); const mqttNodes = require("nr-test-utils").require("@node-red/nodes/core/network/10-mqtt.js"); const BROKER_HOST = process.env.MQTT_BROKER_SERVER || "localhost"; const BROKER_PORT = process.env.MQTT_BROKER_PORT || 1883; -const skipTests = process.env.CI == "true" || process.env.CI == "1"; //CI Env - skip MQTT tests +//By default, MQTT tests are disabled. Set ENV VAR NR_MQTT_TESTS to "1" or "true" to enable +const skipTests = process.env.NR_MQTT_TESTS != "true" && process.env.NR_MQTT_TESTS != "1"; describe('MQTT Nodes', function () { @@ -67,7 +68,7 @@ describe('MQTT Nodes', function () { }); if (skipTests) { - it('should skip following MQTT tests (no broker available)', function (done) { + it('skipping MQTT tests. Set env var "NR_MQTT_TESTS=true" to enable. Requires a v5 capable broker running on localhost:1883.', function (done) { done(); }); }