Add unit tests for refactored API modules

This commit is contained in:
Nick O'Leary 2014-11-04 17:05:29 +00:00
parent 72f9471f2b
commit e7eb02fcb7
16 changed files with 1352 additions and 318 deletions

View File

@ -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");

View File

@ -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);
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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();
}

View File

@ -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,

View File

@ -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;
}
}

View File

@ -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();
});
});
});

View File

@ -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)
});
});
});

View File

@ -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);
});
});
});

575
test/red/api/nodes_spec.js Normal file
View File

@ -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 "<script></script>";
});
request(app)
.get('/nodes')
.set('Accept', 'text/html')
.expect(200)
.expect("<script></script>")
.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":"<script></script>"}[id];
});
request(app)
.get('/nodes/123')
.set('Accept', 'text/html')
.expect(200)
.expect("<script></script>")
.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);
});
});
});
});

183
test/red/api/ui_spec.js Normal file
View File

@ -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;i<b1.length;i++) {
b1[i].should.equal(b2[i]);
}
}
it('returns the default icon when getting an unknown icon', function(done) {
var defaultIcon = fs.readFileSync(path.resolve(__dirname+'/../../../public/icons/arrow-in.png'));
request(app)
.get("/icons/youwonthaveme.png")
.expect("Content-Type", /image\/png/)
.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();
});
});
it('returns a known icon', function(done) {
var injectIcon = fs.readFileSync(path.resolve(__dirname+'/../../../public/icons/inject.png'));
request(app)
.get("/icons/inject.png")
.expect("Content-Type", /image\/png/)
.expect(200)
.parse(binaryParser)
.end(function(err, res){
if (err){
return done(err);
}
Buffer.isBuffer(res.body).should.be.true;
compareBuffers(res.body,injectIcon);
done();
});
});
it('returns a registered icon' , function(done) {
var testIcon = fs.readFileSync(path.resolve(__dirname+'/../../resources/icons/test_icon.png'));
events.emit("node-icon-dir", path.resolve(__dirname+'/../../resources/icons'));
request(app)
.get("/icons/test_icon.png")
.expect("Content-Type", /image\/png/)
.expect(200)
.parse(binaryParser)
.end(function(err, res){
if (err){
return done(err);
}
Buffer.isBuffer(res.body).should.be.true;
compareBuffers(res.body,testIcon);
done();
});
});
});
describe("settings handler", function() {
before(function() {
var userSettings = {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion"
}
settings.init(userSettings);
app = express();
app.get("/settings",ui.settings);
//app.use("/",ui.editor);
});
after(function() {
settings.reset();
});
it('returns the filtered settings', function(done) {
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("httpNodeRoot","testHttpNodeRoot");
res.body.should.have.property("version","testVersion");
res.body.should.not.have.property("foo",123);
done();
});
});
});
describe("editor ui handler", function() {
before(function() {
app = express();
app.use("/",ui.editor);
});
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();
});
});
});
});

View File

