Merge pull request #2873 from node-red/function-modules

Function node external modules
This commit is contained in:
Nick O'Leary
2021-03-01 21:35:31 +00:00
committed by GitHub
28 changed files with 1323 additions and 223 deletions

View File

@@ -0,0 +1,302 @@
// init: init,
// register: register,
// registerSubflow: registerSubflow,
// checkFlowDependencies: checkFlowDependencies,
// require: requireModule
//
const should = require("should");
const sinon = require("sinon");
const fs = require("fs-extra");
const path = require("path");
const os = require("os");
const NR_TEST_UTILS = require("nr-test-utils");
const externalModules = NR_TEST_UTILS.require("@node-red/registry/lib/externalModules");
const exec = NR_TEST_UTILS.require("@node-red/util/lib/exec");
let homeDir;
async function createUserDir() {
if (!homeDir) {
homeDir = path.join(os.tmpdir(),"nr-test-"+Math.floor(Math.random()*100000));
}
await fs.ensureDir(homeDir);
}
async function setupExternalModulesPackage(dependencies) {
await fs.ensureDir(path.join(homeDir,"externalModules"))
await fs.writeFile(path.join(homeDir,"externalModules","package.json"),`{
"name": "Node-RED-External-Modules",
"description": "These modules are automatically installed by Node-RED to use in Function nodes.",
"version": "1.0.0",
"private": true,
"dependencies": ${JSON.stringify(dependencies)}
}`)
}
describe("externalModules api", function() {
beforeEach(async function() {
await createUserDir()
})
afterEach(async function() {
await fs.remove(homeDir);
})
describe("checkFlowDependencies", function() {
beforeEach(function() {
sinon.stub(exec,"run", async function(cmd, args, options) {
let error;
if (args[1] === "moduleNotFound") {
error = new Error();
error.stderr = "E404";
} else if (args[1] === "moduleVersionNotFound") {
error = new Error();
error.stderr = "ETARGET";
} else if (args[1] === "moduleFail") {
error = new Error();
error.stderr = "Some unexpected install error";
}
if (error) {
throw error;
}
})
})
afterEach(function() {
exec.run.restore();
})
it("does nothing when no types are registered",async function() {
externalModules.init({userDir: homeDir});
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "foo"}]}
])
exec.run.called.should.be.false();
});
it("skips install for modules already installed", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
await setupExternalModulesPackage({"foo": "1.2.3", "bar":"2.3.4"});
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "foo"}]}
])
exec.run.called.should.be.false();
})
it("skips install for built-in modules", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "fs"}]}
])
exec.run.called.should.be.false();
})
it("installs missing modules", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
fs.existsSync(path.join(homeDir,"externalModules")).should.be.false();
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "foo"}]}
])
exec.run.called.should.be.true();
fs.existsSync(path.join(homeDir,"externalModules")).should.be.true();
})
it("installs missing modules from inside subflow module", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
externalModules.registerSubflow("sf", {"flow":[{type: "function", libs:[{module: "foo"}]}]});
await externalModules.checkFlowDependencies([
{type: "sf"}
])
exec.run.called.should.be.true();
})
it("reports install fail - 404", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "moduleNotFound"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
exec.run.called.should.be.true();
Array.isArray(err).should.be.true();
err.should.have.length(1);
err[0].should.have.property("module");
err[0].module.should.have.property("module","moduleNotFound");
err[0].should.have.property("error");
err[0].error.should.have.property("code",404);
}
})
it("reports install fail - target", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "moduleVersionNotFound"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
exec.run.called.should.be.true();
Array.isArray(err).should.be.true();
err.should.have.length(1);
err[0].should.have.property("module");
err[0].module.should.have.property("module","moduleVersionNotFound");
err[0].should.have.property("error");
err[0].error.should.have.property("code",404);
}
})
it("reports install fail - unexpected", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "moduleFail"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
exec.run.called.should.be.true();
Array.isArray(err).should.be.true();
err.should.have.length(1);
err[0].should.have.property("module");
err[0].module.should.have.property("module","moduleFail");
err[0].should.have.property("error");
err[0].error.should.have.property("code","unexpected_error");
}
})
it("reports install fail - multiple", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "moduleNotFound"},{module: "moduleFail"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
exec.run.called.should.be.true();
Array.isArray(err).should.be.true();
err.should.have.length(2);
// Sort the array so we know the order to test for
err.sort(function(A,B) {
return A.module.module.localeCompare(B.module.module);
})
err[1].should.have.property("module");
err[1].module.should.have.property("module","moduleNotFound");
err[1].should.have.property("error");
err[1].error.should.have.property("code",404);
err[0].should.have.property("module");
err[0].module.should.have.property("module","moduleFail");
err[0].should.have.property("error");
err[0].error.should.have.property("code","unexpected_error");
}
})
it("reports install fail - install disabled", async function() {
externalModules.init({userDir: homeDir, externalModules: {
modules: {
allowInstall: false
}
}});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "foo"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
// Should not try to install
exec.run.called.should.be.false();
Array.isArray(err).should.be.true();
err.should.have.length(1);
err[0].should.have.property("module");
err[0].module.should.have.property("module","foo");
err[0].should.have.property("error");
err[0].error.should.have.property("code","install_not_allowed");
}
})
it("reports install fail - module disallowed", async function() {
externalModules.init({userDir: homeDir, externalModules: {
modules: {
denyList: ['foo']
}
}});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
// foo disallowed
// bar allowed
{type: "function", libs:[{module: "foo"},{module: "bar"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
exec.run.calledOnce.should.be.true();
Array.isArray(err).should.be.true();
err.should.have.length(1);
err[0].should.have.property("module");
err[0].module.should.have.property("module","foo");
err[0].should.have.property("error");
err[0].error.should.have.property("code","install_not_allowed");
}
})
it("reports install fail - built-in module disallowed", async function() {
externalModules.init({userDir: homeDir, externalModules: {
modules: {
denyList: ['fs']
}
}});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
// foo disallowed
// bar allowed
{type: "function", libs:[{module: "fs"},{module: "bar"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
exec.run.calledOnce.should.be.true();
Array.isArray(err).should.be.true();
err.should.have.length(1);
err[0].should.have.property("module");
err[0].module.should.have.property("module","fs");
err[0].should.have.property("error");
err[0].error.should.have.property("code","module_not_allowed");
}
})
})
describe("require", async function() {
it("requires built-in modules", async function() {
externalModules.init({userDir: homeDir});
const result = externalModules.require("fs")
result.should.eql(require("fs"));
})
it("rejects unknown modules", async function() {
externalModules.init({userDir: homeDir});
try {
externalModules.require("foo")
throw new Error("require did not reject after fail")
} catch(err) {
err.should.have.property("code","module_not_allowed");
}
})
it("rejects disallowed modules", async function() {
externalModules.init({userDir: homeDir, externalModules: {
modules: {
denyList: ['fs']
}
}});
try {
externalModules.require("fs")
throw new Error("require did not reject after fail")
} catch(err) {
err.should.have.property("code","module_not_allowed");
}
})
})
});

