Improve unit test coverage

This commit is contained in:
Nick O'Leary 2021-10-04 17:53:14 +01:00
parent 1419729458
commit 012e1cbcc5
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
9 changed files with 380 additions and 21 deletions

View File

@ -18,14 +18,6 @@ var apiUtils = require("../util");
var express = require("express");
var runtimeAPI;
function getUsername(userObj) {
var username = '__default';
if ( userObj && userObj.name ) {
username = userObj.name;
}
return username;
}
module.exports = {
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;

View File

@ -34,8 +34,8 @@ var defaultContext = {
image: "red/images/node-red.svg"
},
asset: {
red: (process.env.NODE_ENV == "development")? "red/red.js":"red/red.min.js",
main: (process.env.NODE_ENV == "development")? "red/main.js":"red/main.min.js",
red: "red/red.min.js",
main: "red/main.min.js",
vendorMonaco: ""
}
};
@ -94,6 +94,10 @@ module.exports = {
init: function(settings, _runtimeAPI) {
runtimeAPI = _runtimeAPI;
themeContext = clone(defaultContext);
if (process.env.NODE_ENV == "development") {
themeContext.asset.red = "red/red.js";
themeContext.asset.main = "red/main.js";
}
themeSettings = null;
theme = settings.editorTheme || {};
themeContext.asset.vendorMonaco = ((theme.codeEditor || {}).lib === "monaco") ? "vendor/monaco/monaco-bootstrap.js" : "";

View File

@ -150,4 +150,4 @@ module.exports = {
getPluginConfigs,
getPluginList,
exportPluginSettings
}
}

View File

@ -28,7 +28,7 @@ var NR_TEST_UTILS = require("nr-test-utils");
var comms = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/comms");
var Users = NR_TEST_UTILS.require("@node-red/editor-api/lib/auth/users");
var Tokens = NR_TEST_UTILS.require("@node-red/editor-api/lib/auth/tokens");
var Strategies = NR_TEST_UTILS.require("@node-red/editor-api/lib/auth/strategies");
var address = '127.0.0.1';
var listenPort = 0; // use ephemeral port
@ -113,7 +113,6 @@ describe("api/editor/comms", function() {
connections[0].send('topic3', 'correct');
});
ws.on('message', function(msg) {
console.log(msg);
msg.should.equal('[{"topic":"topic3","data":"correct"}]');
ws.close();
done();
@ -343,6 +342,11 @@ describe("api/editor/comms", function() {
var getUser;
var getToken;
var getUserToken;
var getUserTokenHeader;
var authenticateUserToken;
var onSessionExpiry;
var onSessionExpiryCallback;
before(function(done) {
getDefaultUser = sinon.stub(Users,"default").callsFake(function() { return Promise.resolve(null);});
getUser = sinon.stub(Users,"get").callsFake(function(username) {
@ -368,8 +372,19 @@ describe("api/editor/comms", function() {
return Promise.resolve(null);
}
});
getUserTokenHeader = sinon.stub(Users,"tokenHeader").callsFake(function() {
return "custom-header"
})
authenticateUserToken = sinon.stub(Strategies, "authenticateUserToken").callsFake(async function(req) {
var token = req.headers['custom-header'];
if (token === "knock-knock") {
return {user:"fred",scope:["*"]}
}
throw new Error("Invalid user");
})
onSessionExpiry = sinon.stub(Tokens,"onSessionExpiry").callsFake(function(cb) {
onSessionExpiryCallback = cb;
});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {adminAuth:{}}, {comms: mockComms});
server.listen(listenPort, address);
@ -385,6 +400,9 @@ describe("api/editor/comms", function() {
getUser.restore();
getToken.restore();
getUserToken.restore();
getUserTokenHeader.restore();
authenticateUserToken.restore();
onSessionExpiry.restore();
comms.stop();
server.stop(done);
});
@ -428,6 +446,32 @@ describe("api/editor/comms", function() {
}
});
});
it('allows connections that do authenticate - header-provided-token',function(done) {
var ws = new WebSocket(url,{
headers: { "custom-header": "knock-knock" }
});
var received = 0;
ws.on('open', function() {
ws.send('{"subscribe":"foo"}');
connections.should.have.length(1);
connections[0].send('foo', 'correct');
});
ws.on('message', function(msg) {
received++;
if (received == 1) {
msg.should.equal('[{"topic":"foo","data":"correct"}]');
ws.close();
}
});
ws.on('close', function() {
try {
received.should.equal(1);
done();
} catch(err) {
done(err);
}
});
});
it('allows connections that do authenticate - user-provided-token',function(done) {
var ws = new WebSocket(url);
var received = 0;
@ -475,6 +519,50 @@ describe("api/editor/comms", function() {
done();
});
});
it('rejects connections for invalid token - header-provided-token',function(done) {
var ws = new WebSocket(url,{
headers: { "custom-header": "bad token" }
});
var received = 0;
ws.on('open', function() {
ws.send('{"subscribe":"foo"}');
});
ws.on('error', function() {
done();
})
});
it("expires websocket sessions", function(done) {
var ws = new WebSocket(url);
var received = 0;
ws.on('open', function() {
ws.send('{"auth":"1234"}');
});
ws.on('message', function(msg) {
received++;
if (received == 3) {
msg.should.equal('{"auth":"fail"}');
} else if (received == 1) {
msg.should.equal('{"auth":"ok"}');
ws.send('{"subscribe":"foo"}');
connections[0].send('foo', 'correct');
} else {
msg.should.equal('[{"topic":"foo","data":"correct"}]');
setTimeout(function() {
onSessionExpiryCallback({accessToken:"1234"})
},50);
}
});
ws.on('close', function() {
try {
received.should.equal(3);
done();
} catch(err) {
done(err);
}
});
})
});
describe('authentication required, anonymous enabled',function() {

View File

@ -15,6 +15,7 @@
**/
var should = require("should");
var request = require("supertest");
var express = require('express');
var sinon = require('sinon');
var fs = require("fs");
@ -50,10 +51,36 @@ describe("api/editor/theme", function () {
context.should.have.a.property("asset");
context.asset.should.have.a.property("red", "red/red.min.js");
context.asset.should.have.a.property("main", "red/main.min.js");
context.asset.should.have.a.property("vendorMonaco", "");
should.not.exist(theme.settings());
});
it("uses non-minified js files when in dev mode", async function () {
const previousEnv = process.env.NODE_ENV;
try {
process.env.NODE_ENV = 'development'
theme.init({});
var context = await theme.context();
context.asset.should.have.a.property("red", "red/red.js");
context.asset.should.have.a.property("main", "red/main.js");
} finally {
process.env.NODE_ENV = previousEnv;
}
});
it("Adds monaco bootstrap when enabled", async function () {
theme.init({
editorTheme: {
codeEditor: {
lib: 'monaco'
}
}
});
var context = await theme.context();
context.asset.should.have.a.property("vendorMonaco", "vendor/monaco/monaco-bootstrap.js");
});
it("picks up custom theme", async function () {
theme.init({
editorTheme: {
@ -64,7 +91,9 @@ describe("api/editor/theme", function () {
icon: "/absolute/path/to/theme/tabicon",
colour: "#8f008f"
},
css: "/absolute/path/to/custom/css/file.css",
css: [
"/absolute/path/to/custom/css/file.css"
],
scripts: "/absolute/path/to/script.js"
},
header: {
@ -185,4 +214,62 @@ describe("api/editor/theme", function () {
});
it("includes list of plugin themes", function(done) {
theme.init({},{
plugins: { getPluginsByType: _ => [{id:"theme-plugin"}] }
});
const app = theme.app();
request(app)
.get("/")
.end(function(err,res) {
if (err) {
return done(err);
}
try {
const response = JSON.parse(res.text);
response.should.have.property("themes");
response.themes.should.eql(["theme-plugin"])
done();
} catch(err) {
done(err);
}
});
});
it("includes theme plugin settings", async function () {
theme.init({
editorTheme: {
theme: 'test-theme'
}
},{
plugins: { getPlugin: t => {
return ({'test-theme':{
path: '/abosolute/path/to/plugin',
css: [
"path/to/custom/css/file1.css",
"/invalid/path/to/file2.css",
"../another/invalid/path/file3.css"
],
scripts: [
"path/to/custom/js/file1.js",
"/invalid/path/to/file2.js",
"../another/invalid/path/file3.js"
]
}})[t.id];
} }
});
theme.app();
var context = await theme.context();
context.should.have.a.property("page");
context.page.should.have.a.property("css");
context.page.css.should.have.lengthOf(1);
context.page.css[0].should.eql('theme/css/file1.css');
context.page.should.have.a.property("scripts");
context.page.scripts.should.have.lengthOf(1);
context.page.scripts[0].should.eql('theme/scripts/file1.js');
});
});

View File

@ -33,10 +33,21 @@ describe("api/editor/ui", function() {
nodes: {
getIcon: function(opts) {
return new Promise(function(resolve,reject) {
fs.readFile(NR_TEST_UTILS.resolve("@node-red/editor-client/src/images/icons/arrow-in.svg"), function(err,data) {
resolve(data);
})
if (opts.icon === "icon.png") {
fs.readFile(NR_TEST_UTILS.resolve("@node-red/editor-client/src/images/icons/arrow-in.svg"), function(err,data) {
resolve(data);
})
} else {
resolve(null);
}
});
},
getModuleResource: async function(opts) {
if (opts.module !== "test-module" || opts.path !== "a/path/text.txt") {
return null;
} else {
return "Some text data";
}
}
}
});
@ -110,6 +121,53 @@ describe("api/editor/ui", function() {
});
});
it('returns the default icon for invalid paths', function(done) {
var defaultIcon = fs.readFileSync(NR_TEST_UTILS.resolve("@node-red/editor-client/src/images/icons/arrow-in.svg"));
request(app)
.get("/icons/module/unreal.png")
.expect("Content-Type", /image\/svg/)
.expect(200)
.parse(binaryParser)
.end(function(err,res) {
if (err){
return done(err);
}
Buffer.isBuffer(res.body).should.be.true();
compareBuffers(res.body,defaultIcon);
done();
});
});
});
describe("module resource handler", function() {
before(function() {
app = express();
app.get(/^\/resources\/((?:@[^\/]+\/)?[^\/]+)\/(.+)$/,ui.moduleResource);
});
it('returns the requested resource', function(done) {
request(app)
.get("/resources/test-module/a/path/text.txt")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.text.should.eql('Some text data');
done();
});
});
it('404s invalid paths', function(done) {
request(app)
.get("/resources/test-module/../a/path/text.txt")
.expect(404)
.end(function(err,res) {
if (err) {
return done(err);
}
done();
});
});
});
describe("editor ui handler", function() {

View File

@ -150,6 +150,51 @@ test-module-config`)
))
})
})
describe("exportPluginSettings", function() {
it("exports plugin settings - default false", function() {
plugins.init({ "a-plugin": { a: 123, b:234, c: 345} });
plugins.registerPlugin("test-module/test-set","a-plugin",{
settings: {
a: { exportable: true },
b: {exportable: false },
d: { exportable: true, value: 456}
}
});
var exportedSet = {};
plugins.exportPluginSettings(exportedSet);
exportedSet.should.have.property("a-plugin");
// a is exportable
exportedSet["a-plugin"].should.have.property("a",123);
// b is explicitly not exportable
exportedSet["a-plugin"].should.not.have.property("b");
// c isn't listed and default false
exportedSet["a-plugin"].should.not.have.property("c");
// d has a default value
exportedSet["a-plugin"].should.have.property("d",456);
})
it("exports plugin settings - default true", function() {
plugins.init({ "a-plugin": { a: 123, b:234, c: 345} });
plugins.registerPlugin("test-module/test-set","a-plugin",{
settings: {
'*': { exportable: true },
a: { exportable: true },
b: {exportable: false },
d: { exportable: true, value: 456}
});
}
});
var exportedSet = {};
plugins.exportPluginSettings(exportedSet);
exportedSet.should.have.property("a-plugin");
// a is exportable
exportedSet["a-plugin"].should.have.property("a",123);
// b is explicitly not exportable
exportedSet["a-plugin"].should.not.have.property("b");
// c isn't listed, but default true
exportedSet["a-plugin"].should.have.property("c");
// d has a default value
exportedSet["a-plugin"].should.have.property("d",456);
})
});
});

View File

@ -574,4 +574,41 @@ describe("red/nodes/registry/registry",function() {
});
});
describe('#getModuleResource', function() {
beforeEach(function() {
typeRegistry.init(settings,{});
typeRegistry.addModule({
name: "test-module",version:"0.0.1",nodes: {
"test-name":{
id: "test-module/test-name",
module: "test-module",
name: "test-name",
enabled: true,
loaded: false,
config: "configA",
types: [ "test-a","test-b"],
file: "abc"
}
},
resources: {
path: path.join(__dirname, "resources","examples")
}
});
});
it('Returns valid resource path', function() {
const result = typeRegistry.getModuleResource("test-module","one.json");
should.exist(result);
result.should.eql(path.join(__dirname, "resources","examples","one.json"))
});
it('Returns null for path that tries to break out', function() {
// Note - this path exists, but we don't allow .. in the resolved path to
// avoid breaking out of the resources dir
const result = typeRegistry.getModuleResource("test-module","../../index_spec.js");
should.not.exist(result);
});
it('Returns null for path that does not exist', function() {
const result = typeRegistry.getModuleResource("test-module","two.json");
should.not.exist(result);
});
});
});

View File

@ -15,13 +15,61 @@
**/
const should = require("should");
const sinon = require("sinon");
const NR_TEST_UTILS = require("nr-test-utils");
const registryUtil = NR_TEST_UTILS.require("@node-red/registry/lib/util");
// Get the internal runtime api
const runtime = NR_TEST_UTILS.require("@node-red/runtime")._;
const i18n = NR_TEST_UTILS.require("@node-red/util").i18n;
describe("red/nodes/registry/util",function() {
describe("createNodeApi", function() {
it.skip("needs tests");
let i18n_;
let registerType;
let registerSubflow;
before(function() {
i18n_ = sinon.stub(i18n,"_").callsFake(function() {
return Array.prototype.slice.call(arguments,0);
})
registerType = sinon.stub(runtime.nodes,"registerType");
registerSubflow = sinon.stub(runtime.nodes,"registerSubflow");
});
after(function() {
i18n_.restore();
registerType.restore();
registerSubflow.restore();
})
it("builds node-specific view of runtime api", function() {
registryUtil.init(runtime);
var result = registryUtil.createNodeApi({id: "my-node", namespace: "my-namespace"})
// Need a better strategy here.
// For now, validate the node-custom functions
var message = result._("message");
// This should prepend the node's namespace to the message
message.should.eql([ 'my-namespace:message' ]);
var nodeConstructor = () => {};
var nodeOpts = {};
result.nodes.registerType("type",nodeConstructor, nodeOpts);
registerType.called.should.be.true();
registerType.lastCall.args[0].should.eql("my-node")
registerType.lastCall.args[1].should.eql("type")
registerType.lastCall.args[2].should.eql(nodeConstructor)
registerType.lastCall.args[3].should.eql(nodeOpts)
var subflowDef = {};
result.nodes.registerSubflow(subflowDef);
registerSubflow.called.should.be.true();
registerSubflow.lastCall.args[0].should.eql("my-node")
registerSubflow.lastCall.args[1].should.eql(subflowDef)
});
});
describe("checkModuleAllowed", function() {
function checkList(module, version, allowList, denyList) {