mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Merge pull request #3168 from node-red/improve-unit-test-coverage
Improve unit test coverage
This commit is contained in:
		| @@ -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" : ""; | ||||
|   | ||||
| @@ -150,4 +150,4 @@ module.exports = { | ||||
|     getPluginConfigs, | ||||
|     getPluginList, | ||||
|     exportPluginSettings | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -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) { | ||||
|                         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() { | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user