UNPKG

node-red-contrib-chronos

Version:

Time-based Node-RED scheduling, repeating, queueing, routing, filtering and manipulating nodes

1,137 lines (983 loc) 85.4 kB
/* * Copyright (c) 2020 - 2025 Jens-Uwe Rossbach * * This code is licensed under the MIT License. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const sinon = require("sinon"); const helper = require("node-red-node-test-helper"); const configNode = require("../nodes/config.js"); const delayNode = require("../nodes/delay.js"); const chronos = require("../nodes/common/chronos.js"); const moment = require("moment"); require("should-sinon"); const cfgNode = {id: "cn1", type: "chronos-config", name: "config"}; const cfgNodeInvalidTZ = {id: "cn1", type: "chronos-config", name: "config", timezone: "invalid"}; const hlpNode = {id: "hn1", type: "helper"}; const credentials = {"cn1": {latitude: "50", longitude: "10"}}; describe("delay node", function() { before(function(done) { helper.startServer(done); }); after(function(done) { helper.stopServer(done); }); context("node initialization", function() { afterEach(function() { helper.unload(); sinon.restore(); }); it("should correctly load", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "randomDuration", fixedDuration: 13, randomDuration1: 42, randomDuration2: 76, fixedDurationUnit: "days", randomDurationUnit: "minutes", randomizerMillis: true, whenType: "time", whenValue: "12:00", offset: 0, random: false, customDelayType: "flow", customDelayValue: "myVar", preserveCtrlProps: true, ignoreCtrlProps: true}, cfgNode]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.status.should.be.calledOnce(); dn1.should.have.property("name", "delay"); dn1.should.have.property("delayType", "randomDuration"); dn1.should.have.property("fixedDuration", 13); dn1.should.have.property("randomDuration1", 42); dn1.should.have.property("randomDuration2", 76); dn1.should.have.property("fixedDurationUnit", "days"); dn1.should.have.property("randomDurationUnit", "minutes"); dn1.should.have.property("randomizerMillis", true); dn1.should.have.property("whenType", "time"); dn1.should.have.property("whenValue", "12:00"); dn1.should.have.property("offset", 0); dn1.should.have.property("random", false); dn1.should.have.property("customDelayType", "flow"); dn1.should.have.property("customDelayValue", "myVar"); dn1.should.have.property("preserveCtrlProps", true); dn1.should.have.property("ignoreCtrlProps", true); }); it("should preserve backward compatibility", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", whenType: "time", whenValue: "12:00", offset: 0, random: false, expression: "myExpression", preserveCtrlProps: true}, cfgNode]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.status.should.be.calledOnce(); dn1.should.have.property("name", "delay"); dn1.should.have.property("delayType", "pointInTime"); dn1.should.have.property("fixedDuration", 1); dn1.should.have.property("randomDuration1", 1); dn1.should.have.property("randomDuration2", 5); dn1.should.have.property("fixedDurationUnit", "seconds"); dn1.should.have.property("randomDurationUnit", "seconds"); dn1.should.have.property("randomizerMillis", false); dn1.should.have.property("whenType", "time"); dn1.should.have.property("whenValue", "12:00"); dn1.should.have.property("offset", 0); dn1.should.have.property("random", false); dn1.should.have.property("customDelayType", "jsonata"); dn1.should.have.property("customDelayValue", "myExpression"); dn1.should.have.property("preserveCtrlProps", true); dn1.should.have.property("ignoreCtrlProps", undefined); }); it("should fail due to missing configuration", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay"}]; await helper.load(delayNode, flow, {}); const dn1 = helper.getNode("dn1"); dn1.status.should.be.calledOnce(); dn1.error.should.be.calledOnce().and.calledWith("node-red-contrib-chronos/chronos-config:common.error.noConfig"); }); it("should fail due to invalid latitude", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1"}, cfgNode]; const invalidCredentials = {"cn1": {latitude: "", longitude: "10"}}; await helper.load([configNode, delayNode], flow, invalidCredentials); const dn1 = helper.getNode("dn1"); dn1.status.should.be.calledOnce(); dn1.error.should.be.calledOnce().and.calledWith("node-red-contrib-chronos/chronos-config:common.error.invalidConfig"); }); it("should fail due to invalid longitude", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1"}, cfgNode]; const invalidCredentials = {"cn1": {latitude: "50", longitude: ""}}; await helper.load([configNode, delayNode], flow, invalidCredentials); const dn1 = helper.getNode("dn1"); dn1.status.should.be.calledOnce(); dn1.error.should.be.calledOnce().and.calledWith("node-red-contrib-chronos/chronos-config:common.error.invalidConfig"); }); it("should fail due to invalid time zone", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1"}, cfgNodeInvalidTZ]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.status.should.be.calledOnce(); dn1.error.should.be.calledOnce().and.calledWith("node-red-contrib-chronos/chronos-config:common.error.invalidConfig"); }); it("should fail due to invalid when time", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "pointInTime", whenType: "time", whenValue: "invalid", offset: 0, random: false, preserveCtrlProps: true}, cfgNode]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.status.should.be.calledTwice(); dn1.error.should.be.calledOnce().and.calledWith("node-red-contrib-chronos/chronos-config:common.error.invalidConfig"); }); it("should fail due to invalid expression", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "custom", expression: "invalid[", preserveCtrlProps: true}, cfgNode]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.status.should.be.calledTwice(); dn1.error.should.be.calledTwice(); dn1.error.getCall(0).should.be.calledWith("Expected \"]\" before end of expression"); dn1.error.getCall(1).should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.invalidConfig"); }); }); context("duration delay", function() { let clock = null; beforeEach(function() { clock = sinon.useFakeTimers({toFake: ["Date", "setTimeout", "clearTimeout"]}); sinon.stub(chronos, "getCurrentTime").returns(moment().utc()); }); afterEach(function() { helper.unload(); sinon.restore(); }); it("should delay message for fixed duration", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "fixedDuration", fixedDuration: 3, fixedDurationUnit: "days", preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 259200000); dn1.msgQueue.should.have.length(1); clock.tick(259200000); // advance clock by 3 days dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should delay message for random duration", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "randomDuration", randomDuration1: 1, randomDuration2: 6, randomDurationUnit: "seconds", randomizerMillis: false, preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); sinon.stub(Math, "random").returns(0.5); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 4000); dn1.msgQueue.should.have.length(1); clock.tick(4000); // advance clock by 4 seconds dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should delay message for random duration (flipped boundaries)", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "randomDuration", randomDuration1: 6, randomDuration2: 1, randomDurationUnit: "minutes", randomizerMillis: false, preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); sinon.stub(Math, "random").returns(0.5); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 240000); dn1.msgQueue.should.have.length(1); clock.tick(240000); // advance clock by 4 minutes dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should delay message for random duration (equal boundaries)", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "randomDuration", randomDuration1: 6, randomDuration2: 6, randomDurationUnit: "hours", randomizerMillis: false, preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); sinon.stub(Math, "random").returns(0.5); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 21600000); dn1.msgQueue.should.have.length(1); clock.tick(21600000); // advance clock by 6 hours dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should delay message for random duration (milli precision)", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "randomDuration", randomDuration1: 1, randomDuration2: 6, randomDurationUnit: "seconds", randomizerMillis: true, preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); sinon.stub(Math, "random").returns(0.5); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 3500); dn1.msgQueue.should.have.length(1); clock.tick(3500); // advance clock by 3500 milliseconds dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should delay message for random duration (milli precision, flipped boundaries)", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "randomDuration", randomDuration1: 6, randomDuration2: 1, randomDurationUnit: "seconds", randomizerMillis: true, preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); sinon.stub(Math, "random").returns(0.5); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 3500); dn1.msgQueue.should.have.length(1); clock.tick(3500); // advance clock by 3500 milliseconds dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); }); context("time point delay (part 1)", function() { let clock = null; beforeEach(function() { clock = sinon.useFakeTimers({toFake: ["Date", "setTimeout", "clearTimeout"]}); sinon.stub(chronos, "getCurrentTime").returns(moment().utc()); }); afterEach(function() { helper.unload(); sinon.restore(); }); it("should delay message until specific time", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "pointInTime", whenType: "time", whenValue: "00:01", offset: 0, random: false, preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); dn1.msgQueue.should.have.length(1); clock.tick(60000); // advance clock by 1 min dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should trigger at specified time with offset", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], whenType: "time", delayType: "pointInTime", whenValue: "00:01", offset: 1, random: false, preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 120000); dn1.msgQueue.should.have.length(1); clock.tick(120000); // advance clock by 2 mins dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should trigger at specified time with random offset", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], whenType: "time", delayType: "pointInTime", whenValue: "00:01", offset: 2, random: true, preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); sinon.stub(Math, "random").returns(0.5); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 120000); dn1.msgQueue.should.have.length(1); clock.tick(120000); // advance clock by 2 mins dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should handle time error during setup of timer", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "pointInTime", whenType: "sun", whenValue: "sunset", offset: 2, random: true, preserveCtrlProps: true}, cfgNode]; sinon.stub(chronos, "getTime").throws(function() { return new chronos.TimeError("time error", {type: "sun", value: "sunset"}); }); await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.receive({payload: "test"}); dn1.error.should.be.calledWith("time error", {errorDetails: {type: "sun", value: "sunset"}}); dn1.msgQueue.should.have.length(0); }); it("should handle other error during setup of timer", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "pointInTime", whenType: "sun", whenValue: "sunset", offset: 2, random: true, preserveCtrlProps: true}, cfgNode]; sinon.stub(chronos, "getTime").throws("error", "error message"); await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.receive({payload: "test"}); dn1.error.should.be.calledWith("error message"); dn1.msgQueue.should.have.length(0); }); }); context("time point delay (part 2)", function() { let clock = null; beforeEach(function() { clock = sinon.useFakeTimers({now: 3600000, toFake: ["Date", "setTimeout", "clearTimeout"]}); sinon.stub(chronos, "getCurrentTime").returns(moment().utc()); }); afterEach(function() { helper.unload(); sinon.restore(); }); it("should trigger at specified time on next day", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "pointInTime", whenType: "time", whenValue: "00:30", offset: 0, random: false, preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 84600000); dn1.msgQueue.should.have.length(1); clock.tick(84600000); // advance clock by 23h and 30 mins dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should trigger at specified sun time on next day", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "pointInTime", whenType: "sun", whenValue: "sunrise", offset: 0, random: false, preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.stub(chronos, "getTime").returns(moment.utc(1800000)); sinon.spy(clock, "setTimeout"); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 84600000); dn1.msgQueue.should.have.length(1); clock.tick(84600000); // advance clock by 23h and 30 mins dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); }); context("custom delay", function() { let clock = null; beforeEach(function() { clock = sinon.useFakeTimers({now: 1000000000 /* Mon Jan 12 1970 13:46:40 UTC */, toFake: ["Date", "setTimeout", "clearTimeout"]}); sinon.stub(chronos, "getCurrentTime").returns(moment().utc()); sinon.stub(chronos, "getTimeFrom").callsFake(function(node, source) { return moment.utc(source); }); }); afterEach(function() { helper.unload(); sinon.restore(); }); it("should delay message for custom duration", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "custom", customDelayType: "jsonata", customDelayValue: "3000", preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 3000); dn1.msgQueue.should.have.length(1); clock.tick(3000); // advance clock by 3 seconds dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should delay message until custom time point (numeric)", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "custom", customDelayType: "jsonata", customDelayValue: "1000003000", preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 3000); dn1.msgQueue.should.have.length(1); clock.tick(3000); // advance clock by 3 seconds dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should delay message until custom time point (string)", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "custom", customDelayType: "jsonata", customDelayValue: "'1970-01-12T13:46:43'", preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 3000); dn1.msgQueue.should.have.length(1); clock.tick(3000); // advance clock by 3 seconds dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should delay message for fixed duration from context variable", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "custom", fixedDuration: 1, fixedDurationUnit: "seconds", customDelayType: "flow", customDelayValue: "testVariable", preserveCtrlProps: true}, hlpNode, cfgNode]; const ctx = {flow: {}, global: {}}; sinon.spy(clock, "setTimeout"); sinon.stub(helper._RED.util, "parseContextStore").returns({key: "testKey", store: "testStore"}); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); sinon.stub(dn1, "context").returns(ctx); ctx.flow.get = sinon.fake.returns({value: 3, unit: "days"}); ctx.global.get = sinon.fake.returns(undefined); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); helper._RED.util.parseContextStore.should.be.calledWith("testVariable"); dn1.context.should.be.calledOnce(); ctx.flow.get.should.be.calledWith("testKey", "testStore"); clock.setTimeout.should.be.calledWith(sinon.match.any, 259200000); dn1.msgQueue.should.have.length(1); clock.tick(259200000); // advance clock by 3 days dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should delay message for random duration from context variable", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "custom", randomDuration1: 6, randomDuration2: 10, randomDurationUnit: "minutes", randomizerMillis: false, customDelayType: "global", customDelayValue: "testVariable", preserveCtrlProps: true}, hlpNode, cfgNode]; const ctx = {flow: {}, global: {}}; sinon.spy(clock, "setTimeout"); sinon.stub(Math, "random").returns(0.5); sinon.stub(helper._RED.util, "parseContextStore").returns({key: "testKey", store: "testStore"}); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); sinon.stub(dn1, "context").returns(ctx); ctx.flow.get = sinon.fake.returns(undefined); ctx.global.get = sinon.fake.returns({value1: 1, value2: 6, unit: "seconds"}); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); helper._RED.util.parseContextStore.should.be.calledWith("testVariable"); dn1.context.should.be.calledOnce(); ctx.global.get.should.be.calledWith("testKey", "testStore"); clock.setTimeout.should.be.calledWith(sinon.match.any, 4000); dn1.msgQueue.should.have.length(1); clock.tick(4000); // advance clock by 4 seconds dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should delay message until specific time from context variable", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "custom", whenType: "sun", whenValue: "sunset", offset: 10, random: false, customDelayType: "flow", customDelayValue: "testVariable", preserveCtrlProps: true}, hlpNode, cfgNode]; const ctx = {flow: {}, global: {}}; sinon.spy(clock, "setTimeout"); sinon.stub(helper._RED.util, "parseContextStore").returns({key: "testKey", store: "testStore"}); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); sinon.stub(dn1, "context").returns(ctx); ctx.flow.get = sinon.fake.returns({type: "time", value: "13:47:40", offset: 0, random: false}); ctx.global.get = sinon.fake.returns(undefined); hn1.on("input", function(msg) { try { msg.should.have.property("payload", "test"); done(); } catch (e) { done(e); } }); dn1.receive({payload: "test"}); helper._RED.util.parseContextStore.should.be.calledWith("testVariable"); dn1.context.should.be.calledOnce(); ctx.flow.get.should.be.calledWith("testKey", "testStore"); clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); dn1.msgQueue.should.have.length(1); clock.tick(60000); // advance clock by 1 min dn1.msgQueue.should.have.length(0); } catch (e) { done(e); } }); }); it("should handle time error due to invalid expression", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "custom", customDelayType: "jsonata", customDelayValue: "$nonExistingFunc()", preserveCtrlProps: true}, cfgNode]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.receive({payload: "test"}); dn1.error.should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.evaluationFailed", {errorDetails: {expression: "$nonExistingFunc()", code: sinon.match.any, description: sinon.match.any, position: sinon.match.any, token: sinon.match.any}}); dn1.msgQueue.should.have.length(0); }); it("should handle time error due to invalid return value", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "custom", customDelayType: "jsonata", customDelayValue: "true", preserveCtrlProps: true}, cfgNode]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.receive({payload: "test"}); dn1.error.should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.notTime", {errorDetails: {expression: "true", result: true}}); dn1.msgQueue.should.have.length(0); }); it("should handle time error due to invalid relative numeric time (< 1)", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "custom", customDelayType: "jsonata", customDelayValue: "0", preserveCtrlProps: true}, cfgNode]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.receive({payload: "test"}); dn1.error.should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.intervalOutOfRange", {errorDetails: {expression: "0", result: 0}}); dn1.msgQueue.should.have.length(0); }); it("should handle time error due to invalid relative numeric time (> 604.800.000)", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "custom", customDelayType: "jsonata", customDelayValue: "1604801000", preserveCtrlProps: true}, cfgNode]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.receive({payload: "test"}); dn1.error.should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.intervalOutOfRange", {errorDetails: {expression: "1604801000", result: 1604801000}}); dn1.msgQueue.should.have.length(0); }); it("should handle time error due to invalid absolute numeric time (diff < 1)", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "custom", customDelayType: "jsonata", customDelayValue: "1000000000", preserveCtrlProps: true}, cfgNode]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.receive({payload: "test"}); dn1.error.should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.intervalOutOfRange", {errorDetails: {expression: "1000000000", result: 1000000000}}); dn1.msgQueue.should.have.length(0); }); it("should handle time error due to invalid absolute numeric time (diff > 604.800.000)", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "custom", customDelayType: "jsonata", customDelayValue: "1604801000", preserveCtrlProps: true}, cfgNode]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.receive({payload: "test"}); dn1.error.should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.intervalOutOfRange", {errorDetails: {expression: "1604801000", result: 1604801000}}); dn1.msgQueue.should.have.length(0); }); it("should handle time error due to invalid string time (diff < 1)", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "custom", customDelayType: "jsonata", customDelayValue: "'1970-01-12T13:46:40'", preserveCtrlProps: true}, cfgNode]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.receive({payload: "test"}); dn1.error.should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.intervalOutOfRange", {errorDetails: {expression: "'1970-01-12T13:46:40'", result: '1970-01-12T13:46:40'}}); dn1.msgQueue.should.have.length(0); }); it("should handle time error due to invalid string time (diff > 604.800.000))", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "custom", customDelayType: "jsonata", customDelayValue: "'1970-01-19T13:46:41'", preserveCtrlProps: true}, cfgNode]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.receive({payload: "test"}); dn1.error.should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.intervalOutOfRange", {errorDetails: {expression: "'1970-01-19T13:46:41'", result: '1970-01-19T13:46:41'}}); dn1.msgQueue.should.have.length(0); }); it("should handle time error due to invalid string time (no time))", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "custom", customDelayType: "jsonata", customDelayValue: "'invalid'", preserveCtrlProps: true}, cfgNode]; await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.receive({payload: "test"}); dn1.error.should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.notTime", {errorDetails: {expression: "'invalid'", result: 'invalid'}}); dn1.msgQueue.should.have.length(0); }); it("should handle time error due to invalid context variable", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "custom", customDelayType: "flow", customDelayValue: "invalidVariable", preserveCtrlProps: true}, cfgNode]; const ctx = {flow: {}, global: {}}; sinon.stub(helper._RED.util, "parseContextStore").returns({key: "testKey", store: "testStore"}); await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); sinon.stub(dn1, "context").returns(ctx); ctx.flow.get = sinon.fake.returns(false); ctx.global.get = sinon.fake.returns(undefined); dn1.receive({payload: "test"}); dn1.error.should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.invalidContext", {errorDetails: {store: "testStore", key: "testKey", value: false}}); dn1.msgQueue.should.have.length(0); }); }); context("control and override properties", function() { let clock = null; beforeEach(function() { clock = sinon.useFakeTimers({toFake: ["Date", "setTimeout", "clearTimeout"]}); sinon.stub(chronos, "getCurrentTime").returns(moment().utc()); }); afterEach(function() { helper.unload(); sinon.restore(); }); it("should drop messages", async function() { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", delayType: "pointInTime", whenType: "time", whenValue: "00:01", offset: 0, random: false, preserveCtrlProps: true}, cfgNode]; sinon.spy(clock, "setTimeout"); await helper.load([configNode, delayNode], flow, credentials); const dn1 = helper.getNode("dn1"); dn1.receive({payload: "test1"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); dn1.msgQueue.should.have.length(1); dn1.receive({payload: "test2"}); clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); dn1.msgQueue.should.have.length(2); dn1.receive({drop: true}); dn1.msgQueue.should.have.length(0); }); it("should drop messages and re-enqueue", function(done) { const flow = [{id: "dn1", type: "chronos-delay", name: "delay", config: "cn1", wires: [["hn1"]], delayType: "pointInTime", whenType: "time", whenValue: "00:01", offset: 0, random: false, preserveCtrlProps: true}, hlpNode, cfgNode]; sinon.spy(clock, "setTimeout"); helper.load([configNode, delayNode], flow, credentials, function() { try { const dn1 = helper.getNode("dn1"); const hn1 = helper.getNode("hn1"); hn1.on("input", function(msg) { try { msg.should.have.property("drop", true); msg.should.have.property("enqueue", true); done(); } catch (e) { done(e); } });