Update Node/Flow to trigger msg routing hooks

This commit is contained in:
Nick O'Leary
2020-07-30 17:52:39 +01:00
parent 27c0e45940
commit 08148a07b2
4 changed files with 900 additions and 294 deletions

View File

@@ -19,6 +19,7 @@ var sinon = require('sinon');
var NR_TEST_UTILS = require("nr-test-utils");
var RedNode = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
var Log = NR_TEST_UTILS.require("@node-red/util").log;
var hooks = NR_TEST_UTILS.require("@node-red/runtime/lib/hooks");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
describe('Node', function() {
@@ -181,6 +182,93 @@ describe('Node', function() {
});
n.receive(message);
});
it('triggers onComplete hook when done callback provided', function(done) {
var handleCompleteCalled = false;
var hookCalled = false;
var n = new RedNode({id:'123',type:'abc', _flow: {
handleComplete: function(node,msg) {
handleCompleteCalled = true;
}
}});
var hookError;
hooks.add("onComplete",function(completeEvent) {
hookCalled = true;
try {
handleCompleteCalled.should.be.false("onComplete should be called before handleComplete")
should.not.exist(completeEvent.error);
completeEvent.msg.should.deepEqual(message);
completeEvent.node.id.should.eql("123");
completeEvent.node.node.should.equal(n);
} catch(err) {
hookError = err;
}
})
var message = {payload:"hello world"};
n.on('input',function(msg, nodeSend, nodeDone) {
nodeDone();
});
n.receive(message);
setTimeout(function() {
if (hookError) {
done(hookError);
return
}
try {
hookCalled.should.be.true("onComplete hook should be called");
handleCompleteCalled.should.be.true("handleComplete should be called");
done();
} catch(err) {
done(err);
}
})
});
it('triggers onComplete hook when done callback provided - with error', function(done) {
var handleCompleteCalled = false;
var hookCalled = false;
var errorReported = false;
var n = new RedNode({id:'123',type:'abc', _flow: {
handleComplete: function(node,msg) {
handleCompleteCalled = true;
}
}});
var hookError;
hooks.add("onComplete",function(completeEvent) {
hookCalled = true;
try {
handleCompleteCalled.should.be.false("onComplete should be called before handleComplete")
should.exist(completeEvent.error);
completeEvent.error.toString().should.equal("Error: test error")
completeEvent.msg.should.deepEqual(message);
completeEvent.node.id.should.eql("123");
completeEvent.node.node.should.equal(n);
} catch(err) {
hookError = err;
}
})
var message = {payload:"hello world"};
n.on('input',function(msg, nodeSend, nodeDone) {
nodeDone(new Error("test error"));
});
n.error = function(err,msg) {
errorReported = true;
}
n.receive(message);
setTimeout(function() {
if (hookError) {
done(hookError);
return
}
try {
hookCalled.should.be.true("onComplete hook should be called");
handleCompleteCalled.should.be.false("handleComplete should not be called");
done();
} catch(err) {
done(err);
}
})
});
it('logs error if callback provides error', function(done) {
var n = new RedNode({id:'123',type:'abc'});
sinon.stub(n,"error",function(err,msg) {});
@@ -201,153 +289,250 @@ describe('Node', function() {
});
n.receive(message);
});
it("triggers hooks when receiving a message", function(done) {
var hookErrors = [];
var messageReceived = false;
var hooksCalled = [];
hooks.add("onReceive", function(receiveEvent) {
hooksCalled.push("onReceive")
try {
messageReceived.should.be.false("Message should not have been received before onReceive")
receiveEvent.msg.should.be.exactly(message);
receiveEvent.destination.id.should.equal("123")
receiveEvent.destination.node.should.equal(n)
} catch(err) {
hookErrors.push(err);
}
})
hooks.add("postReceive", function(receiveEvent) {
hooksCalled.push("postReceive")
try {
messageReceived.should.be.true("Message should have been received before postReceive")
receiveEvent.msg.should.be.exactly(message);
receiveEvent.destination.id.should.equal("123")
receiveEvent.destination.node.should.equal(n)
} catch(err) {
hookErrors.push(err);
}
})
var n = new RedNode({id:'123',type:'abc'});
var message = {payload:"hello world"};
n.on('input',function(msg) {
messageReceived = true;
try {
should.strictEqual(this,n);
hooksCalled.should.eql(["onReceive"])
should.deepEqual(msg,message);
} catch(err) {
hookErrors.push(err)
}
});
n.receive(message);
setTimeout(function() {
hooks.clear();
if (hookErrors.length > 0) {
done(hookErrors[0])
} else {
done();
}
},10);
});
describe("errors thrown by hooks are reported", function() {
before(function() {
hooks.add("onReceive",function(recEvent) {
if (recEvent.msg.payload === "trigger-onReceive") {
throw new Error("onReceive Error")
}
})
hooks.add("postReceive",function(recEvent) {
if (recEvent.msg.payload === "trigger-postReceive") {
throw new Error("postReceive Error")
}
})
})
after(function() {
hooks.clear();
})
function testHook(hook, msgExpected, done) {
var messageReceived = false;
var errorReceived;
var n = new RedNode({id:'123',type:'abc'});
var message = {payload:"trigger-"+hook};
n.on('input',function(msg) {
messageReceived = true;
});
n.error = function (err) {
errorReceived = err;
}
n.receive(message);
setTimeout(function() {
try {
messageReceived.should.equal(msgExpected,`Hook ${hook} messageReceived expected ${msgExpected} actual ${messageReceived}`);
should.exist(errorReceived);
errorReceived.toString().should.containEql(hook)
done()
} catch(err) {
done(err);
}
},10);
}
it("onReceive", function(done) { testHook("onReceive", false, done)})
it("postReceive", function(done) { testHook("postReceive", true, done)})
})
});
describe("hooks can halt receive", function() {
before(function() {
hooks.add("onReceive",function(recEvent) {
if (recEvent.msg.payload === "trigger-onReceive") {
return false;
}
})
})
after(function() {
hooks.clear();
})
function testHook(hook, msgExpected, done) {
var messageReceived = false;
var errorReceived;
var n = new RedNode({id:'123',type:'abc'});
var message = {payload:"trigger-"+hook};
n.on('input',function(msg) {
messageReceived = true;
});
n.error = function (err) {
errorReceived = err;
}
n.receive(message);
setTimeout(function() {
try {
messageReceived.should.equal(msgExpected,`Hook ${hook} messageReceived expected ${msgExpected} actual ${messageReceived}`);
should.not.exist(errorReceived);
done()
} catch(err) {
done(err);
}
},10);
}
it("onReceive", function(done) { testHook("onReceive", false, done)})
})
describe('#send', function() {
it('emits a single message', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
send: (node,dst,msg) => { setImmediate(function() { n2.receive(msg) ;})}
send: (sendEvents) => {
try {
sendEvents.should.have.length(1);
sendEvents[0].msg.should.equal(message);
sendEvents[0].destination.should.eql({id:"n2", node: undefined});
sendEvents[0].source.should.eql({id:"n1", node: n1, port: 0})
done();
} catch(err) {
done(err);
}
},
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"};
var messageReceived = false;
n2.on('input',function(msg) {
// msg equals message, and is not a new copy
messageReceived = true;
should.strictEqual(this,n2);
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done();
});
n1.send(message);
messageReceived.should.be.false();
});
it('emits a single message - multiple input event listeners', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"};
var messageReceived = 0;
n2.on('input',function(msg) {
// msg equals message, and is not a new copy
messageReceived++;
messageReceived.should.be.exactly(1);
should.strictEqual(this,n2);
should.deepEqual(msg,message);
should.strictEqual(msg,message);
});
n2.on('input',function(msg) {
messageReceived++;
messageReceived.should.be.exactly(2);
should.strictEqual(this,n2);
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done();
});
n1.send(message);
messageReceived.should.be.exactly(0);
});
it('emits a single message - synchronous mode', function(done) {
var flow = {
handleError: (node,logMessage,msg,reportingNode) => {done(logMessage)},
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
send: (node,dst,msg) => { n2.receive(msg) ;},
asyncMessageDelivery: false
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"};
var messageReceived = false;
var notSyncErr;
n2.on('input',function(msg) {
try {
// msg equals message, and is not a new copy
messageReceived = true;
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done(notSyncErr);
} catch(err) {
done(err);
}
});
n1.send(message);
try {
messageReceived.should.be.true();
} catch(err) {
notSyncErr = err;
}
});
it('emits a message with callback provided send', function(done) {
var flow = {
handleError: (node,logMessage,msg,reportingNode) => {done(logMessage)},
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
handleComplete: (node,msg) => {},
send: (node,dst,msg) => { setImmediate(function() { n2.receive(msg) ;})}
send: (sendEvents) => {
try {
sendEvents.should.have.length(1);
sendEvents[0].msg.should.equal(message);
sendEvents[0].destination.should.eql({id:"n2", node: undefined});
sendEvents[0].source.should.eql({id:"n1", node: n1, port: 0});
sendEvents[0].cloneMessage.should.be.false();
done();
} catch(err) {
done(err);
}
},
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"};
var messageReceived = false;
n1.on('input',function(msg,nodeSend,nodeDone) {
nodeSend(msg);
nodeDone();
});
n2.on('input',function(msg) {
// msg equals message, and is not a new copy
messageReceived = true;
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done();
});
n1.receive(message);
messageReceived.should.be.false();
});
it('emits multiple messages on a single output', function(done) {
var flow = {
handleError: (node,logMessage,msg,reportingNode) => {done(logMessage)},
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
send: (node,dst,msg) => { setImmediate(function() { n2.receive(msg) ;})}
send: (sendEvents) => {
try {
sendEvents.should.have.length(2);
sendEvents[0].msg.should.equal(messages[0]);
sendEvents[0].destination.should.eql({id:"n2", node: undefined});
sendEvents[0].source.should.eql({id:"n1", node: n1, port: 0});
sendEvents[0].cloneMessage.should.be.false();
sendEvents[1].msg.should.equal(messages[1]);
sendEvents[1].destination.should.eql({id:"n2", node: undefined});
sendEvents[1].source.should.eql({id:"n1", node: n1, port: 0});
sendEvents[1].cloneMessage.should.be.true();
done();
} catch(err) {
done(err);
}
},
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var messages = [
{payload:"hello world"},
{payload:"hello world again"}
];
var rcvdCount = 0;
n2.on('input',function(msg) {
if (rcvdCount === 0) {
// first msg sent, don't clone
should.deepEqual(msg,messages[rcvdCount]);
should.strictEqual(msg,messages[rcvdCount]);
rcvdCount += 1;
} else {
// second msg sent, clone
msg.payload.should.equal(messages[rcvdCount].payload);
should.notStrictEqual(msg,messages[rcvdCount]);
done();
}
});
n1.send([messages]);
});
it('emits messages to multiple outputs', function(done) {
var flow = {
handleError: (node,logMessage,msg,reportingNode) => {done(logMessage)},
getNode: (id) => { return {'n1':n1,'n2':n2,'n3':n3,'n4':n4,'n5':n5}[id]},
send: (node,dst,msg) => { setImmediate(function() { flow.getNode(dst).receive(msg) })}
send: (sendEvents) => {
try {
sendEvents.should.have.length(3);
sendEvents[0].msg.should.equal(messages[0]);
sendEvents[0].destination.should.eql({id:"n2", node: undefined});
sendEvents[0].source.should.eql({id:"n1", node: n1, port: 0});
sendEvents[0].cloneMessage.should.be.false();
should.exist(sendEvents[0].msg._msgid);
sendEvents[1].msg.should.equal(messages[2]);
sendEvents[1].destination.should.eql({id:"n4", node: undefined});
sendEvents[1].source.should.eql({id:"n1", node: n1, port: 2})
sendEvents[1].cloneMessage.should.be.true();
should.exist(sendEvents[1].msg._msgid);
sendEvents[2].msg.should.equal(messages[2]);
sendEvents[2].destination.should.eql({id:"n5", node: undefined});
sendEvents[2].source.should.eql({id:"n1", node: n1, port: 2})
sendEvents[2].cloneMessage.should.be.true();
should.exist(sendEvents[2].msg._msgid);
sendEvents[0].msg._msgid.should.eql(sendEvents[1].msg._msgid)
sendEvents[1].msg._msgid.should.eql(sendEvents[2].msg._msgid)
done();
} catch(err) {
done(err);
}
}
};
var n1 = new RedNode({_flow:flow, id:'n1',type:'abc',wires:[['n2'],['n3'],['n4','n5']]});
var n2 = new RedNode({_flow:flow, id:'n2',type:'abc'});
@@ -362,43 +547,6 @@ describe('Node', function() {
var rcvdCount = 0;
// first message sent, don't clone
// message uuids should match
n2.on('input',function(msg) {
should.deepEqual(msg,messages[0]);
should.strictEqual(msg,messages[0]);
rcvdCount += 1;
if (rcvdCount == 3) {
done();
}
});
n3.on('input',function(msg) {
should.fail(null,null,"unexpected message");
});
// second message sent, clone
// message uuids wont match since we've cloned
n4.on('input',function(msg) {
msg.payload.should.equal(messages[2].payload);
should.notStrictEqual(msg,messages[2]);
rcvdCount += 1;
if (rcvdCount == 3) {
done();
}
});
// third message sent, clone
// message uuids wont match since we've cloned
n5.on('input',function(msg) {
msg.payload.should.equal(messages[2].payload);
should.notStrictEqual(msg,messages[2]);
rcvdCount += 1;
if (rcvdCount == 3) {
done();
}
});
n1.send(messages);
});
@@ -421,107 +569,85 @@ describe('Node', function() {
n1.send();
});
it('emits messages ignoring non-existent nodes', function(done) {
var flow = {
handleError: (node,logMessage,msg,reportingNode) => {done(logMessage)},
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
send: (node,dst,msg) => { setImmediate(function() { var n = flow.getNode(dst); n && n.receive(msg) })}
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n9'],['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
// it('emits messages without cloning req or res', function(done) {
// var flow = {
// getNode: (id) => { return {'n1':n1,'n2':n2,'n3':n3}[id]},
// send: (node,dst,msg) => { setImmediate(function() { flow.getNode(dst).receive(msg) })}
// };
// var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[[['n2'],['n3']]]});
// var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
// var n3 = new RedNode({_flow:flow,id:'n3',type:'abc'});
//
// var req = {};
// var res = {};
// var cloned = {};
// var message = {payload: "foo", cloned: cloned, req: req, res: res};
//
// var rcvdCount = 0;
//
// // first message to be sent, so should not be cloned
// n2.on('input',function(msg) {
// should.deepEqual(msg, message);
// msg.cloned.should.be.exactly(message.cloned);
// msg.req.should.be.exactly(message.req);
// msg.res.should.be.exactly(message.res);
// rcvdCount += 1;
// if (rcvdCount == 2) {
// done();
// }
// });
//
// // second message to be sent, so should be cloned
// // message uuids wont match since we've cloned
// n3.on('input',function(msg) {
// msg.payload.should.equal(message.payload);
// msg.cloned.should.not.be.exactly(message.cloned);
// msg.req.should.be.exactly(message.req);
// msg.res.should.be.exactly(message.res);
// rcvdCount += 1;
// if (rcvdCount == 2) {
// done();
// }
// });
//
// n1.send(message);
// });
var messages = [
{_msgid:"123", payload:"hello world"},
{_msgid:"234", payload:"hello world again"}
];
n2.on('input',function(msg) {
should.deepEqual(msg,messages[1]);
done();
});
n1.send(messages);
});
it('emits messages without cloning req or res', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2,'n3':n3}[id]},
send: (node,dst,msg) => { setImmediate(function() { flow.getNode(dst).receive(msg) })}
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[[['n2'],['n3']]]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var n3 = new RedNode({_flow:flow,id:'n3',type:'abc'});
var req = {};
var res = {};
var cloned = {};
var message = {payload: "foo", cloned: cloned, req: req, res: res};
var rcvdCount = 0;
// first message to be sent, so should not be cloned
n2.on('input',function(msg) {
should.deepEqual(msg, message);
msg.cloned.should.be.exactly(message.cloned);
msg.req.should.be.exactly(message.req);
msg.res.should.be.exactly(message.res);
rcvdCount += 1;
if (rcvdCount == 2) {
done();
}
});
// second message to be sent, so should be cloned
// message uuids wont match since we've cloned
n3.on('input',function(msg) {
msg.payload.should.equal(message.payload);
msg.cloned.should.not.be.exactly(message.cloned);
msg.req.should.be.exactly(message.req);
msg.res.should.be.exactly(message.res);
rcvdCount += 1;
if (rcvdCount == 2) {
done();
}
});
n1.send(message);
});
it("logs the uuid for all messages sent", function(done) {
var logHandler = {
msgIds:[],
messagesSent: 0,
emit: function(event, msg) {
if (msg.event == "node.abc.send" && msg.level == Log.METRIC) {
this.messagesSent++;
this.msgIds.push(msg.msgid);
(typeof msg.msgid).should.not.be.equal("undefined");
}
}
};
Log.addHandler(logHandler);
var flow = {
getNode: (id) => { return {'n1':sender,'n2':receiver1,'n3':receiver2}[id]},
send: (node,dst,msg) => { setImmediate(function() { flow.getNode(dst).receive(msg) })}
};
var sender = new RedNode({_flow:flow,id:'n1',type:'abc', wires:[['n2', 'n3']]});
var receiver1 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var receiver2 = new RedNode({_flow:flow,id:'n3',type:'abc'});
sender.send({"some": "message"});
setTimeout(function() {
try {
logHandler.messagesSent.should.equal(1);
should.exist(logHandler.msgIds[0])
Log.removeHandler(logHandler);
done();
} catch(err) {
Log.removeHandler(logHandler);
done(err);
}
},50)
})
// it("logs the uuid for all messages sent", function(done) {
// var logHandler = {
// msgIds:[],
// messagesSent: 0,
// emit: function(event, msg) {
// if (msg.event == "node.abc.send" && msg.level == Log.METRIC) {
// this.messagesSent++;
// this.msgIds.push(msg.msgid);
// (typeof msg.msgid).should.not.be.equal("undefined");
// }
// }
// };
//
// Log.addHandler(logHandler);
// var flow = {
// getNode: (id) => { return {'n1':sender,'n2':receiver1,'n3':receiver2}[id]},
// send: (node,dst,msg) => { setImmediate(function() { flow.getNode(dst).receive(msg) })}
// };
//
// var sender = new RedNode({_flow:flow,id:'n1',type:'abc', wires:[['n2', 'n3']]});
// var receiver1 = new RedNode({_flow:flow,id:'n2',type:'abc'});
// var receiver2 = new RedNode({_flow:flow,id:'n3',type:'abc'});
// sender.send({"some": "message"});
// setTimeout(function() {
// try {
// logHandler.messagesSent.should.equal(1);
// should.exist(logHandler.msgIds[0])
// Log.removeHandler(logHandler);
// done();
// } catch(err) {
// Log.removeHandler(logHandler);
// done(err);
// }
// },50)
// })
});