@ -14,9 +14,226 @@
* limitations under the License.
**/
var should = require("should");
var when = require("when");
var sinon = require("sinon");
var child_process = require('child_process');
var comms = require("../../red/comms");
var redNodes = require("../../red/nodes");
var api = require("../../red/api");
var server = require("../../red/server");
describe("red/server", function() {
it('can be required without errors', function() {
require("../../red/server");
var commsMessages = [];
var commsPublish;
beforeEach(function() {
commsMessages = [];
});
before(function() {
commsPublish = sinon.stub(comms,"publish", function(topic,msg,retained) {
commsMessages.push({topic:topic,msg:msg,retained:retained});
});
});
after(function() {
commsPublish.restore();
});
it("initialises components", function() {
var commsInit = sinon.stub(comms,"init",function() {});
var apiInit = sinon.stub(api,"init",function() {});
var dummyServer = {};
server.init(dummyServer,{httpAdminRoot:"/"});
commsInit.called.should.be.true;
apiInit.called.should.be.true;
should.exist(server.app);
should.exist(server.nodeApp);
server.server.should.equal(dummyServer);
commsInit.restore();
apiInit.restore();
});
it("does not initalise api when disabled", function() {
var commsInit = sinon.stub(comms,"init",function() {});
var apiInit = sinon.stub(api,"init",function() {});
var dummyServer = {};
server.init(dummyServer,{httpAdminRoot:false});
commsInit.called.should.be.true;
apiInit.called.should.be.false;
should.exist(server.app);
should.exist(server.nodeApp);
server.server.should.equal(dummyServer);
commsInit.restore();
apiInit.restore();
});
it("stops components", function() {
var commsStop = sinon.stub(comms,"stop",function() {} );
var stopFlows = sinon.stub(redNodes,"stopFlows",function() {} );
server.stop();
commsStop.called.should.be.true;
stopFlows.called.should.be.true;
commsStop.restore();
stopFlows.restore();
});
it("reports added modules", function() {
var nodes = [
{types:["a"]},
{module:"foo",types:["b"]},
{types:["c"],err:"error"}
];
var result = server.reportAddedModules(nodes);
result.should.equal(nodes);
commsMessages.should.have.length(1);
commsMessages[0].topic.should.equal("node/added");
commsMessages[0].msg.should.eql(nodes);
});
it("reports removed modules", function() {
var nodes = [
{types:["a"]},
{module:"foo",types:["b"]},
{types:["c"],err:"error"}
];
var result = server.reportRemovedModules(nodes);
result.should.equal(nodes);
commsMessages.should.have.length(1);
commsMessages[0].topic.should.equal("node/removed");
commsMessages[0].msg.should.eql(nodes);
});
describe("installs module", function() {
it("rejects invalid module names", function(done) {
var promises = [];
promises.push(server.installModule("this_wont_exist "));
promises.push(server.installModule("this_wont_exist;no_it_really_wont"));
when.settle(promises).then(function(results) {
results[0].state.should.be.eql("rejected");
results[1].state.should.be.eql("rejected");
done();
});
});
it("rejects when npm returns a 404", function(done) {
var exec = sinon.stub(child_process,"exec",function(cmd,cb) {
cb(new Error(),""," 404 this_wont_exist");
});
server.installModule("this_wont_exist").otherwise(function(err) {
err.code.should.be.eql(404);
done();
}).finally(function() {
exec.restore();
});
});
it("rejects with generic error", function(done) {
var exec = sinon.stub(child_process,"exec",function(cmd,cb) {
cb(new Error("test_error"),"","");
});
server.installModule("this_wont_exist").then(function() {
done(new Error("Unexpected success"));
}).otherwise(function(err) {
err.message.should.be.eql("Install failed");
done();
}).finally(function() {
exec.restore();
});
});
it("succeeds when module is found", function(done) {
var nodeInfo = {module:"foo",types:["a"]};
var exec = sinon.stub(child_process,"exec",function(cmd,cb) {
cb(null,"","");
});
var addModule = sinon.stub(redNodes,"addModule",function(md) {
return when.resolve(nodeInfo);
});
server.installModule("this_wont_exist").then(function(info) {
info.should.eql(nodeInfo);
commsMessages.should.have.length(1);
commsMessages[0].topic.should.equal("node/added");
commsMessages[0].msg.should.eql(nodeInfo);
done();
}).otherwise(function(err) {
done(err);
}).finally(function() {
exec.restore();
addModule.restore();
});
});
});
describe("uninstalls module", function() {
it("rejects invalid module names", function(done) {
var promises = [];
promises.push(server.uninstallModule("this_wont_exist "));
promises.push(server.uninstallModule("this_wont_exist;no_it_really_wont"));
when.settle(promises).then(function(results) {
results[0].state.should.be.eql("rejected");
results[1].state.should.be.eql("rejected");
done();
});
});
it("rejects with generic error", function(done) {
var nodeInfo = [{module:"foo",types:["a"]}];
var removeModule = sinon.stub(redNodes,"removeModule",function(md) {
return when.resolve(nodeInfo);
});
var exec = sinon.stub(child_process,"exec",function(cmd,cb) {
cb(new Error("test_error"),"","");
});
server.uninstallModule("this_wont_exist").then(function() {
done(new Error("Unexpected success"));
}).otherwise(function(err) {
err.message.should.be.eql("Removal failed");
done();
}).finally(function() {
exec.restore();
removeModule.restore();
});
});
it("succeeds when module is found", function(done) {
var nodeInfo = [{module:"foo",types:["a"]}];
var removeModule = sinon.stub(redNodes,"removeModule",function(md) {
return nodeInfo;
});
var exec = sinon.stub(child_process,"exec",function(cmd,cb) {
cb(null,"","");
});
server.uninstallModule("this_wont_exist").then(function(info) {
info.should.eql(nodeInfo);
commsMessages.should.have.length(1);
commsMessages[0].topic.should.equal("node/removed");
commsMessages[0].msg.should.eql(nodeInfo);
done();
}).otherwise(function(err) {
done(err);
}).finally(function() {
exec.restore();
removeModule.restore();
});
});
});
});

View File

@ -108,7 +108,27 @@ describe("red/settings", function() {
}).otherwise(function(err) {
done(err);
});
});
it('removes persistent settings when reset', function() {
var userSettings = {
a: 123,
b: "test",
c: [1,2,3]
}
settings.init(userSettings);
settings.available().should.be.false;
settings.should.have.property("a",123);
settings.should.have.property("b","test");
settings.c.should.be.an.Array.with.lengthOf(3);
settings.reset();
settings.should.not.have.property("a");
settings.should.not.have.property("d");
settings.should.not.have.property("c");
});
});

