mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Merge pull request #3168 from node-red/improve-unit-test-coverage
Improve unit test coverage
This commit is contained in:
commit
a88be35292
@ -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;
|
||||
|
@ -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" : "";
|
||||
|
@ -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() {
|
||||
|
@ -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');
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -33,10 +33,21 @@ describe("api/editor/ui", function() {
|
||||
nodes: {
|
||||
getIcon: function(opts) {
|
||||
return new Promise(function(resolve,reject) {
|
||||
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() {
|
||||
|
@ -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);
|
||||
})
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user