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
JavaScript
/*
* 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);
}
});