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;i