View File

@ -1,177 +0,0 @@
/**
* 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 request = require("supertest");
var express = require("express");
var redUI = require("../../red/ui");
describe("red/ui icon handler", function() {
it('returns the default icon when getting an unknown icon', function(done) {
var app = express();
redUI({},app);
request(app)
.get("/icons/youwonthaveme.png")
.expect('Content-Type', /image\/png/)
.expect(200)
.end(function(err, res){
if (err){
return done(err);
}
done();
});
});
it('returns an icon from disk', function(done) {
var app = express();
redUI({},app);
request(app)
.get("/icons/arduino.png")
.expect('Content-Type', /image\/png/)
.expect(200)
.end(function(err, res){
if (err){
return done(err);
}
done();
});
});
});
describe("icon cache handler", function() {
var fs = require('fs-extra');
var path = require('path');
var events = require("../../red/events");
var tempDir = path.join(__dirname,".tmp/");
var cachedFakePNG = tempDir + "cacheMe.png";
beforeEach(function(done) {
fs.remove(tempDir,function(err) {
fs.mkdirSync(tempDir);
fs.writeFileSync(cachedFakePNG, "Hello PNG\n");
done();
});
});
afterEach(function(done) {
fs.exists(cachedFakePNG, function(exists) {
if(exists) {
fs.unlinkSync(cachedFakePNG);
}
fs.remove(tempDir,done);
})
});
/*
* This test case test that:
* 1) any directory can be added to the path lookup (such as /tmp) by
* calling the right event
* 2) that a file we know exists gets cached so that the lookup/verification
* of actual existence doesn't occur again when a subsequent request comes in
*
* The second point verifies that the cache works. If the cache wouldn't work
* the default PNG would be served
*/
it('returns an icon using icon cache', function(done) {
var app = express();
redUI({},app);
events.emit("node-icon-dir", tempDir);
request(app)
.get("/icons/cacheMe.png")
.expect('Content-Type', /image\/png/)
.expect(200)
.end(function(err, res){
if (err){
return done(err);
}
fs.unlink(cachedFakePNG, function(err) {
if(err) {
return done(err);
}
request(app)
.get("/icons/cacheMe.png")
.expect('Content-Type', /text\/html/)
.expect(404)
.end(function(err, res){
if (err){
return done(err);
}
done();
});
});
});
});
});
describe("red/ui settings handler", function() {
it('returns the provided settings', function(done) {
var settings = {
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
};
var app = express();
redUI(settings,app);
request(app)
.get("/settings")
.expect('Content-Type', /application\/json/)
.expect(200, "{\n \"httpNodeRoot\": \"testHttpNodeRoot\",\n \"version\": \"testVersion\"\n}")
.end(function(err, res){
if (err){
return done(err);
}
done();
});
});
});
describe("red/ui root handler", function() {
it('server up the main page', function(done) {
var app = express();
redUI({},app);
request(app)
.get("/")
.expect('Content-Type', /text\/html/)
.expect(200)
.end(function(err, res){
if (err){
return done(err);
}
done();
});
});
it('redirects to path ending with /', function(done) {
var rapp = express();
redUI({},rapp);
var app = express().use('/root', rapp);
request(app)
.get("/root")
.expect('Content-Type', /text\/plain/)
.expect(302)
.end(function(err, res){
if (err){
return done(err);
}
done();
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B