From 8ba6a7436e7e403fc51a6246588ef001363fd727 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Fri, 15 Apr 2022 18:21:36 +0100 Subject: [PATCH] Add tests for MQTT v5 auto parsing hints --- test/nodes/core/network/21-mqtt_spec.js | 168 ++++++++++++++++++++++-- 1 file changed, 157 insertions(+), 11 deletions(-) diff --git a/test/nodes/core/network/21-mqtt_spec.js b/test/nodes/core/network/21-mqtt_spec.js index 56fa1abb3..28321d115 100644 --- a/test/nodes/core/network/21-mqtt_spec.js +++ b/test/nodes/core/network/21-mqtt_spec.js @@ -4,6 +4,7 @@ "use strict"; const should = require("should"); const helper = require("node-red-node-test-helper"); +const { doesNotThrow } = require("should"); 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; @@ -92,7 +93,21 @@ describe('MQTT Nodes', function () { options.expectMsg = Object.assign({}, options.sendMsg); testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, options, { done: done }); }); - itConditional('should send JSON and receive string (auto)', function (done) { + //Prior to V3, "auto" mode would only parse to string or buffer. + // itConditional('should send JSON and receive string (auto mode)', function (done) { + // if (skipTests) { return this.skip() } + // this.timeout = 2000; + // const options = {} + // options.sendMsg = { + // topic: nextTopic(), + // payload: '{"prop":"value1", "num":1}', + // qos: 1 + // } + // options.expectMsg = Object.assign({}, options.sendMsg); + // testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, options, { done: done }); + // }) + //In V3, "auto" mode should try to parse JSON, then string and fall back to buffer + itConditional('should send JSON and receive object (auto mode)', function (done) { if (skipTests) { return this.skip() } this.timeout = 2000; const options = {} @@ -102,9 +117,22 @@ describe('MQTT Nodes', function () { qos: 1 } options.expectMsg = Object.assign({}, options.sendMsg); + options.expectMsg.payload = JSON.parse(options.sendMsg.payload); testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, options, { done: done }); }) - itConditional('should send JSON and receive string (utf8)', function (done) { + itConditional('should send invalid JSON and receive string (auto mode)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 2000; + const options = {} + options.sendMsg = { + topic: nextTopic(), + payload: '{prop:"value3", "num":3}'// send invalid JSON ... + } + options.expectMsg = Object.assign({}, options.sendMsg);//expect same payload + testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, options, { done: done }); + }); + + itConditional('should send JSON and receive string (utf8 mode)', function (done) { if (skipTests) { return this.skip() } this.timeout = 2000; const options = {} @@ -116,7 +144,7 @@ describe('MQTT Nodes', function () { options.expectMsg = Object.assign({}, options.sendMsg); testSendRecv({}, { datatype: "utf8", topicType: "static" }, {}, options, { done: done }); }); - itConditional('should send JSON and receive Object (json)', function (done) { + itConditional('should send JSON and receive Object (json mode)', function (done) { if (skipTests) { return this.skip() } this.timeout = 2000; const options = {} @@ -127,7 +155,31 @@ describe('MQTT Nodes', function () { 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) { + itConditional('should send invalid JSON and raise error (json mode)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 2000; + const options = {} + options.sendMsg = { + topic: nextTopic(), + payload: '{prop:"value3", "num":3}', // send invalid JSON ... + } + const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null } + hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => { + helperNode.on("input", function (msg) { + try { + msg.should.have.a.property("error").type("object"); + msg.error.should.have.a.property("source").type("object"); + msg.error.source.should.have.a.property("id", mqttIn.id); + done(); + } catch (err) { + done(err) + } + }); + return true; //handled + } + testSendRecv({}, { datatype: "json", topicType: "static" }, {}, options, hooks); + }); + itConditional('should send String and receive Buffer (buffer mode)', function (done) { if (skipTests) { return this.skip() } this.timeout = 2000; const options = {} @@ -138,7 +190,7 @@ describe('MQTT Nodes', function () { 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) { + itConditional('should send utf8 Buffer and receive String (auto mode)', function (done) { if (skipTests) { return this.skip() } this.timeout = 2000; const options = {} @@ -149,7 +201,7 @@ describe('MQTT Nodes', function () { 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) { + itConditional('should send non utf8 Buffer and receive Buffer (auto mode)', function (done) { if (skipTests) { return this.skip() } this.timeout = 2000; const options = {} @@ -158,7 +210,7 @@ describe('MQTT Nodes', function () { topic: nextTopic(), payload: Buffer.from([0xC0, 0xC1, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF]) //non valid UTF8 } - options.expectMsg = Object.assign({}, options.sendMsg); + options.expectMsg = Object.assign({}, options.sendMsg, {payload: Buffer.from([0xC0, 0xC1, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF])}); testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, options, hooks); }); itConditional('should send/receive all v5 flags and settings', function (done) { @@ -168,16 +220,16 @@ describe('MQTT Nodes', function () { 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, + topic: t + "/command", payload: Buffer.from('{"version":"v5"}'), qos: 1, retain: true, responseTopic: t + "/response", userProperties: { prop1: "val1" }, - contentType: "application/json", + contentType: "text/plain", correlationData: Buffer.from([1, 2, 3]), payloadFormatIndicator: true, messageExpiryInterval: 2000, } options.expectMsg = Object.assign({}, options.sendMsg); - options.expectMsg.payload = options.expectMsg.payload.toString(); //auto mode + payloadFormatIndicator should make a string + options.expectMsg.payload = options.expectMsg.payload.toString(); //auto mode + payloadFormatIndicator + contentType: "text/plain" should make a string delete options.expectMsg.payloadFormatIndicator; //Seems mqtt.js only publishes payloadFormatIndicator the will msg const inOptions = { datatype: "auto", topicType: "static", @@ -185,6 +237,98 @@ describe('MQTT Nodes', function () { } testSendRecv({ protocolVersion: 5 }, inOptions, {}, options, hooks); }); + itConditional('should send regular string with v5 media type "text/plain" and receive a string (auto mode)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 2000; + const options = {} + const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null } + options.sendMsg = { + topic: nextTopic(), payload: "abc", contentType: "text/plain" + } + options.expectMsg = Object.assign({}, options.sendMsg); + testSendRecv({ protocolVersion: 5 }, { datatype: "auto", topicType: "static" }, {}, options, hooks); + }); + itConditional('should send JSON with v5 media type "text/plain" and receive a string (auto mode)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 2000; + const options = {} + const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null } + options.sendMsg = { + topic: nextTopic(), payload: '{"prop":"val"}', contentType: "text/plain" + } + options.expectMsg = Object.assign({}, options.sendMsg); + testSendRecv({ protocolVersion: 5 }, { datatype: "auto", topicType: "static" }, {}, options, hooks); + }); + itConditional('should send JSON with v5 media type "application/json" and receive an object (auto mode)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 2000; + const options = {} + const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null } + options.sendMsg = { + topic: nextTopic(), payload: '{"prop":"val"}', contentType: "application/json", + } + options.expectMsg = Object.assign({}, options.sendMsg, { payload: JSON.parse(options.sendMsg.payload)}); + testSendRecv({ protocolVersion: 5 }, { datatype: "auto", topicType: "static" }, {}, options, hooks); + }); + itConditional('should send invalid JSON with v5 media type "application/json" and raise an error (auto mode)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 2000; + const options = {} + options.sendMsg = { + topic: nextTopic(), + payload: '{prop:"value3", "num":3}', contentType: "application/json", // send invalid JSON ... + } + const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null } + hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => { + helperNode.on("input", function (msg) { + try { + msg.should.have.a.property("error").type("object"); + msg.error.should.have.a.property("source").type("object"); + msg.error.source.should.have.a.property("id", mqttIn.id); + done(); + } catch (err) { + done(err) + } + }); + return true; //handled + } + testSendRecv({ protocolVersion: 5 }, { datatype: "auto", topicType: "static" }, {}, options, hooks); + }); + + itConditional('should send buffer with v5 media type "application/json" and receive an object (auto mode)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 2000; + const options = {} + const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null } + options.sendMsg = { + topic: nextTopic(), payload: Buffer.from([0x7b,0x22,0x70,0x72,0x6f,0x70,0x22,0x3a,0x22,0x76,0x61,0x6c,0x22,0x7d]), contentType: "application/json", + } + options.expectMsg = Object.assign({}, options.sendMsg, { payload: {"prop":"val"}}); + testSendRecv({ protocolVersion: 5 }, { datatype: "auto", topicType: "static" }, {}, options, hooks); + }); + itConditional('should send buffer with v5 media type "text/plain" and receive a string (auto mode)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 2000; + const options = {} + const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null } + options.sendMsg = { + topic: nextTopic(), payload: Buffer.from([0x7b,0x22,0x70,0x72,0x6f,0x70,0x22,0x3a,0x22,0x76,0x61,0x6c,0x22,0x7d]), contentType: "text/plain", + } + options.expectMsg = Object.assign({}, options.sendMsg, { payload: '{"prop":"val"}'}); + testSendRecv({ protocolVersion: 5 }, { datatype: "auto", topicType: "static" }, {}, options, hooks); + }); + itConditional('should send buffer with v5 media type "application/zip" and receive a buffer (auto mode)', function (done) { + if (skipTests) { return this.skip() } + this.timeout = 2000; + const options = {} + const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null } + options.sendMsg = { + topic: nextTopic(), payload: Buffer.from([0x7b,0x22,0x70,0x72,0x6f,0x70,0x22,0x3a,0x22,0x76,0x61,0x6c,0x22,0x7d]), contentType: "application/zip", + } + options.expectMsg = Object.assign({}, options.sendMsg, { payload: Buffer.from([0x7b,0x22,0x70,0x72,0x6f,0x70,0x22,0x3a,0x22,0x76,0x61,0x6c,0x22,0x7d])}); + testSendRecv({ protocolVersion: 5 }, { datatype: "auto", topicType: "static" }, {}, options, hooks); + }); + itConditional('should subscribe dynamically via action', function (done) { if (skipTests) { return this.skip() } this.timeout = 2000; @@ -463,14 +607,16 @@ function buildBasicMQTTSendRecvFlow(brokerOptions, inOptions, outOptions) { 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", {}); + const catchNode = buildNode("catch", "catch.node", "catch_node", {"scope": ["mqtt.in"]}, ["helper.node"]); return { nodes: { [broker.name]: broker, [inNode.name]: inNode, [outNode.name]: outNode, [helper.name]: helper, + [catchNode.name]: catchNode, }, - flow: [broker, inNode, outNode, helper] + flow: [broker, inNode, outNode, helper, catchNode] } }