mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
6fb96fa3c1
The exec and events components are common components that are used by both runtime and registry. It makes sense to move them into the util package. This also adds some docs to the registry module
616 lines
23 KiB
JavaScript
616 lines
23 KiB
JavaScript
/**
|
|
* Copyright JS Foundation and other contributors, http://js.foundation
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
**/
|
|
|
|
var should = require("should");
|
|
var sinon = require("sinon");
|
|
var clone = require("clone");
|
|
var NR_TEST_UTILS = require("nr-test-utils");
|
|
|
|
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
|
|
var RedNode = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
|
|
var RED = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes");
|
|
var events = NR_TEST_UTILS.require("@node-red/util/lib/events");
|
|
var credentials = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/credentials");
|
|
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry")
|
|
var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Flow");
|
|
|
|
describe('flows/index', function() {
|
|
|
|
var storage;
|
|
var eventsOn;
|
|
var credentialsClean;
|
|
var credentialsLoad;
|
|
|
|
var flowCreate;
|
|
var getType;
|
|
|
|
var mockLog = {
|
|
log: sinon.stub(),
|
|
debug: sinon.stub(),
|
|
trace: sinon.stub(),
|
|
warn: sinon.stub(),
|
|
info: sinon.stub(),
|
|
metric: sinon.stub(),
|
|
_: function() { return "abc"}
|
|
}
|
|
|
|
|
|
before(function() {
|
|
getType = sinon.stub(typeRegistry,"get",function(type) {
|
|
return type.indexOf('missing') === -1;
|
|
});
|
|
});
|
|
after(function() {
|
|
getType.restore();
|
|
});
|
|
|
|
|
|
beforeEach(function() {
|
|
eventsOn = sinon.spy(events,"on");
|
|
credentialsClean = sinon.stub(credentials,"clean",function(conf) {
|
|
conf.forEach(function(n) {
|
|
delete n.credentials;
|
|
});
|
|
return Promise.resolve();
|
|
});
|
|
credentialsLoad = sinon.stub(credentials,"load",function(creds) {
|
|
if (creds && creds.hasOwnProperty("$") && creds['$'] === "fail") {
|
|
return Promise.reject("creds error");
|
|
}
|
|
return Promise.resolve();
|
|
});
|
|
flowCreate = sinon.stub(Flow,"create",function(parent, global, flow) {
|
|
var id;
|
|
if (typeof flow === 'undefined') {
|
|
flow = global;
|
|
id = '_GLOBAL_';
|
|
} else {
|
|
id = flow.id;
|
|
}
|
|
flowCreate.flows[id] = {
|
|
flow: flow,
|
|
global: global,
|
|
start: sinon.spy(),
|
|
update: sinon.spy(),
|
|
stop: sinon.spy(),
|
|
getActiveNodes: function() {
|
|
return flow.nodes||{};
|
|
},
|
|
handleError: sinon.spy(),
|
|
handleStatus: sinon.spy()
|
|
|
|
}
|
|
return flowCreate.flows[id];
|
|
});
|
|
flowCreate.flows = {};
|
|
|
|
storage = {
|
|
saveFlows: function(conf) {
|
|
storage.conf = conf;
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
});
|
|
|
|
afterEach(function(done) {
|
|
eventsOn.restore();
|
|
credentialsClean.restore();
|
|
credentialsLoad.restore();
|
|
flowCreate.restore();
|
|
|
|
flows.stopFlows().then(done);
|
|
|
|
});
|
|
// describe('#init',function() {
|
|
// it('registers the type-registered handler', function() {
|
|
// flows.init({},{});
|
|
// eventsOn.calledOnce.should.be.true();
|
|
// });
|
|
// });
|
|
|
|
describe('#setFlows', function() {
|
|
it('sets the full flow', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.setFlows(originalConfig).then(function() {
|
|
credentialsClean.called.should.be.true();
|
|
storage.hasOwnProperty('conf').should.be.true();
|
|
flows.getFlows().flows.should.eql(originalConfig);
|
|
done();
|
|
});
|
|
|
|
});
|
|
it('loads the full flow for type load', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
var loadStorage = {
|
|
saveFlows: function(conf) {
|
|
loadStorage.conf = conf;
|
|
return Promise.resolve(456);
|
|
},
|
|
getFlows: function() {
|
|
return Promise.resolve({flows:originalConfig,rev:123})
|
|
}
|
|
}
|
|
flows.init({log:mockLog, settings:{},storage:loadStorage});
|
|
flows.setFlows(originalConfig,"load").then(function() {
|
|
credentialsClean.called.should.be.false();
|
|
// 'load' type does not trigger a save
|
|
loadStorage.hasOwnProperty('conf').should.be.false();
|
|
flows.getFlows().flows.should.eql(originalConfig);
|
|
done();
|
|
});
|
|
|
|
});
|
|
|
|
it('extracts credentials from the full flow', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[],credentials:{"a":1}},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.setFlows(originalConfig).then(function() {
|
|
credentialsClean.called.should.be.true();
|
|
storage.hasOwnProperty('conf').should.be.true();
|
|
var cleanedFlows = flows.getFlows();
|
|
storage.conf.flows.should.eql(cleanedFlows.flows);
|
|
cleanedFlows.flows.should.not.eql(originalConfig);
|
|
cleanedFlows.flows[0].credentials = {"a":1};
|
|
cleanedFlows.flows.should.eql(originalConfig);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('sets the full flow including credentials', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
var credentials = {"t1-1":{"a":1}};
|
|
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.setFlows(originalConfig,credentials).then(function() {
|
|
credentialsClean.called.should.be.false();
|
|
credentialsLoad.called.should.be.true();
|
|
credentialsLoad.lastCall.args[0].should.eql(credentials);
|
|
flows.getFlows().flows.should.eql(originalConfig);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('updates existing flows with partial deployment - nodes', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
var newConfig = clone(originalConfig);
|
|
newConfig.push({id:"t1-2",x:10,y:10,z:"t1",type:"test",wires:[]});
|
|
newConfig.push({id:"t2",type:"tab"});
|
|
newConfig.push({id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]});
|
|
storage.getFlows = function() {
|
|
return Promise.resolve({flows:originalConfig});
|
|
}
|
|
events.once('flows:started',function() {
|
|
flows.setFlows(newConfig,"nodes").then(function() {
|
|
flows.getFlows().flows.should.eql(newConfig);
|
|
flowCreate.flows['t1'].update.called.should.be.true();
|
|
flowCreate.flows['t2'].start.called.should.be.true();
|
|
flowCreate.flows['_GLOBAL_'].update.called.should.be.true();
|
|
done();
|
|
})
|
|
});
|
|
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.load().then(function() {
|
|
flows.startFlows();
|
|
});
|
|
});
|
|
|
|
it('updates existing flows with partial deployment - flows', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
var newConfig = clone(originalConfig);
|
|
newConfig.push({id:"t1-2",x:10,y:10,z:"t1",type:"test",wires:[]});
|
|
newConfig.push({id:"t2",type:"tab"});
|
|
newConfig.push({id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]});
|
|
storage.getFlows = function() {
|
|
return Promise.resolve({flows:originalConfig});
|
|
}
|
|
|
|
events.once('flows:started',function() {
|
|
flows.setFlows(newConfig,"nodes").then(function() {
|
|
flows.getFlows().flows.should.eql(newConfig);
|
|
flowCreate.flows['t1'].update.called.should.be.true();
|
|
flowCreate.flows['t2'].start.called.should.be.true();
|
|
flowCreate.flows['_GLOBAL_'].update.called.should.be.true();
|
|
flows.stopFlows().then(done);
|
|
})
|
|
});
|
|
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.load().then(function() {
|
|
flows.startFlows();
|
|
});
|
|
});
|
|
|
|
it('returns error if it cannot decrypt credentials', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
var credentials = {"$":"fail"};
|
|
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.setFlows(originalConfig,credentials).then(function() {
|
|
done("Unexpected success when credentials couldn't be decrypted")
|
|
}).catch(function(err) {
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#load', function() {
|
|
it('loads the flow config', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
storage.getFlows = function() {
|
|
return Promise.resolve({flows:originalConfig});
|
|
}
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.load().then(function() {
|
|
credentialsLoad.called.should.be.true();
|
|
// 'load' type does not trigger a save
|
|
storage.hasOwnProperty('conf').should.be.false();
|
|
flows.getFlows().flows.should.eql(originalConfig);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#startFlows', function() {
|
|
it('starts the loaded config', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
storage.getFlows = function() {
|
|
return Promise.resolve({flows:originalConfig});
|
|
}
|
|
|
|
events.once('flows:started',function() {
|
|
Object.keys(flowCreate.flows).should.eql(['_GLOBAL_','t1']);
|
|
done();
|
|
});
|
|
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.load().then(function() {
|
|
flows.startFlows();
|
|
});
|
|
});
|
|
it('does not start if nodes missing', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1-2",x:10,y:10,z:"t1",type:"missing",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
storage.getFlows = function() {
|
|
return Promise.resolve({flows:originalConfig});
|
|
}
|
|
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.load().then(function() {
|
|
flows.startFlows();
|
|
flowCreate.called.should.be.false();
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('starts when missing nodes registered', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1-2",x:10,y:10,z:"t1",type:"missing",wires:[]},
|
|
{id:"t1-3",x:10,y:10,z:"t1",type:"missing2",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
storage.getFlows = function() {
|
|
return Promise.resolve({flows:originalConfig});
|
|
}
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.load().then(function() {
|
|
flows.startFlows();
|
|
flowCreate.called.should.be.false();
|
|
|
|
events.emit("type-registered","missing");
|
|
setTimeout(function() {
|
|
flowCreate.called.should.be.false();
|
|
events.emit("type-registered","missing2");
|
|
setTimeout(function() {
|
|
flowCreate.called.should.be.true();
|
|
done();
|
|
},10);
|
|
},10);
|
|
});
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
describe.skip('#get',function() {
|
|
|
|
});
|
|
|
|
describe('#eachNode', function() {
|
|
it('iterates the flow nodes', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
storage.getFlows = function() {
|
|
return Promise.resolve({flows:originalConfig});
|
|
}
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.load().then(function() {
|
|
var c = 0;
|
|
flows.eachNode(function(node) {
|
|
c++
|
|
})
|
|
c.should.equal(2);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#stopFlows', function() {
|
|
|
|
});
|
|
// describe('#handleError', function() {
|
|
// it('passes error to correct flow', function(done) {
|
|
// var originalConfig = [
|
|
// {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
// {id:"t1",type:"tab"}
|
|
// ];
|
|
// storage.getFlows = function() {
|
|
// return Promise.resolve({flows:originalConfig});
|
|
// }
|
|
//
|
|
// events.once('flows:started',function() {
|
|
// flows.handleError(originalConfig[0],"message",{});
|
|
// flowCreate.flows['t1'].handleError.called.should.be.true();
|
|
// done();
|
|
// });
|
|
//
|
|
// flows.init({log:mockLog, settings:{},storage:storage});
|
|
// flows.load().then(function() {
|
|
// flows.startFlows();
|
|
// });
|
|
// });
|
|
// it('passes error to flows that use the originating global config', function(done) {
|
|
// var originalConfig = [
|
|
// {id:"configNode",type:"test"},
|
|
// {id:"t1",type:"tab"},
|
|
// {id:"t1-1",x:10,y:10,z:"t1",type:"test",config:"configNode",wires:[]},
|
|
// {id:"t2",type:"tab"},
|
|
// {id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]},
|
|
// {id:"t3",type:"tab"},
|
|
// {id:"t3-1",x:10,y:10,z:"t3",type:"test",config:"configNode",wires:[]}
|
|
// ];
|
|
// storage.getFlows = function() {
|
|
// return Promise.resolve({flows:originalConfig});
|
|
// }
|
|
//
|
|
// events.once('flows:started',function() {
|
|
// flows.handleError(originalConfig[0],"message",{});
|
|
// try {
|
|
// flowCreate.flows['t1'].handleError.called.should.be.true();
|
|
// flowCreate.flows['t2'].handleError.called.should.be.false();
|
|
// flowCreate.flows['t3'].handleError.called.should.be.true();
|
|
// done();
|
|
// } catch(err) {
|
|
// done(err);
|
|
// }
|
|
// });
|
|
//
|
|
// flows.init({log:mockLog, settings:{},storage:storage});
|
|
// flows.load().then(function() {
|
|
// flows.startFlows();
|
|
// });
|
|
// });
|
|
// });
|
|
// describe('#handleStatus', function() {
|
|
// it('passes status to correct flow', function(done) {
|
|
// var originalConfig = [
|
|
// {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
// {id:"t1",type:"tab"}
|
|
// ];
|
|
// storage.getFlows = function() {
|
|
// return Promise.resolve({flows:originalConfig});
|
|
// }
|
|
//
|
|
// events.once('flows:started',function() {
|
|
// flows.handleStatus(originalConfig[0],"message");
|
|
// flowCreate.flows['t1'].handleStatus.called.should.be.true();
|
|
// done();
|
|
// });
|
|
//
|
|
// flows.init({log:mockLog, settings:{},storage:storage});
|
|
// flows.load().then(function() {
|
|
// flows.startFlows();
|
|
// });
|
|
// });
|
|
//
|
|
// it('passes status to flows that use the originating global config', function(done) {
|
|
// var originalConfig = [
|
|
// {id:"configNode",type:"test"},
|
|
// {id:"t1",type:"tab"},
|
|
// {id:"t1-1",x:10,y:10,z:"t1",type:"test",config:"configNode",wires:[]},
|
|
// {id:"t2",type:"tab"},
|
|
// {id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]},
|
|
// {id:"t3",type:"tab"},
|
|
// {id:"t3-1",x:10,y:10,z:"t3",type:"test",config:"configNode",wires:[]}
|
|
// ];
|
|
// storage.getFlows = function() {
|
|
// return Promise.resolve({flows:originalConfig});
|
|
// }
|
|
//
|
|
// events.once('flows:started',function() {
|
|
// flows.handleStatus(originalConfig[0],"message");
|
|
// try {
|
|
// flowCreate.flows['t1'].handleStatus.called.should.be.true();
|
|
// flowCreate.flows['t2'].handleStatus.called.should.be.false();
|
|
// flowCreate.flows['t3'].handleStatus.called.should.be.true();
|
|
// done();
|
|
// } catch(err) {
|
|
// done(err);
|
|
// }
|
|
// });
|
|
//
|
|
// flows.init({log:mockLog, settings:{},storage:storage});
|
|
// flows.load().then(function() {
|
|
// flows.startFlows();
|
|
// });
|
|
// });
|
|
// });
|
|
|
|
describe('#checkTypeInUse', function() {
|
|
|
|
before(function() {
|
|
sinon.stub(typeRegistry,"getNodeInfo",function(id) {
|
|
if (id === 'unused-module') {
|
|
return {types:['one','two','three']}
|
|
} else {
|
|
return {types:['one','test','three']}
|
|
}
|
|
});
|
|
});
|
|
|
|
after(function() {
|
|
typeRegistry.getNodeInfo.restore();
|
|
});
|
|
|
|
it('returns cleanly if type not is use', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.setFlows(originalConfig).then(function() {
|
|
flows.checkTypeInUse("unused-module");
|
|
done();
|
|
});
|
|
});
|
|
it('throws error if type is in use', function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.setFlows(originalConfig).then(function() {
|
|
/*jshint immed: false */
|
|
try {
|
|
flows.checkTypeInUse("used-module");
|
|
done("type_in_use error not thrown");
|
|
} catch(err) {
|
|
err.code.should.eql("type_in_use");
|
|
done();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#addFlow', function() {
|
|
it("rejects duplicate node id",function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
storage.getFlows = function() {
|
|
return Promise.resolve({flows:originalConfig});
|
|
}
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.load().then(function() {
|
|
flows.addFlow({
|
|
label:'new flow',
|
|
nodes:[
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}
|
|
]
|
|
}).then(function() {
|
|
done(new Error('failed to reject duplicate node id'));
|
|
}).catch(function(err) {
|
|
done();
|
|
})
|
|
});
|
|
|
|
});
|
|
|
|
it("addFlow",function(done) {
|
|
var originalConfig = [
|
|
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t1",type:"tab"}
|
|
];
|
|
storage.getFlows = function() {
|
|
return Promise.resolve({flows:originalConfig});
|
|
}
|
|
storage.setFlows = function() {
|
|
return Promise.resolve();
|
|
}
|
|
flows.init({log:mockLog, settings:{},storage:storage});
|
|
flows.load().then(function() {
|
|
return flows.startFlows();
|
|
}).then(function() {
|
|
flows.addFlow({
|
|
label:'new flow',
|
|
nodes:[
|
|
{id:"t2-1",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t2-2",x:10,y:10,z:"t1",type:"test",wires:[]},
|
|
{id:"t2-3",z:"t1",type:"test"}
|
|
]
|
|
}).then(function(id) {
|
|
flows.getFlows().flows.should.have.lengthOf(6);
|
|
var createdFlows = Object.keys(flowCreate.flows);
|
|
createdFlows.should.have.lengthOf(3);
|
|
createdFlows[2].should.eql(id);
|
|
done();
|
|
}).catch(function(err) {
|
|
done(err);
|
|
})
|
|
});
|
|
|
|
});
|
|
})
|
|
describe('#updateFlow', function() {
|
|
it.skip("updateFlow");
|
|
})
|
|
describe('#removeFlow', function() {
|
|
it.skip("removeFlow");
|
|
})
|
|
describe('#disableFlow', function() {
|
|
it.skip("disableFlow");
|
|
})
|
|
describe('#enableFlow', function() {
|
|
it.skip("enableFlow");
|
|
})
|
|
});
|