diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js b/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js index 3e7b0de87..6d1c62e11 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js @@ -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; diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js index 5a74b62c2..b3aa0be83 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js @@ -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" : ""; diff --git a/packages/node_modules/@node-red/registry/lib/plugins.js b/packages/node_modules/@node-red/registry/lib/plugins.js index 182ad5bb6..4735d1b73 100644 --- a/packages/node_modules/@node-red/registry/lib/plugins.js +++ b/packages/node_modules/@node-red/registry/lib/plugins.js @@ -150,4 +150,4 @@ module.exports = { getPluginConfigs, getPluginList, exportPluginSettings -} \ No newline at end of file +} diff --git a/test/unit/@node-red/editor-api/lib/editor/comms_spec.js b/test/unit/@node-red/editor-api/lib/editor/comms_spec.js index f0f50aba0..4197dc469 100644 --- a/test/unit/@node-red/editor-api/lib/editor/comms_spec.js +++ b/test/unit/@node-red/editor-api/lib/editor/comms_spec.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() { diff --git a/test/unit/@node-red/editor-api/lib/editor/theme_spec.js b/test/unit/@node-red/editor-api/lib/editor/theme_spec.js index 716ee8357..900be126f 100644 --- a/test/unit/@node-red/editor-api/lib/editor/theme_spec.js +++ b/test/unit/@node-red/editor-api/lib/editor/theme_spec.js @@ -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'); + + }); }); diff --git a/test/unit/@node-red/editor-api/lib/editor/ui_spec.js b/test/unit/@node-red/editor-api/lib/editor/ui_spec.js index 2c1312524..0380adcde 100644 --- a/test/unit/@node-red/editor-api/lib/editor/ui_spec.js +++ b/test/unit/@node-red/editor-api/lib/editor/ui_spec.js @@ -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() { diff --git a/test/unit/@node-red/registry/lib/plugins_spec.js b/test/unit/@node-red/registry/lib/plugins_spec.js index 509b89bb1..b1888b3c8 100644 --- a/test/unit/@node-red/registry/lib/plugins_spec.js +++ b/test/unit/@node-red/registry/lib/plugins_spec.js @@ -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} -}); \ No newline at end of file + } + }); + 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); + }) + }); +}); diff --git a/test/unit/@node-red/registry/lib/registry_spec.js b/test/unit/@node-red/registry/lib/registry_spec.js index 60b48938d..5501264a7 100644 --- a/test/unit/@node-red/registry/lib/registry_spec.js +++ b/test/unit/@node-red/registry/lib/registry_spec.js @@ -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); + }); + }); }); diff --git a/test/unit/@node-red/registry/lib/util_spec.js b/test/unit/@node-red/registry/lib/util_spec.js index d2384e5dc..7e94d4fe5 100644 --- a/test/unit/@node-red/registry/lib/util_spec.js +++ b/test/unit/@node-red/registry/lib/util_spec.js @@ -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) {