From e7eb02fcb754faf9536df48c54e2cea84dfbf0c6 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 4 Nov 2014 17:05:29 +0000 Subject: [PATCH] Add unit tests for refactored API modules --- red/api/flows.js | 1 + red/api/index.js | 2 + red/api/library.js | 73 ++-- red/api/nodes.js | 3 +- red/api/ui.js | 2 +- red/server.js | 13 +- red/settings.js | 8 + test/red/api/flows_spec.js | 91 +++++ test/red/api/index_spec.js | 93 +++++ test/red/{ => api}/library_spec.js | 208 ++++++----- test/red/api/nodes_spec.js | 575 +++++++++++++++++++++++++++++ test/red/api/ui_spec.js | 183 +++++++++ test/red/server_spec.js | 221 ++++++++++- test/red/settings_spec.js | 20 + test/red/ui_spec.js | 177 --------- test/resources/icons/test_icon.png | Bin 0 -> 163 bytes 16 files changed, 1352 insertions(+), 318 deletions(-) create mode 100644 test/red/api/flows_spec.js create mode 100644 test/red/api/index_spec.js rename test/red/{ => api}/library_spec.js (57%) create mode 100644 test/red/api/nodes_spec.js create mode 100644 test/red/api/ui_spec.js delete mode 100644 test/red/ui_spec.js create mode 100644 test/resources/icons/test_icon.png diff --git a/red/api/flows.js b/red/api/flows.js index a7ef22311..9be6763b0 100644 --- a/red/api/flows.js +++ b/red/api/flows.js @@ -17,6 +17,7 @@ var express = require('express'); var fs = require("fs"); var events = require("../events"); var path = require("path"); +var util = require("util"); var redNodes = require("../nodes"); var settings = require("../settings"); diff --git a/red/api/index.js b/red/api/index.js index f56f6c40e..ce655bf99 100644 --- a/red/api/index.js +++ b/red/api/index.js @@ -60,6 +60,8 @@ function init(adminApp) { adminApp.get("/library/flows",library.getAll); adminApp.get(new RegExp("/library/flows\/(.*)"),library.get); + + // Error Handler adminApp.use(errorHandler); } diff --git a/red/api/library.js b/red/api/library.js index 9ad0d99ff..d7f792169 100644 --- a/red/api/library.js +++ b/red/api/library.js @@ -20,48 +20,49 @@ var redApp = null; var storage = require("../storage"); function createLibrary(type) { - - redApp.get(new RegExp("/library/"+type+"($|\/(.*))"),function(req,res) { - var path = req.params[1]||""; - storage.getLibraryEntry(type,path).then(function(result) { - if (typeof result === "string") { - res.writeHead(200, {'Content-Type': 'text/plain'}); - res.write(result); - res.end(); - } else { - res.json(result); - } - }).otherwise(function(err) { - if (err) { - util.log("[red] Error loading library entry '"+path+"' : "+err); - if (err.message.indexOf('forbidden') === 0) { - res.send(403); - return; + if (redApp) { + redApp.get(new RegExp("/library/"+type+"($|\/(.*))"),function(req,res) { + var path = req.params[1]||""; + storage.getLibraryEntry(type,path).then(function(result) { + if (typeof result === "string") { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write(result); + res.end(); + } else { + res.json(result); } - } - res.send(404); - }); - }); - - redApp.post(new RegExp("/library/"+type+"\/(.*)"),function(req,res) { - var path = req.params[0]; - var fullBody = ''; - req.on('data', function(chunk) { - fullBody += chunk.toString(); - }); - req.on('end', function() { - storage.saveLibraryEntry(type,path,req.query,fullBody).then(function() { - res.send(204); - }).otherwise(function(err) { - util.log("[red] Error saving library entry '"+path+"' : "+err); + }).otherwise(function(err) { + if (err) { + util.log("[red] Error loading library entry '"+path+"' : "+err); if (err.message.indexOf('forbidden') === 0) { res.send(403); return; } - res.send(500); - }); + } + res.send(404); + }); }); - }); + + redApp.post(new RegExp("/library/"+type+"\/(.*)"),function(req,res) { + var path = req.params[0]; + var fullBody = ''; + req.on('data', function(chunk) { + fullBody += chunk.toString(); + }); + req.on('end', function() { + storage.saveLibraryEntry(type,path,req.query,fullBody).then(function() { + res.send(204); + }).otherwise(function(err) { + util.log("[red] Error saving library entry '"+path+"' : "+err); + if (err.message.indexOf('forbidden') === 0) { + res.send(403); + return; + } + res.send(500); + }); + }); + }); + } } module.exports = { init: function(app) { diff --git a/red/api/nodes.js b/red/api/nodes.js index 793461771..a55e295d8 100644 --- a/red/api/nodes.js +++ b/red/api/nodes.js @@ -55,7 +55,7 @@ module.exports = { return; } promise.then(function(info) { - res.json(info); + res.json(info); }).otherwise(function(err) { if (err.code === 404) { res.send(404); @@ -90,7 +90,6 @@ module.exports = { promise.then(function(removedNodes) { res.json(removedNodes); }).otherwise(function(err) { - console.log(err.stack); res.send(400,err.toString()); }); } catch(err) { diff --git a/red/api/ui.js b/red/api/ui.js index fa0957ff0..1654a2838 100644 --- a/red/api/ui.js +++ b/red/api/ui.js @@ -32,7 +32,7 @@ events.on("node-icon-dir",function(dir) { module.exports = { ensureSlash: function(req,res,next) { if (req.originalUrl.slice(-1) != "/") { - res.redirect(req.originalUrl+"/"); + res.redirect(301,req.originalUrl+"/"); } else { next(); } diff --git a/red/server.js b/red/server.js index b035c9add..8394c84b9 100644 --- a/red/server.js +++ b/red/server.js @@ -17,7 +17,7 @@ var express = require('express'); var util = require('util'); var when = require('when'); -var exec = require('child_process').exec; +var child_process = require('child_process'); var redNodes = require("./nodes"); var comms = require("./comms"); @@ -28,7 +28,7 @@ var nodeApp = null; var server = null; var settings = null; -function createServer(_server,_settings) { +function init(_server,_settings) { server = _server; settings = _settings; @@ -37,7 +37,6 @@ function createServer(_server,_settings) { nodeApp = express(); app = express(); - if (settings.httpAdminRoot !== false) { require("./api").init(app); } @@ -147,7 +146,7 @@ function installModule(module) { return; } util.log("[red] Installing module: "+module); - var child = exec('npm install --production '+module, function(err, stdin, stdout) { + var child = child_process.exec('npm install --production '+module, function(err, stdin, stdout) { if (err) { var lookFor404 = new RegExp(" 404 .*"+module+"$","m"); if (lookFor404.test(stdout)) { @@ -171,14 +170,14 @@ function installModule(module) { } function uninstallModule(module) { - var list = redNodes.removeModule(module); return when.promise(function(resolve,reject) { if (/[\s;]/.test(module)) { reject(new Error("Invalid module name")); return; } + var list = redNodes.removeModule(module); util.log("[red] Removing module: "+module); - var child = exec('npm remove '+module, function(err, stdin, stdout) { + var child = child_process.exec('npm remove '+module, function(err, stdin, stdout) { if (err) { util.log("[red] Removal of module "+module+" failed:"); util.log("------------------------------------------"); @@ -202,7 +201,7 @@ function stop() { } module.exports = { - init: createServer, + init: init, start: start, stop: stop, diff --git a/red/settings.js b/red/settings.js index f4301efc0..b597db38d 100644 --- a/red/settings.js +++ b/red/settings.js @@ -75,9 +75,17 @@ var persistentSettings = { }, reset: function() { + for (var i in userSettings) { + if (userSettings.hasOwnProperty(i)) { + delete persistentSettings[i]; + } + } userSettings = null; globalSettings = null; storage = null; + + + } } diff --git a/test/red/api/flows_spec.js b/test/red/api/flows_spec.js new file mode 100644 index 000000000..4e2f00312 --- /dev/null +++ b/test/red/api/flows_spec.js @@ -0,0 +1,91 @@ +/** + * Copyright 2014 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var should = require("should"); +var request = require('supertest'); +var express = require('express'); +var sinon = require('sinon'); +var when = require('when'); + +var app = express(); +var redNodes = require("../../../red/nodes"); + +var flows = require("../../../red/api/flows"); + +describe("flows api", function() { + + var app; + + before(function() { + app = express(); + app.use(express.json()); + app.get("/flows",flows.get); + app.post("/flows",flows.post); + }); + + it('returns flow', function(done) { + var getFlows = sinon.stub(redNodes,'getFlows', function() { + return [1,2,3]; + }); + request(app) + .get('/flows') + .set('Accept', 'application/json') + .expect(200) + .end(function(err,res) { + getFlows.restore(); + if (err) { + throw err; + } + res.body.should.be.an.Array.and.have.lengthOf(3); + done(); + }); + }); + + it('sets flows', function(done) { + var setFlows = sinon.stub(redNodes,'setFlows', function() { + return when.resolve(); + }); + request(app) + .post('/flows') + .set('Accept', 'application/json') + .expect(204) + .end(function(err,res) { + setFlows.restore(); + if (err) { + throw err; + } + done(); + }); + }); + it('returns error when set fails', function(done) { + var setFlows = sinon.stub(redNodes,'setFlows', function() { + return when.reject(new Error("test error")); + }); + request(app) + .post('/flows') + .set('Accept', 'application/json') + .expect(500) + .end(function(err,res) { + setFlows.restore(); + if (err) { + throw err; + } + res.text.should.eql("test error"); + done(); + }); + }); + +}); diff --git a/test/red/api/index_spec.js b/test/red/api/index_spec.js new file mode 100644 index 000000000..b2ae23931 --- /dev/null +++ b/test/red/api/index_spec.js @@ -0,0 +1,93 @@ +/** + * Copyright 2014 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var should = require("should"); +var request = require("supertest"); +var express = require("express"); +var fs = require("fs"); +var path = require("path"); + +var settings = require("../../../red/settings"); +var api = require("../../../red/api"); + + +describe("api index", function() { + var app; + + describe("disables editor", function() { + before(function() { + settings.init({disableEditor:true}); + app = express(); + api.init(app); + }); + after(function() { + settings.reset(); + }); + + it('does not serve the editor', function(done) { + request(app) + .get("/") + .expect(404,done) + }); + it('does not serve icons', function(done) { + request(app) + .get("/icons/default.png") + .expect(404,done) + }); + it('does not serve settings', function(done) { + request(app) + .get("/settings") + .expect(404,done) + }); + + }); + + describe("enables editor", function() { + before(function() { + settings.init({disableEditor:false}); + app = express(); + api.init(app); + }); + after(function() { + settings.reset(); + }); + + it('serves the editor', function(done) { + request(app) + .get("/") + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + // Index page should probably mention Node-RED somewhere + res.text.indexOf("Node-RED").should.not.eql(-1); + done(); + }); + }); + it('serves icons', function(done) { + request(app) + .get("/icons/inject.png") + .expect("Content-Type", /image\/png/) + .expect(200,done) + }); + it('serves settings', function(done) { + request(app) + .get("/settings") + .expect(200,done) + }); + }); +}); \ No newline at end of file diff --git a/test/red/library_spec.js b/test/red/api/library_spec.js similarity index 57% rename from test/red/library_spec.js rename to test/red/api/library_spec.js index 522552872..796d6b1b9 100644 --- a/test/red/library_spec.js +++ b/test/red/api/library_spec.js @@ -15,52 +15,68 @@ **/ var should = require("should"); -var sinon = require('sinon'); var request = require('supertest'); -var http = require('http'); var express = require('express'); -var fs = require('fs-extra'); -var path = require('path'); var when = require('when'); var app = express(); -var RED = require("../../red/red.js"); -var server = require("../../red/server.js"); -var nodes = require("../../red/nodes"); +var RED = require("../../../red/red.js"); +var storage = require("../../../red/storage"); +var library = require("../../../red/api/library"); -describe("library", function() { - var userDir = path.join(__dirname,".testUserHome"); - before(function(done) { - fs.remove(userDir,function(err) { - fs.mkdir(userDir,function() { - sinon.stub(nodes, 'load', function() { - return when.promise(function(resolve,reject){ - resolve([]); - }); - }); - RED.init(http.createServer(function(req,res){app(req,res)}), - {userDir: userDir}); - server.start().then(function () { done(); }); - }); +describe("library api", function() { + + function initStorage(_flows,_libraryEntries) { + var flows = _flows; + var libraryEntries = _libraryEntries; + storage.init({ + storageModule: { + init: function() { + return when.resolve(); + }, + getAllFlows: function() { + return when.resolve(flows); + }, + getFlow: function(fn) { + if (flows[fn]) { + return when.resolve(flows[fn]); + } else { + return when.reject(); + } + }, + saveFlow: function(fn,data) { + flows[fn] = data; + return when.resolve(); + }, + getLibraryEntry: function(type,path) { + if (libraryEntries[type] && libraryEntries[type][path]) { + return when.resolve(libraryEntries[type][path]); + } else { + return when.reject(); + } + }, + saveLibraryEntry: function(type,path,meta,body) { + libraryEntries[type][path] = body; + return when.resolve(); + } + } }); - }); - - after(function(done) { - fs.remove(userDir,done); - server.stop(); - nodes.load.restore(); - }); - - afterEach(function(done) { - fs.remove(userDir,function(err) { - fs.mkdir(userDir,done); - }); - }); + } describe("flows", function() { + var app; + + before(function() { + app = express(); + app.use(express.json()); + app.get("/library/flows",library.getAll); + app.post(new RegExp("/library/flows\/(.*)"),library.post); + app.get(new RegExp("/library/flows\/(.*)"),library.get); + }); it('returns empty result', function(done) { - request(RED.httpAdmin) + initStorage({}); + request(app) .get('/library/flows') .expect(200) .end(function(err,res) { @@ -68,20 +84,24 @@ describe("library", function() { throw err; } res.body.should.not.have.property('f'); + res.body.should.not.have.property('d'); done(); }); }); it('returns 404 for non-existent entry', function(done) { - request(RED.httpAdmin) + initStorage({}); + request(app) .get('/library/flows/foo') .expect(404) .end(done); }); - + + it('can store and retrieve item', function(done) { + initStorage({}); var flow = '[]'; - request(RED.httpAdmin) + request(app) .post('/library/flows/foo') .set('Content-Type', 'text/plain') .send(flow) @@ -89,7 +109,7 @@ describe("library", function() { if (err) { throw err; } - request(RED.httpAdmin) + request(app) .get('/library/flows/foo') .expect(200) .end(function(err,res) { @@ -101,55 +121,57 @@ describe("library", function() { }); }); }); - + it('lists a stored item', function(done) { - request(RED.httpAdmin) - .post('/library/flows/bar') - .expect(204) - .end(function () { - request(RED.httpAdmin) - .get('/library/flows') - .expect(200) - .end(function(err,res) { - if (err) { - throw err; - } - res.body.should.have.property('f'); - should.deepEqual(res.body.f,['bar']); - done(); - }); + initStorage({f:["bar"]}); + request(app) + .get('/library/flows') + .expect(200) + .end(function(err,res) { + if (err) { + throw err; + } + res.body.should.have.property('f'); + should.deepEqual(res.body.f,['bar']); + done(); }); }); - - it('returns 403 for malicious access attempt', function(done) { + + it('returns 403 for malicious get attempt', function(done) { + initStorage({}); // without the userDir override the malicious url would be // http://127.0.0.1:1880/library/flows/../../package to // obtain package.json from the node-red root. - request(RED.httpAdmin) + request(app) .get('/library/flows/../../../../../package') .expect(403) .end(done); }); - - it('returns 403 for malicious access attempt', function(done) { + it('returns 403 for malicious post attempt', function(done) { + initStorage({}); // without the userDir override the malicious url would be // http://127.0.0.1:1880/library/flows/../../package to // obtain package.json from the node-red root. - request(RED.httpAdmin) + request(app) .post('/library/flows/../../../../../package') .expect(403) .end(done); }); - }); describe("type", function() { + var app; + before(function() { - RED.library.register('test'); + app = express(); + app.use(express.json()); + library.init(app); + RED.library.register("test"); }); it('returns empty result', function(done) { - request(RED.httpAdmin) + initStorage({},{'test':{"":[]}}); + request(app) .get('/library/test') .expect(200) .end(function(err,res) { @@ -160,17 +182,19 @@ describe("library", function() { done(); }); }); - + it('returns 404 for non-existent entry', function(done) { - request(RED.httpAdmin) + initStorage({},{}); + request(app) .get('/library/test/foo') .expect(404) .end(done); }); - + it('can store and retrieve item', function(done) { + initStorage({},{'test':{}}); var flow = '[]'; - request(RED.httpAdmin) + request(app) .post('/library/test/foo') .set('Content-Type', 'text/plain') .send(flow) @@ -178,7 +202,7 @@ describe("library", function() { if (err) { throw err; } - request(RED.httpAdmin) + request(app) .get('/library/test/foo') .expect(200) .end(function(err,res) { @@ -190,48 +214,46 @@ describe("library", function() { }); }); }); - + it('lists a stored item', function(done) { - request(RED.httpAdmin) - .post('/library/test/bar') - .expect(204) - .end(function () { - request(RED.httpAdmin) - .get('/library/test') - .expect(200) - .end(function(err,res) { - if (err) { - throw err; - } - should.deepEqual(res.body,[{ fn: 'bar'}]); - done(); - }); - }); + initStorage({},{'test':{'':['abc','def']}}); + request(app) + .get('/library/test') + .expect(200) + .end(function(err,res) { + if (err) { + throw err; + } + // This response isn't strictly accurate - but it + // verifies the api returns what storage gave it + should.deepEqual(res.body,['abc','def']); + done(); + }); }); - - + + it('returns 403 for malicious access attempt', function(done) { - request(RED.httpAdmin) + request(app) .get('/library/test/../../../../../../../../../../etc/passwd') .expect(403) .end(done); }); - + it('returns 403 for malicious access attempt', function(done) { - request(RED.httpAdmin) + request(app) .get('/library/test/..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\etc\\passwd') .expect(403) .end(done); }); - + it('returns 403 for malicious access attempt', function(done) { - request(RED.httpAdmin) + request(app) .post('/library/test/../../../../../../../../../../etc/passwd') .set('Content-Type', 'text/plain') .send('root:x:0:0:root:/root:/usr/bin/tclsh') .expect(403) .end(done); }); - + }); }); diff --git a/test/red/api/nodes_spec.js b/test/red/api/nodes_spec.js new file mode 100644 index 000000000..0e683348a --- /dev/null +++ b/test/red/api/nodes_spec.js @@ -0,0 +1,575 @@ +/** + * Copyright 2014 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var should = require("should"); +var request = require('supertest'); +var express = require('express'); +var sinon = require('sinon'); +var when = require('when'); + +var app = express(); +var redNodes = require("../../../red/nodes"); +var server = require("../../../red/server"); +var settings = require("../../../red/settings"); + +var nodes = require("../../../red/api/nodes"); + +describe("nodes api", function() { + + var app; + + before(function() { + app = express(); + app.use(express.json()); + app.get("/nodes",nodes.getAll); + app.post("/nodes",nodes.post); + app.get("/nodes/:id",nodes.get); + app.put("/nodes/:id",nodes.put); + app.delete("/nodes/:id",nodes.delete); + }); + + describe('get nodes', function() { + it('returns node list', function(done) { + var getNodeList = sinon.stub(redNodes,'getNodeList', function() { + return [1,2,3]; + }); + request(app) + .get('/nodes') + .set('Accept', 'application/json') + .expect(200) + .end(function(err,res) { + getNodeList.restore(); + if (err) { + throw err; + } + res.body.should.be.an.Array.and.have.lengthOf(3); + done(); + }); + }); + + it('returns node configs', function(done) { + var getNodeConfigs = sinon.stub(redNodes,'getNodeConfigs', function() { + return ""; + }); + request(app) + .get('/nodes') + .set('Accept', 'text/html') + .expect(200) + .expect("") + .end(function(err,res) { + getNodeConfigs.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + it('returns an individual node info', function(done) { + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo', function(id) { + return {"123":{id:"123"}}[id]; + }); + request(app) + .get('/nodes/123') + .set('Accept', 'application/json') + .expect(200) + .end(function(err,res) { + getNodeInfo.restore(); + if (err) { + throw err; + } + res.body.should.have.property("id","123"); + done(); + }); + }); + + it('returns an individual node configs', function(done) { + var getNodeConfig = sinon.stub(redNodes,'getNodeConfig', function(id) { + return {"123":""}[id]; + }); + request(app) + .get('/nodes/123') + .set('Accept', 'text/html') + .expect(200) + .expect("") + .end(function(err,res) { + getNodeConfig.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + it('returns 404 for unknown node', function(done) { + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo', function(id) { + return {"123":{id:"123"}}[id]; + }); + request(app) + .get('/nodes/456') + .set('Accept', 'application/json') + .expect(404) + .end(function(err,res) { + getNodeInfo.restore(); + if (err) { + throw err; + } + done(); + }); + }); + }); + + describe('install', function() { + + it('returns 400 if settings are unavailable', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return false; + }); + request(app) + .post('/nodes') + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + describe('by module', function() { + it('installs the module and returns node info', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return null; + }); + var installModule = sinon.stub(server,'installModule', function() { + return when.resolve({id:"123"}); + }); + + request(app) + .post('/nodes') + .send({module: 'foo'}) + .expect(200) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeModuleInfo.restore(); + installModule.restore(); + if (err) { + throw err; + } + res.body.should.have.property("id","123"); + done(); + }); + }); + + it('fails the install if already installed', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return {id:"123"}; + }); + var installModule = sinon.stub(server,'installModule', function() { + return when.resolve({id:"123"}); + }); + + request(app) + .post('/nodes') + .send({module: 'foo'}) + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeModuleInfo.restore(); + installModule.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + it('fails the install if module error', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return null; + }); + var installModule = sinon.stub(server,'installModule', function() { + return when.reject(new Error("test error")); + }); + + request(app) + .post('/nodes') + .send({module: 'foo'}) + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeModuleInfo.restore(); + installModule.restore(); + if (err) { + throw err; + } + res.text.should.equal("Error: test error"); + done(); + }); + }); + it('fails the install if module not found', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return null; + }); + var installModule = sinon.stub(server,'installModule', function() { + var err = new Error("test error"); + err.code = 404; + return when.reject(err); + }); + + request(app) + .post('/nodes') + .send({module: 'foo'}) + .expect(404) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeModuleInfo.restore(); + installModule.restore(); + if (err) { + throw err; + } + done(); + }); + }); + }); + }); + describe('delete', function() { + it('returns 400 if settings are unavailable', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return false; + }); + request(app) + .del('/nodes/123') + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + describe('by module', function() { + it('uninstalls the module and returns node info', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return null; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return {id:"123"}; + }); + var uninstallModule = sinon.stub(server,'uninstallModule', function() { + return when.resolve({id:"123"}); + }); + + request(app) + .del('/nodes/foo') + .expect(200) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + getNodeModuleInfo.restore(); + uninstallModule.restore(); + if (err) { + throw err; + } + res.body.should.have.property("id","123"); + done(); + }); + }); + + it('fails the uninstall if the module is not installed', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return null; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return null; + }); + + request(app) + .del('/nodes/foo') + .expect(404) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + getNodeModuleInfo.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + it('fails the uninstall if the module is not installed', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return null; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return {id:"123"}; + }); + var uninstallModule = sinon.stub(server,'uninstallModule', function() { + return when.reject(new Error("test error")); + }); + + request(app) + .del('/nodes/foo') + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + getNodeModuleInfo.restore(); + uninstallModule.restore(); + if (err) { + throw err; + } + res.text.should.equal("Error: test error"); + done(); + }); + }); + }); + + }); + + describe('enable/disable', function() { + it('returns 400 if settings are unavailable', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return false; + }); + request(app) + .put('/nodes/123') + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + it('returns 400 for invalid payload', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + + request(app) + .put('/nodes/foo') + .send({}) + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + if (err) { + throw err; + } + res.text.should.equal("Invalid request"); + + done(); + }); + }); + it('returns 404 for unknown node', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return null; + }); + + request(app) + .put('/nodes/foo') + .send({enabled:false}) + .expect(404) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + it('enables disabled node', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return {id:"123",enabled: false}; + }); + var enableNode = sinon.stub(redNodes,'enableNode',function(id) { + return {id:"123",enabled: true,types:['a']}; + }); + + request(app) + .put('/nodes/foo') + .send({enabled:true}) + .expect(200) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + enableNode.restore(); + if (err) { + throw err; + } + res.body.should.have.property("id","123"); + res.body.should.have.property("enabled",true); + + done(); + }); + }); + it('disables enabled node', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return {id:"123",enabled: true}; + }); + var disableNode = sinon.stub(redNodes,'disableNode',function(id) { + return {id:"123",enabled: false,types:['a']}; + }); + + request(app) + .put('/nodes/foo') + .send({enabled:false}) + .expect(200) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + disableNode.restore(); + if (err) { + throw err; + } + res.body.should.have.property("id","123"); + res.body.should.have.property("enabled",false); + + done(); + }); + }); + describe('no-ops if already in the right state', function() { + function run(state,done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return {id:"123",enabled: state}; + }); + var enableNode = sinon.stub(redNodes,'enableNode',function(id) { + return {id:"123",enabled: true,types:['a']}; + }); + + var disableNode = sinon.stub(redNodes,'disableNode',function(id) { + return {id:"123",enabled: false,types:['a']}; + }); + + request(app) + .put('/nodes/foo') + .send({enabled:state}) + .expect(200) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + var enableNodeCalled = enableNode.called; + var disableNodeCalled = disableNode.called; + enableNode.restore(); + disableNode.restore(); + if (err) { + throw err; + } + enableNodeCalled.should.be.false; + disableNodeCalled.should.be.false; + res.body.should.have.property("id","123"); + res.body.should.have.property("enabled",state); + + done(); + }); + } + it('already enabled', function(done) { + run(true,done); + }); + it('already disabled', function(done) { + run(false,done); + }); + }); + describe('does not no-op if err on node', function() { + function run(state,done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return {id:"123",enabled: state, err:"foo" }; + }); + var enableNode = sinon.stub(redNodes,'enableNode',function(id) { + return {id:"123",enabled: true,types:['a']}; + }); + + var disableNode = sinon.stub(redNodes,'disableNode',function(id) { + return {id:"123",enabled: false,types:['a']}; + }); + + request(app) + .put('/nodes/foo') + .send({enabled:state}) + .expect(200) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + var enableNodeCalled = enableNode.called; + var disableNodeCalled = disableNode.called; + enableNode.restore(); + disableNode.restore(); + if (err) { + throw err; + } + enableNodeCalled.should.be.equal(state); + disableNodeCalled.should.be.equal(!state); + res.body.should.have.property("id","123"); + res.body.should.have.property("enabled",state); + + done(); + }); + } + it('already enabled', function(done) { + run(true,done); + }); + it('already disabled', function(done) { + run(false,done); + }); + }); + }); + + +}); \ No newline at end of file diff --git a/test/red/api/ui_spec.js b/test/red/api/ui_spec.js new file mode 100644 index 000000000..139a1aa22 --- /dev/null +++ b/test/red/api/ui_spec.js @@ -0,0 +1,183 @@ +/** + * Copyright 2014 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var should = require("should"); +var request = require("supertest"); +var express = require("express"); +var fs = require("fs"); +var path = require("path"); + +var settings = require("../../../red/settings"); +var events = require("../../../red/events"); +var ui = require("../../../red/api/ui"); + + +describe("ui api", function() { + var app; + + + describe("slash handler", function() { + before(function() { + app = express(); + app.get("/foo",ui.ensureSlash,function(req,res) { res.send(200);}); + }); + it('redirects if the path does not end in a slash',function(done) { + request(app) + .get('/foo') + .expect(301,done); + }); + it('does not redirect if the path ends in a slash',function(done) { + request(app) + .get('/foo/') + .expect(200,done); + }); + }); + + describe("icon handler", function() { + before(function() { + app = express(); + app.get("/icons/:icon",ui.icon); + }); + + function binaryParser(res, callback) { + res.setEncoding('binary'); + res.data = ''; + res.on('data', function (chunk) { + res.data += chunk; + }); + res.on('end', function () { + callback(null, new Buffer(res.data, 'binary')); + }); + } + function compareBuffers(b1,b2) { + b1.length.should.equal(b2.length); + for (var i=0;ipm|JY5_^IIbrr*#Br`l>NUU`M`mYFm{Gnm$g4<{K@tLs$lSR^>bP0l+XkK5O^!s literal 0 HcmV?d00001