View File

@@ -40,7 +40,7 @@ describe('red/registry/index', function() {
stubs.push(sinon.stub(loader,"init"));
stubs.push(sinon.stub(typeRegistry,"init"));
registry.init({});
registry.init({settings:{}});
installer.init.called.should.be.true();
loader.init.called.should.be.true();
typeRegistry.init.called.should.be.true();

View File

@@ -36,6 +36,7 @@ describe('flows/index', function() {
var flowCreate;
var getType;
var checkFlowDependencies;
var mockLog = {
log: sinon.stub(),
@@ -52,9 +53,16 @@ describe('flows/index', function() {
getType = sinon.stub(typeRegistry,"get",function(type) {
return type.indexOf('missing') === -1;
});
checkFlowDependencies = sinon.stub(typeRegistry, "checkFlowDependencies", async function(flow) {
if (flow[0].id === "node-with-missing-modules") {
throw new Error("Missing module");
}
});
});
after(function() {
getType.restore();
checkFlowDependencies.restore();
});
@@ -306,7 +314,7 @@ describe('flows/index', function() {
flows.init({log:mockLog, settings:{},storage:storage});
flows.load().then(function() {
flows.startFlows();
return flows.startFlows();
});
});
it('does not start if nodes missing', function(done) {
@@ -321,9 +329,14 @@ describe('flows/index', function() {
flows.init({log:mockLog, settings:{},storage:storage});
flows.load().then(function() {
flows.startFlows();
flowCreate.called.should.be.false();
done();
return flows.startFlows();
}).then(() => {
try {
flowCreate.called.should.be.false();
done();
} catch(err) {
done(err);
}
});
});
@@ -339,9 +352,9 @@ describe('flows/index', function() {
}
flows.init({log:mockLog, settings:{},storage:storage});
flows.load().then(function() {
flows.startFlows();
return flows.startFlows();
}).then(() => {
flowCreate.called.should.be.false();
events.emit("type-registered","missing");
setTimeout(function() {
flowCreate.called.should.be.false();
@@ -354,7 +367,44 @@ describe('flows/index', function() {
});
});
it('does not start if external modules missing', function(done) {
var originalConfig = [
{id:"node-with-missing-modules",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1",type:"tab"}
];
storage.getFlows = function() {
return Promise.resolve({flows:originalConfig});
}
var receivedEvent = null;
var handleEvent = function(payload) {
receivedEvent = payload;
}
events.on("runtime-event",handleEvent);
//{id:"runtime-state",payload:{error:"missing-modules", type:"warning",text:"notification.warnings.missing-modules",modules:missingModules},retain:true});"
flows.init({log:mockLog, settings:{},storage:storage});
flows.load().then(flows.startFlows).then(() => {
events.removeListener("runtime-event",handleEvent);
try {
flowCreate.called.should.be.false();
receivedEvent.should.have.property('id','runtime-state');
receivedEvent.should.have.property('payload',
{ error: 'missing-modules',
type: 'warning',
text: 'notification.warnings.missing-modules',
modules: [] }
);
done();
}catch(err) {
done(err)
}
});
});
});