node-red-contrib-chronos
Version:
Time-based Node-RED scheduling, repeating, queueing, routing, filtering and manipulating nodes
1,107 lines (961 loc) • 104 kB
JavaScript
/*
* Copyright (c) 2020 - 2026 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 cfgNodeInvalidTZ = {id: "cn1", type: "chronos-config", name: "config", timezone: "invalid"};
const repeatNode = require("../nodes/repeat.js");
const chronos = require("../nodes/common/chronos.js");
const moment = require("moment");
const cronosjs = require("cronosjs");
require("should-sinon");
const cfgNode = {id: "cn1", type: "chronos-config", name: "config"};
const hlpNode = {id: "hn1", type: "helper"};
const credentials = {"cn1": {latitude: "50", longitude: "10"}};
describe("repeat until 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: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "*/5 * * * * *", customRepetitionType: "flow", customRepetitionValue: "myVar", untilType: "time", untilValue: "12:00", untilOffset: 0, untilRandom: false, preserveCtrlProps: true, ignoreCtrlProps: true, msgIngress: "forward"}, cfgNode];
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
rn1.status.should.be.calledOnce();
rn1.should.have.property("name", "repeat");
rn1.should.have.property("mode", "simple");
rn1.should.have.property("interval", 1);
rn1.should.have.property("intervalUnit", "seconds");
rn1.should.have.property("crontab", "*/5 * * * * *");
rn1.should.have.property("customRepetitionType", "flow");
rn1.should.have.property("customRepetitionValue", "myVar");
rn1.should.have.property("untilType", "time");
rn1.should.have.property("untilValue", "12:00");
rn1.should.have.property("untilOffset", 0);
rn1.should.have.property("untilRandom", false);
rn1.should.have.property("preserveCtrlProps", true);
rn1.should.have.property("ignoreCtrlProps", true);
rn1.should.have.property("msgIngress", "forward");
});
it("should preserve backward compatibility", async function()
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", interval: 1, intervalUnit: "seconds", expression: "myExpression", untilType: "time", untilValue: "12:00", untilOffset: 0, untilRandom: false, preserveCtrlProps: true}, cfgNode];
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
rn1.status.should.be.calledOnce();
rn1.should.have.property("name", "repeat");
rn1.should.have.property("mode", "simple");
rn1.should.have.property("interval", 1);
rn1.should.have.property("intervalUnit", "seconds");
rn1.should.have.property("crontab", "");
rn1.should.have.property("customRepetitionType", "jsonata");
rn1.should.have.property("customRepetitionValue", "myExpression");
rn1.should.have.property("untilValue", "12:00");
rn1.should.have.property("untilType", "time");
rn1.should.have.property("untilValue", "12:00");
rn1.should.have.property("untilOffset", 0);
rn1.should.have.property("untilRandom", false);
rn1.should.have.property("preserveCtrlProps", true);
rn1.should.have.property("msgIngress", "forward:forced");
});
it("should fail due to missing configuration", async function()
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat"}];
await helper.load(repeatNode, flow, {});
const rn1 = helper.getNode("rn1");
rn1.status.should.be.calledOnce();
rn1.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: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1"}, cfgNode];
const invalidCredentials = {"cn1": {latitude: "", longitude: "10"}};
await helper.load([configNode, repeatNode], flow, invalidCredentials);
const rn1 = helper.getNode("rn1");
rn1.status.should.be.calledOnce();
rn1.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: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1"}, cfgNode];
const invalidCredentials = {"cn1": {latitude: "50", longitude: ""}};
await helper.load([configNode, repeatNode], flow, invalidCredentials);
const rn1 = helper.getNode("rn1");
rn1.status.should.be.calledOnce();
rn1.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: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1"}, cfgNodeInvalidTZ];
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
rn1.status.should.be.calledOnce();
rn1.error.should.be.calledOnce().and.calledWith("node-red-contrib-chronos/chronos-config:common.error.invalidConfig");
});
it("should fail due to invalid cron table", async function()
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", mode: "advanced", interval: 1, intervalUnit: "seconds", crontab: "invalid", untilType: "time", untilValue: "invalid", untilOffset: 0, untilRandom: false, preserveCtrlProps: true}, cfgNode];
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
rn1.status.should.be.calledOnce();
rn1.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: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", mode: "custom", interval: 1, intervalUnit: "seconds", crontab: "", customRepetitionType: "jsonata", customRepetitionValue: "invalid[", untilType: "nextMsg", untilValue: "", untilDate: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: false}, cfgNode];
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
rn1.receive({payload: "test"});
rn1.error.getCall(0).should.be.calledWith("Expected \"]\" before end of expression");
rn1.error.getCall(1).should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.invalidConfig");
});
it("should fail due to invalid until time", async function()
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "time", untilValue: "invalid", untilOffset: 0, untilRandom: false, preserveCtrlProps: true}, cfgNode];
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
rn1.status.should.be.calledTwice();
rn1.error.should.be.calledOnce().and.calledWith("node-red-contrib-chronos/chronos-config:common.error.invalidConfig");
});
it("should fail due to invalid until expression", async function()
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "jsonata", untilValue: "invalid[", untilDate: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: false}, cfgNode];
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
rn1.receive({payload: "test"});
rn1.error.getCall(0).should.be.calledWith("Expected \"]\" before end of expression");
rn1.error.getCall(1).should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.invalidConfig");
});
});
context("message ingress behavior", function()
{
let clock = null;
let curTime = 0;
beforeEach(function()
{
curTime = 0;
clock = sinon.useFakeTimers({toFake: ["setTimeout", "clearTimeout"]});
sinon.stub(chronos, "getCurrentTime").callsFake(function() { return moment.utc(curTime); });
});
afterEach(function()
{
helper.unload();
sinon.restore();
});
it("should never forward message on ingress", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "nextMsg", untilValue: "", untilOffset: 0, untilRandom: false, msgIngress: "noop", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
done("unexpected message received");
});
rn1.receive({payload: "test"});
done();
}
catch (e)
{
done(e);
}
});
});
it("should not forward message on ingress due to exceeded until time", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "time", untilValue: "00:00", untilOffset: 0, untilRandom: false, msgIngress: "forward", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
done("unexpected message received");
});
curTime += 60000;
rn1.receive({payload: "test"});
done();
}
catch (e)
{
done(e);
}
});
});
it("should forward message on ingress (until time not reached)", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "time", untilValue: "00:00", untilOffset: 0, untilRandom: false, msgIngress: "forward", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
try
{
msg.should.have.property("payload", "test");
done();
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test"});
}
catch (e)
{
done(e);
}
});
});
it("should forward message on ingress (no until time)", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "nextMsg", untilValue: "", untilOffset: 0, untilRandom: false, msgIngress: "forward", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
try
{
msg.should.have.property("payload", "test");
done();
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test"});
}
catch (e)
{
done(e);
}
});
});
it("should forward message on ingress (forced)", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "time", untilValue: "00:00", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
try
{
msg.should.have.property("payload", "test");
done();
}
catch (e)
{
done(e);
}
});
curTime += 60000;
rn1.receive({payload: "test"});
}
catch (e)
{
done(e);
}
});
});
it("should override ingress behavior", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "nextMsg", untilValue: "", untilOffset: 0, untilRandom: false, msgIngress: "noop", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
try
{
msg.should.have.property("payload", "test");
msg.should.have.property("ingress", "forward");
done();
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test", ingress: "forward"});
}
catch (e)
{
done(e);
}
});
});
it("should override ingress behavior and not preserve", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "nextMsg", untilValue: "", untilOffset: 0, untilRandom: false, msgIngress: "noop", preserveCtrlProps: false}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
try
{
msg.should.have.property("payload", "test");
msg.should.not.have.property("ingress");
done();
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test", ingress: "forward"});
}
catch (e)
{
done(e);
}
});
});
function testInvalidIngressOverride(title, override)
{
it("should fall back to configured ingress behavior: " + title, function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "nextMsg", untilValue: "", untilOffset: 0, untilRandom: false, msgIngress: "forward", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
try
{
msg.should.have.property("payload", "test");
msg.should.have.property("ingress", override);
done();
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test", ingress: override});
}
catch (e)
{
done(e);
}
});
});
}
testInvalidIngressOverride("null override", null);
testInvalidIngressOverride("empty override", "");
testInvalidIngressOverride("invalid override", 42);
testInvalidIngressOverride("invalid ingress", "invalid");
});
context("message repeating in simple mode (until next message)", 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();
});
function testRepeatInterval(interval, unit)
{
it("should repeat message every " + interval + " " + unit, function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], interval: interval, intervalUnit: unit, untilType: "nextMsg", untilValue: "", untilOffset: 0, untilRandom: false, preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
const intvMs = (unit == "seconds") ? interval * 1000 : (unit == "minutes") ? interval * 60000 : interval * 3600000;
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
try
{
msg.should.have.property("payload", "test");
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test"});
clock.tick(intvMs);
clock.tick(intvMs);
clock.tick(intvMs);
clock.setTimeout.should.be.calledWith(sinon.match.any, intvMs).and.have.callCount(4);
done();
}
catch (e)
{
done(e);
}
});
});
}
testRepeatInterval(4, "seconds");
testRepeatInterval(3, "minutes");
testRepeatInterval(2, "hours");
it("should repeat message until next message is received", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "nextMsg", untilValue: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
let count = 0;
hn1.on("input", function(msg)
{
try
{
count++;
msg.should.have.property("payload", "test" + ((count <= 3) ? 1 : 2));
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test1"});
clock.tick(1000);
clock.tick(1000);
rn1.receive({payload: "test2"});
clock.tick(1000);
clock.tick(1000);
clock.setTimeout.should.be.calledWith(sinon.match.any, 1000).and.have.callCount(6);
done();
}
catch (e)
{
done(e);
}
});
});
it("should repeat message until stopped", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "nextMsg", untilValue: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
let count = 0;
hn1.on("input", function(msg)
{
try
{
count++;
if (count <= 3)
{
msg.should.have.property("payload", "test");
}
else
{
done("unexpected message received");
}
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test"});
clock.tick(1000);
clock.tick(1000);
clock.setTimeout.should.be.calledWith(sinon.match.any, 1000).and.have.callCount(3);
clock.setTimeout.resetHistory()
rn1.receive({stop: true});
clock.tick(1000);
clock.tick(1000);
clock.setTimeout.should.not.be.called();
done();
}
catch (e)
{
done(e);
}
});
});
it("should override interval", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "nextMsg", untilValue: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
let count = 0;
hn1.on("input", function(msg)
{
try
{
count++;
msg.should.have.property("payload", "test" + ((count <= 3) ? 1 : 2));
msg.should.have.property("interval", {value: ((count <= 3) ? 2 : 3), unit: "seconds"});
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test1", interval: {value: 2, unit: "seconds"}});
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.setTimeout.should.be.calledWith(sinon.match.any, 2000).and.have.callCount(3);
clock.setTimeout.resetHistory();
rn1.receive({payload: "test2", interval: {value: 3, unit: "seconds"}});
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.setTimeout.should.be.calledWith(sinon.match.any, 3000).and.have.callCount(3);
done();
}
catch (e)
{
done(e);
}
});
});
it("should override interval and not preserve", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "nextMsg", untilValue: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: false}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
let count = 0;
hn1.on("input", function(msg)
{
try
{
count++;
msg.should.have.property("payload", "test" + ((count <= 3) ? 1 : 2));
msg.should.not.have.property("interval");
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test1", interval: {value: 2, unit: "seconds"}});
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.setTimeout.should.be.calledWith(sinon.match.any, 2000).and.have.callCount(3);
clock.setTimeout.resetHistory();
rn1.receive({payload: "test2", interval: {value: 3, unit: "seconds"}});
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.setTimeout.should.be.calledWith(sinon.match.any, 3000).and.have.callCount(3);
done();
}
catch (e)
{
done(e);
}
});
});
function testInvalidIntervalOverride(title, override)
{
it("should fall back to configured interval: " + title, function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "nextMsg", untilValue: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
try
{
msg.should.have.property("payload", "test");
msg.should.have.property("interval", override);
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test", interval: override});
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.setTimeout.should.be.calledWith(sinon.match.any, 1000).and.have.callCount(5);
done();
}
catch (e)
{
done(e);
}
});
});
}
testInvalidIntervalOverride("invalid override", "invalid");
testInvalidIntervalOverride("null override", null);
testInvalidIntervalOverride("empty override", {});
testInvalidIntervalOverride("no value", {unit: "hours"});
testInvalidIntervalOverride("no uniot", {value: 4});
testInvalidIntervalOverride("invalid unit", {value: 4, unit: "invalid"});
testInvalidIntervalOverride("invalid value", {value: "invalid", unit: "seconds"});
testInvalidIntervalOverride("value too small for seconds", {value: 0, unit: "seconds"});
testInvalidIntervalOverride("value too large for seconds", {value: 60, unit: "seconds"});
testInvalidIntervalOverride("value too small for minutes", {value: 0, unit: "minutes"});
testInvalidIntervalOverride("value too large for minutes", {value: 60, unit: "minutes"});
testInvalidIntervalOverride("value too small for hours", {value: 0, unit: "hours"});
testInvalidIntervalOverride("value too large for hours", {value: 25, unit: "hours"});
it("should ignore overridden interval", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "nextMsg", untilValue: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: true, ignoreCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
try
{
msg.should.have.property("payload", "test");
msg.should.have.property("interval", 4000);
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test", interval: 4000});
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.tick(1000);
clock.setTimeout.should.be.calledWith(sinon.match.any, 1000).and.have.callCount(5);
done();
}
catch (e)
{
done(e);
}
});
});
it("should handle time error", async function()
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "time", untilValue: "00:00", untilDate: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: false}, cfgNode];
sinon.stub(chronos, "getTime").throws(function() { return new chronos.TimeError("time error", {type: "sun", value: "sunset"}); });
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
rn1.receive({payload: "test"});
rn1.error.should.be.calledWith("time error", {_msgid: sinon.match.any, payload: "test", errorDetails: {type: "sun", value: "sunset"}});
});
it("should handle time error (rename errorDetails)", async function()
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "time", untilValue: "00:00", untilDate: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: false}, cfgNode];
sinon.stub(chronos, "getTime").throws(function() { return new chronos.TimeError("time error", {type: "sun", value: "sunset"}); });
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
rn1.receive({payload: "test", errorDetails: "details"});
rn1.error.should.be.calledWith("time error", {_msgid: sinon.match.any, payload: "test", _errorDetails: "details", errorDetails: {type: "sun", value: "sunset"}});
});
it("should handle time error (no details)", async function()
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "time", untilValue: "00:00", untilDate: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: false}, cfgNode];
sinon.stub(chronos, "getTime").throws(function() { return new chronos.TimeError("time error", null); });
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
rn1.receive({payload: "test"});
rn1.error.should.be.calledWith("time error", {_msgid: sinon.match.any, payload: "test"});
});
it("should handle other error", async function()
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "time", untilValue: "00:00", untilDate: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: false}, cfgNode];
sinon.stub(chronos, "getTime").throws("error", "error message");
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
rn1.receive({payload: "test"});
rn1.error.should.be.calledWith("error message");
});
it("should handle time error caused by ending time (invalid JSONata expression)", async function()
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "jsonata", untilValue: "$invalFunc()", untilDate: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: false}, cfgNode];
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
rn1.receive({payload: "test"});
rn1.error.should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.evaluationFailed", {payload: "test", _msgid: sinon.match.any, errorDetails: {expression: "$invalFunc()", code: sinon.match.any, description: sinon.match.any, position: sinon.match.any, token: sinon.match.any}});
});
it("should handle time error caused by ending time (not boolean)", async function()
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "jsonata", untilValue: "42", untilDate: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: false}, cfgNode];
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
rn1.receive({payload: "test"});
rn1.error.should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.notBoolean", {payload: "test", _msgid: sinon.match.any, errorDetails: {expression: "42", result: 42}});
});
it("should handle time error caused by invalid ending time from context variable", async function()
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "flow", untilValue: "invalidVariable", untilDate: "", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: false}, cfgNode];
const ctx = {flow: {}, global: {}};
sinon.stub(helper._RED.util, "parseContextStore").returns({key: "testKey", store: "testStore"});
await helper.load([configNode, repeatNode], flow, credentials);
const rn1 = helper.getNode("rn1");
sinon.stub(rn1, "context").returns(ctx);
ctx.flow.get = sinon.fake.returns("42");
ctx.global.get = sinon.fake.returns(undefined);
rn1.receive({payload: "test"});
rn1.error.should.be.calledWith("node-red-contrib-chronos/chronos-config:common.error.invalidContext", {payload: "test", _msgid: sinon.match.any, errorDetails: {store: "testStore", key: "testKey", value: "42"}});
});
});
context("message repeating in simple mode (until specifc time)", function()
{
let clock = null;
let curTime = 0;
beforeEach(function()
{
curTime = 0;
clock = sinon.useFakeTimers({toFake: ["setTimeout", "clearTimeout"]});
sinon.stub(chronos, "getCurrentTime").callsFake(function() { return moment.utc(curTime); });
sinon.stub(chronos, "getUserDate").callsFake(function(RED, node, value) { return moment.utc(value, "YYYY-MM-DD"); });
});
afterEach(function()
{
helper.unload();
sinon.restore();
});
it("should repeat message until ending time is reached", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "seconds", crontab: "", untilType: "time", untilValue: "00:00:03", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
try
{
msg.should.have.property("payload", "test");
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test"});
curTime += 1000;
clock.tick(1000);
curTime += 1000;
clock.tick(1000);
curTime += 1000;
clock.tick(1000);
curTime += 1000;
clock.tick(1000);
curTime += 1000;
clock.tick(1000);
curTime += 1000;
clock.tick(1000);
clock.setTimeout.should.be.calledWith(sinon.match.any, 1000).and.have.callCount(3);
done();
}
catch (e)
{
done(e);
}
});
});
it("should repeat message until ending time at specific date is reached", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 24, intervalUnit: "hours", crontab: "", untilType: "time", untilValue: "00:05:00", untilDate: "2000-01-03", untilOffset: 0, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
curTime = 946684800000; // 2000-01-01 00:00:00
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
try
{
msg.should.have.property("payload", "test");
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test"});
curTime += 86400000;
clock.tick(86400000);
curTime += 86400000;
clock.tick(86400000);
curTime += 86400000;
clock.tick(86400000);
curTime += 86400000;
clock.tick(86400000);
curTime += 86400000;
clock.tick(86400000);
curTime += 86400000;
clock.tick(86400000);
clock.setTimeout.should.be.calledWith(sinon.match.any, 86400000).and.have.callCount(2);
done();
}
catch (e)
{
done(e);
}
});
});
it("should repeat message until ending time with offset is reached", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "minutes", crontab: "", untilType: "time", untilValue: "00:02:00", untilOffset: 1, untilRandom: false, msgIngress: "forward:forced", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.getNode("hn1");
hn1.on("input", function(msg)
{
try
{
msg.should.have.property("payload", "test");
}
catch (e)
{
done(e);
}
});
rn1.receive({payload: "test"});
curTime += 60000;
clock.tick(60000);
curTime += 60000;
clock.tick(60000);
curTime += 60000;
clock.tick(60000);
curTime += 60000;
clock.tick(60000);
curTime += 60000;
clock.tick(60000);
curTime += 60000;
clock.tick(60000);
clock.setTimeout.should.be.calledWith(sinon.match.any, 60000).and.have.callCount(3);
done();
}
catch (e)
{
done(e);
}
});
});
it("should repeat message until ending time with random offset is reached", function(done)
{
const flow = [{id: "rn1", type: "chronos-repeat", name: "repeat", config: "cn1", wires: [["hn1"]], mode: "simple", interval: 1, intervalUnit: "minutes", crontab: "", untilType: "time", untilValue: "00:02:00", untilOffset: 2, untilRandom: true, msgIngress: "forward:forced", preserveCtrlProps: true}, hlpNode, cfgNode];
sinon.spy(clock, "setTimeout");
sinon.stub(Math, "random").returns(0.5);
helper.load([configNode, repeatNode], flow, credentials, function()
{
try
{
const rn1 = helper.getNode("rn1");
const hn1 = helper.