diff options
Diffstat (limited to 'dgbuilder/test')
52 files changed, 6153 insertions, 0 deletions
diff --git a/dgbuilder/test/_spec.js b/dgbuilder/test/_spec.js new file mode 100644 index 00000000..9eb7e07f --- /dev/null +++ b/dgbuilder/test/_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. + **/ + +/** + * This test simply checks that for every .js file there exists + * a *_spec.js file under ./test correspondingly. + */ + +/** + * Currently we're only checking the core components under ./red + * TODO: Increase the scope of this check + */ + +var fs = require("fs"); +var should = require("should"); +var path = require('path'); + +// Directories to check with .js files and _spec.js files respectively +var jsdir = path.resolve(__dirname, "../red"); +var testdir = path.resolve(__dirname, "red"); + +var fs = require('fs'); +var walkDirectory = function(dir, topdir, done) { + fs.readdir(dir, function(err, list) { + var error; + var errReturned = false; + if (err) { + return done(err); + } + + var i = 0; + (function next() { + var file = list[i++]; + + // return error if there are no more files to check and error has not been previously returned to avoid multiple calls to done() + if (!file) { + if (!errReturned) { + errReturned = true; + return done(error); + } + } else { + file = path.resolve(dir, file); + fs.stat(file, function(err, stat) { + if (stat && stat.isDirectory()) { + walkDirectory(file, false, function(err) { + if (!error) { + error = err; + } + next(); + }); + } else { + if (path.extname(file) === ".js") { + var testFile = file.replace(jsdir, testdir).replace(".js", "_spec.js"); + fs.exists(testFile, function (exists) { + try { + exists.should.equal(true, testFile + " does not exist"); + } catch (err) { + if (!topdir) { + return done(err); + } else { + error = err; + return; + } + } + }); + } + next(); + } + }); + } + })(); + }); +}; + +describe('_spec.js', function() { + this.timeout(50000); // we might not finish within the Mocha default timeout limit, project will also grow + it('is checking if all .js files have a corresponding _spec.js test file.', function(done) { + walkDirectory(jsdir, true, done); + }); +}); diff --git a/dgbuilder/test/nodes/core/core/20-inject_spec.js b/dgbuilder/test/nodes/core/core/20-inject_spec.js new file mode 100644 index 00000000..aeeac9dd --- /dev/null +++ b/dgbuilder/test/nodes/core/core/20-inject_spec.js @@ -0,0 +1,118 @@ +/** + * 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 injectNode = require("../../../../nodes/core/core/20-inject.js"); +var helper = require("../../helper.js"); + +describe('inject node', function() { + + before(function(done) { + helper.startServer(done); + }); + + afterEach(function() { + helper.unload(); + }); + + it('should inject once', function(done) { + + helper.load(injectNode, [{id:"n1", type:"inject", + payload:"payload", topic: "t1", + once: true, wires:[["n2"]] }, + {id:"n2", type:"helper"}], + function() { + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 't1'); + msg.should.have.property('payload', 'payload'); + done(); + }); + }); + }); + + it('should inject repeatedly', function(done) { + + helper.load(injectNode, [{id:"n1", type:"inject", + payload:"payload", topic: "t2", + repeat: 0.2, wires:[["n2"]] }, + {id:"n2", type:"helper"}], + function() { + var n2 = helper.getNode("n2"); + var count = 0; + n2.on("input", function(msg) { + msg.should.have.property('topic', 't2'); + msg.should.have.property('payload', 'payload'); + count += 1; + if (count > 2) { + helper.clearFlows().then(function() { + done(); + }); + } + }); + }); + }); + + it('should inject with cron', function(done) { + helper.load(injectNode, [{id:"n1", type:"inject", + payloadType:"date", topic: "t3", + crontab: "* * * * * *", wires:[["n3"]] }, + {id:"n3", type:"helper"}], + function() { + var n3 = helper.getNode("n3"); + n3.on("input", function(msg) { + msg.should.have.property('topic', 't3'); + msg.should.have.property('payload').be.a.Number; + helper.clearFlows().then(function() { + done(); + }); + }); + }); + }); + + describe('post', function() { + it('should inject message', function(done) { + helper.load(injectNode, + [{id:"n1", type:"inject", + payloadType:"some type", topic: "t4", + wires:[["n4"]] }, + { id:"n4", type:"helper"}], function() { + var n4 = helper.getNode("n4"); + n4.on("input", function(msg) { + msg.should.have.property('topic', 't4'); + msg.should.have.property('payload', ''); + helper.clearFlows().then(function() { + done(); + }); + }); + helper.request() + .post('/inject/n1') + .expect(200).end(function(err) { + if (err) { + return helper.clearFlows() + .then(function () { + done(err); + }); + } + }); + }); + }); + + it('should fail for invalid node', function(done) { + helper.request().post('/inject/invalid').expect(404).end(done); + }); + }); +}); diff --git a/dgbuilder/test/nodes/core/core/58-debug_spec.js b/dgbuilder/test/nodes/core/core/58-debug_spec.js new file mode 100644 index 00000000..f83fc2ca --- /dev/null +++ b/dgbuilder/test/nodes/core/core/58-debug_spec.js @@ -0,0 +1,298 @@ +/** + * 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 debugNode = require("../../../../nodes/core/core/58-debug.js"); +var helper = require("../../helper.js"); +var WebSocket = require('ws'); + +describe('debug node', function() { + + before(function(done) { + helper.startServer(done); + }); + + afterEach(function() { + helper.unload(); + }); + + + it('should be loaded', function(done) { + var flow = [{id:"n1", type:"debug", name: "Debug" }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + n1.should.have.property('name', 'Debug'); + done(); + }); + }); + + it('should publish on input', function(done) { + var flow = [{id:"n1", type:"debug", name: "Debug" }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + websocket_test(function() { + n1.emit("input", {payload:"test"}); + }, function(msg) { + JSON.parse(msg).should.eql({ + topic:"debug",data:{id:"n1",name:"Debug",msg:"test"} + }); + }, done); + }); + }); + + it('should publish to console', function(done) { + var flow = [{id:"n1", type:"debug", console: "true" }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + var count = 0; + n1.on('log', function(msg) { + msg.should.eql({level:'log',id:'n1',type:'debug',msg:'test'}); + count++; + if (count == 2) { + done(); + } + }); + websocket_test(function() { + n1.emit("input", {payload:"test"}); + }, function(msg) { + JSON.parse(msg).should.eql({ + topic:"debug",data:{id:"n1",msg:"test"} + }); + count++; + }, function() { + if (count == 2) { + done(); + } + }); + }); + }); + + it('should publish complete message', function(done) { + var flow = [{id:"n1", type:"debug", complete: "true" }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + websocket_test(function() { + n1.emit("input", {payload:"test"}); + }, function(msg) { + JSON.parse(msg).should.eql({ + topic:"debug", + data:{id:"n1",msg:'(Object) {\n "payload": "test"\n}'} + }); + }, done); + }); + }); + + it('should publish an Error', function(done) { + var flow = [{id:"n1", type:"debug" }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + websocket_test(function() { + n1.emit("input", {payload: new Error("oops")}); + }, function(msg) { + JSON.parse(msg).should.eql({ + topic:"debug",data:{id:"n1",msg:"Error: oops"} + }); + }, done); + }); + }); + + it('should publish a boolean', function(done) { + var flow = [{id:"n1", type:"debug" }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + websocket_test(function() { + n1.emit("input", {payload: true}); + }, function(msg) { + JSON.parse(msg).should.eql({ + topic:"debug",data:{id:"n1",msg: '(boolean) true'} + }); + }, done); + }); + }); + + it('should publish with no payload', function(done) { + var flow = [{id:"n1", type:"debug" }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + websocket_test(function() { + n1.emit("input", {}); + }, function(msg) { + JSON.parse(msg).should.eql({ + topic:"debug",data:{id:"n1",msg: '(undefined)'} + }); + }, done); + }); + }); + + it('should publish an object', function(done) { + var flow = [{id:"n1", type:"debug" }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + websocket_test(function() { + n1.emit("input", {payload: {type:'foo'}}); + }, function(msg) { + JSON.parse(msg).should.eql({ + topic:"debug", + data:{id:"n1",msg:'(Object) {\n "type": "foo"\n}'} + }); + }, done); + }); + }); + + it('should publish an array', function(done) { + var flow = [{id:"n1", type:"debug" }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + websocket_test(function() { + n1.emit("input", {payload: [0,1,2,3]}); + }, function(msg) { + JSON.parse(msg).should.eql({ + topic:"debug", + data:{id:"n1",msg: '(Array) [\n 0,\n 1,\n 2,\n 3\n]'} + }); + }, done); + }); + }); + + it('should publish an object with circular references', function(done) { + var flow = [{id:"n1", type:"debug" }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + websocket_test(function() { + var o = { name: 'bar' }; + o.o = o; + n1.emit("input", {payload: o}); + }, function(msg) { + JSON.parse(msg).should.eql({ + topic:"debug", + data:{ + id:"n1", + msg:'(Object) {\n "name": "bar",\n "o": "[circular]"\n}' + } + }); + }, done); + }); + }); + + it('should truncated a long message', function(done) { + var flow = [{id:"n1", type:"debug" }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + websocket_test(function() { + n1.emit("input", {payload: Array(1002).join("X")}); + }, function(msg) { + JSON.parse(msg).should.eql({ + topic:"debug", + data:{ + id:"n1", + msg: Array(1001).join("X")+' ....' + } + }); + }, done); + }); + }); + + it('should convert Buffer to hex', function(done) { + var flow = [{id:"n1", type:"debug" }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + websocket_test(function() { + n1.emit("input", {payload: new Buffer('HELLO', 'utf8')}); + }, function(msg) { + JSON.parse(msg).should.eql({ + topic:"debug", + data:{ + id:"n1", + msg: '(Buffer) 48454c4c4f', + } + }); + }, done); + }); + }); + + it('should publish when active', function(done) { + var flow = [{id:"n1", type:"debug", active: false }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + websocket_test(function() { + n1.emit("input", {payload:"message 1"}); + helper.request() + .post('/debug/n1/enable') + .expect(200).end(function(err) { + if (err) { return done(err); } + n1.emit("input", {payload:"message 2"}); + }); + }, function(msg) { + JSON.parse(msg).should.eql({ + topic:"debug",data:{id:"n1",msg:"message 2"} + }); + }, done); + }); + }); + + it('should not publish when inactive', function(done) { + var flow = [{id:"n1", type:"debug", active: true }]; + helper.load(debugNode, flow, function() { + var n1 = helper.getNode("n1"); + websocket_test(function(close) { + helper.request() + .post('/debug/n1/disable') + .expect(201).end(function(err) { + if (err) { + close(); + return done(err); + } + n1.emit("input", {payload:"message"}); + setTimeout(function() { + close(); + done(); + }, 200); + }); + }, function(msg) { + should.fail(null,null,"unexpected message"); + }, function() {}); + }); + }); + + describe('post', function() { + it('should return 404 on invalid state', function(done) { + var flow = [{id:"n1", type:"debug", active: true }]; + helper.load(debugNode, flow, function() { + helper.request() + .post('/debug/n1/foobar') + .expect(404).end(done); + }); + }); + + it('should return 404 on invalid node', function(done) { + helper.request() + .post('/debug/n99/enable') + .expect(404).end(done); + }); + }); + +}); + +function websocket_test(open_callback, message_callback, done_callback) { + var ws = new WebSocket(helper.url() + "/comms"); + var close_callback = function() { ws.close(); }; + ws.on('open', function() { open_callback(close_callback); }); + ws.on('message', function(msg) { + message_callback(msg, close_callback); + ws.close(); + done_callback(); + }); +} diff --git a/dgbuilder/test/nodes/core/core/80-function_spec.js b/dgbuilder/test/nodes/core/core/80-function_spec.js new file mode 100644 index 00000000..9efd93aa --- /dev/null +++ b/dgbuilder/test/nodes/core/core/80-function_spec.js @@ -0,0 +1,149 @@ +/** + * 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 functionNode = require("../../../../nodes/core/core/80-function.js"); +var helper = require("../../helper.js"); + +describe('function node', function() { + + before(function(done) { + helper.startServer(done); + }); + + afterEach(function() { + helper.unload(); + }); + + it('should be loaded', function(done) { + var flow = [{id:"n1", type:"function", name: "function" }]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + n1.should.have.property('name', 'function'); + done(); + }); + }); + + it('should send returned message', function(done) { + var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"return msg;"}, + {id:"n2", type:"helper"}]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'foo'); + done(); + }); + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + + it('should pass through _topic', function(done) { + var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"return msg;"}, + {id:"n2", type:"helper"}]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'foo'); + msg.should.have.property('_topic', 'baz'); + done(); + }); + n1.receive({payload:"foo",topic: "bar", _topic: "baz"}); + }); + }); + + it('should send to multiple outputs', function(done) { + var flow = [{id:"n1",type:"function",wires:[["n2"],["n3"]], + func:"return [{payload: '1'},{payload: '2'}];"}, + {id:"n2", type:"helper"}, {id:"n3", type:"helper"} ]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var n3 = helper.getNode("n3"); + var count = 0; + n2.on("input", function(msg) { + should(msg).have.property('payload', '1'); + count++; + if (count == 2) { + done(); + } + }); + n3.on("input", function(msg) { + should(msg).have.property('payload', '2'); + count++; + if (count == 2) { + done(); + } + }); + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + + it('should send to multiple messages', function(done) { + var flow = [{id:"n1",type:"function",wires:[["n2"]], + func:"return [[{payload: 1},{payload: 2}]];"}, + {id:"n2", type:"helper"} ]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var count = 0; + n2.on("input", function(msg) { + count++; + should(msg).have.property('payload', count); + should(msg).have.property('_topic', 'baz'); + if (count == 2) { + done(); + } + }); + n1.receive({payload:"foo", topic: "bar", _topic:"baz"}); + }); + }); + + it('should allow input to be discarded by returning null', function(done) { + var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"return null"}, + {id:"n2", type:"helper"}]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + setTimeout(function() { + done(); + }, 200); + n2.on("input", function(msg) { + should.fail(null,null,"unexpected message"); + }); + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + + it('should handle and log script error', function(done) { + var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"retunr"}]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + n1.on("log", function(msg) { + msg.should.have.property('level', 'error'); + msg.should.have.property('id', 'n1'); + msg.should.have.property('type', 'function'); + msg.should.have.property('msg', 'ReferenceError: retunr is not defined'); + done(); + }); + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + +}); diff --git a/dgbuilder/test/nodes/core/core/80-template_spec.js b/dgbuilder/test/nodes/core/core/80-template_spec.js new file mode 100644 index 00000000..a89afa37 --- /dev/null +++ b/dgbuilder/test/nodes/core/core/80-template_spec.js @@ -0,0 +1,46 @@ +/** + * 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 templateNode = require("../../../../nodes/core/core/80-template.js"); +var helper = require("../../helper.js"); + +describe('template node', function() { + + before(function(done) { + helper.startServer(done); + }); + + afterEach(function() { + helper.unload(); + }); + + + it('should modify payload', function(done) { + var flow = [{id:"n1", type:"template", field: "payload", template: "payload={{payload}}",wires:[["n2"]]},{id:"n2",type:"helper"}]; + helper.load(templateNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'payload=foo'); + done(); + }); + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + +}); diff --git a/dgbuilder/test/nodes/core/core/89-delay_spec.js b/dgbuilder/test/nodes/core/core/89-delay_spec.js new file mode 100644 index 00000000..22c3173a --- /dev/null +++ b/dgbuilder/test/nodes/core/core/89-delay_spec.js @@ -0,0 +1,420 @@ +/** + * 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 delayNode = require("../../../../nodes/core/core/89-delay.js"); +var helper = require("../../helper.js"); + +var GRACE_PERCENTAGE=10; + +var nanosToSeconds = 1000000000; +var millisToSeconds = 1000; + +var secondsToMinutes = 60; +var secondsToHours = 3600; +var secondsToDays = 86400; + + +describe('delayNode', function() { + + beforeEach(function(done) { + helper.startServer(done); + }); + + afterEach(function(done) { + helper.unload(); + helper.stopServer(done); + }); + + it('should be loaded', function(done) { + var flow = [{"id":"delayNode1","type":"delay","name":"delayNode","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"wires":[[]]}]; + helper.load(delayNode, flow, function() { + var delayNode1 = helper.getNode("delayNode1"); + delayNode1.should.have.property('name', 'delayNode'); + done(); + }); + }); + + var TimeUnitEnum = { + MILLIS : "milliseconds", + SECONDS : "seconds", + MINUTES : "minutes", + HOURS : "hours", + DAYS : "days" + } + + /** + * Tells whether two numeric values are close enough to each other + * @param actualValue - the value we're testing + * @param expectedValue - the value we're matching the test value against + * @param tolerancePercent - the percentage of tolerated deviation (0 means equals) + */ + function closeEnough(actualValue, expectedValue, tolerancePercent) { + var toReturn; + var toleranceFraction = expectedValue * (tolerancePercent/100); + var minExpected = expectedValue - toleranceFraction; + var maxExpected = expectedValue + toleranceFraction; + + if(actualValue >= minExpected && actualValue <= maxExpected) { + toReturn = true; + } else { + toReturn = false; + } + return toReturn; + } + + /** + * Runs a delay test + * @param aTimeout - the timeout quantity + * @param aTimeoutUnit - the unit of the timeout: milliseconds, seconds, minutes, hours, days + */ + function genericDelayTest(aTimeout, aTimeoutUnit, done) { + var flow = [{"id":"delayNode1","type":"delay","name":"delayNode","pauseType":"delay","timeout":aTimeout,"timeoutUnits":aTimeoutUnit,"rate":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(delayNode, flow, function() { + var delayNode1 = helper.getNode("delayNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + var endTime = process.hrtime(startTime); + var runtimeNanos = ( (endTime[0] * nanosToSeconds) + endTime[1] ); + var runtimeSeconds = runtimeNanos / nanosToSeconds; + var aTimeoutUnifiedToSeconds; + + // calculating the timeout in seconds + if(aTimeoutUnit == TimeUnitEnum.MILLIS) { + aTimeoutUnifiedToSeconds = aTimeout / millisToSeconds; + } else if(aTimeoutUnit == TimeUnitEnum.SECONDS) { + aTimeoutUnifiedToSeconds = aTimeout; + } else if(aTimeoutUnit == TimeUnitEnum.MINUTES) { + aTimeoutUnifiedToSeconds = aTimeout * secondsToMinutes; + } else if(aTimeoutUnit == TimeUnitEnum.HOURS) { + aTimeoutUnifiedToSeconds = aTimeout * secondsToHours; + } else if(aTimeoutUnit == TimeUnitEnum.DAYS) { + aTimeoutUnifiedToSeconds = aTimeout * secondsToDays; + } + + if(closeEnough(runtimeSeconds, aTimeoutUnifiedToSeconds, GRACE_PERCENTAGE)) { + done(); + } else { + try { + should.fail(null, null, "Delayed runtime seconds " + runtimeSeconds + " was not close enough to exlected timeout seconds: " + aTimeoutUnifiedToSeconds); + } catch (err) { + done(err); + } + } + } catch(err) { + done(err); + } + }); + var startTime = process.hrtime(); + delayNode1.receive({payload:"delayMe"}); + }); + } + + /** + * We send a message, take a timestamp then when the message is received by the helper node, we take another timestamp. + * Then check if the message has been delayed by the expected amount. + */ + it('delays the message in seconds', function(done) { + genericDelayTest(0.5, "seconds", done); + }); + + it('delays the message in milliseconds', function(done) { + genericDelayTest(500, "milliseconds", done); + }); + + it('delays the message in minutes', function(done) { // this is also 0.5 seconds + genericDelayTest(0.00833, "minutes", done); + }); + + it('delays the message in hours', function(done) { // this is also 0.5 seconds + genericDelayTest(0.0001388, "hours", done); + }); + + it('delays the message in days', function(done) { // this is also 0.5 seconds + genericDelayTest(0.000005787, "days", done); + }); + + /** + * Runs a rate limit test - only testing seconds! + * @param aLimit - the message limit count + * @param runtimeInMillis - when to terminate run and count messages received + */ + function genericRateLimitSECONDSTest(aLimit, runtimeInMillis, done) { + var flow = [{"id":"delayNode1","type":"delay","name":"delayNode","pauseType":"rate","timeout":5,"timeoutUnits":"seconds","rate":aLimit,"rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(delayNode, flow, function() { + var delayNode1 = helper.getNode("delayNode1"); + var helperNode1 = helper.getNode("helperNode1"); + var receivedMessagesStack = []; + var rate = 1000/aLimit; + + var receiveTimestamp; + + helperNode1.on("input", function(msg) { + if(receiveTimestamp) { + var elapse = process.hrtime(receiveTimestamp); + var receiveInterval = (elapse[0] * 1000) + ((elapse[1] / nanosToSeconds) * 1000); + receiveInterval.should.be.above(rate * 0.9); + } + receiveTimestamp = process.hrtime(); + receivedMessagesStack.push(msg); + }); + + var possibleMaxMessageCount = Math.ceil(aLimit * (runtimeInMillis / 1000) + aLimit); // +aLimit as at the start of the 2nd period, we're allowing the 3rd burst + + var i = 0; + for(; i < possibleMaxMessageCount + 1; i++) { + delayNode1.receive({payload:i}); + } + + setTimeout(function() { + try { + receivedMessagesStack.length.should.be.lessThan(possibleMaxMessageCount); + for(var j = 0; j < receivedMessagesStack.length; j++) { + if(receivedMessagesStack[j].payload === j) { + if(j === (receivedMessagesStack.length -1)) { // last message, all matched so far + done(); + } + } else { + should.fail(null, null, "Received messages were not received in order. Message was " + receivedMessagesStack[i].payload + " on count " + i); + } + } + } catch (err) { + done(err); + } + }, runtimeInMillis); + }); + } + + it('limits the message rate to 1 per second', function(done) { + genericRateLimitSECONDSTest(1, 1500, done); + }); + + it('limits the message rate to 2 per second, 2 seconds', function(done) { + this.timeout(6000); + genericRateLimitSECONDSTest(2, 2100, done); + }); + + /** + * Runs a rate limit test with drop support - only testing seconds! + * @param aLimit - the message limit count + * @param runtimeInMillis - when to terminate run and count messages received + */ + function dropRateLimitSECONDSTest(aLimit, runtimeInMillis, done) { + var flow = [{"id":"delayNode1","type":"delay","name":"delayNode","pauseType":"rate","timeout":5,"timeoutUnits":"seconds","rate":aLimit,"rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(delayNode, flow, function() { + var delayNode1 = helper.getNode("delayNode1"); + var helperNode1 = helper.getNode("helperNode1"); + var receivedMessagesStack = []; + + var rate = 1000/aLimit; + + var receiveTimestamp; + + helperNode1.on("input", function(msg) { + if(receiveTimestamp) { + var elapse = process.hrtime(receiveTimestamp); + var receiveInterval = (elapse[0] * 1000) + ((elapse[1] / nanosToSeconds) * 1000); + receiveInterval.should.be.above(rate * 0.9); + } + receiveTimestamp = process.hrtime(); + receivedMessagesStack.push(msg); + }); + + var possibleMaxMessageCount = Math.ceil(aLimit * (runtimeInMillis / 1000) + aLimit); // +aLimit as at the start of the 2nd period, we're allowing the 3rd burst + + var i = 0; + delayNode1.receive({payload:i}); + i++; + for(; i < possibleMaxMessageCount + 1; i++) { + setTimeout(function() { + delayNode1.receive({payload:i}); + }, 2 * ((rate * i) / possibleMaxMessageCount) ); + } + + //we need to send a message delayed so that it doesn't get dropped + setTimeout(function() { + delayNode1.receive({payload:++i}); + }, runtimeInMillis - 300); // should give enough time to squeeze another message in + + setTimeout(function() { + try { + receivedMessagesStack.length.should.be.lessThan(possibleMaxMessageCount + 1); + receivedMessagesStack.length.should.be.greaterThan(2); // ensure that we receive more than 1st and last message + receivedMessagesStack[0].payload.should.be.exactly(0); // means we received the last message injected just before test termination + var foundAtLeastOneDrop = false; + for(var i = 0; i < receivedMessagesStack.length; i++) { + if(i > 0) { + if(receivedMessagesStack[i].payload - receivedMessagesStack[i - 1].payload > 1) { + foundAtLeastOneDrop = true; + } + } + } + foundAtLeastOneDrop.should.be.true; + done(); + } catch (err) { + done(err); + } + }, runtimeInMillis); + }); + } + + it('limits the message rate to 1 per second, 4 seconds, with drop', function(done) { + this.timeout(6000); + dropRateLimitSECONDSTest(1, 4000, done); + }); + + it('limits the message rate to 2 per second, 5 seconds, with drop', function(done) { + this.timeout(6000); + dropRateLimitSECONDSTest(2, 5000, done); + }); + + /** + * Returns true if the actualTimeout is gracefully in between the timeoutFrom and timeoutTo + * values. Gracefully means that inBetween could actually mean smaller/greater values + * than the timeout range so long as it's within an actual grace percentage. + * @param timeoutFrom - The expected timeout range (low number) + * @param timeoutTo - The expected timeout range (high number) + * @param actualTimeout - The actual measured timeout value of test + * @param allowedGracePercent - The percentage of grace allowed + */ + function inBetweenDelays(timeoutFrom, timeoutTo, actualTimeout, allowedGracePercent) { + if(closeEnough(actualTimeout, timeoutFrom, allowedGracePercent)) { + return true; + } else if(closeEnough(actualTimeout, timeoutTo, allowedGracePercent)) { + return true; + } else if(timeoutFrom < actualTimeout && timeoutTo > actualTimeout) { + return true; + } else { + return false; + } + } + + /** + * Runs a RANDOM DELAY test, checks if the delay is in between the given timeout values + * @param aTimeoutFrom - the timeout quantity which is the minimal acceptable wait period + * @param aTimeoutTo - the timeout quantity which is the maximum acceptable wait period + * @param aTimeoutUnit - the unit of the timeout: milliseconds, seconds, minutes, hours, days + */ + function randomDelayTest(aTimeoutFrom, aTimeoutTo, aTimeoutUnit, done) { + var flow = [{"id":"delayNode1","type":"delay","name":"delayNode","pauseType":"random","timeout":5,"timeoutUnits":"seconds","rate":"1","rateUnits":"second","randomFirst":aTimeoutFrom,"randomLast":aTimeoutTo,"randomUnits":aTimeoutUnit,"drop":false,"wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(delayNode, flow, function() { + var delayNode1 = helper.getNode("delayNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + var endTime = process.hrtime(startTime); + var runtimeNanos = ( (endTime[0] * nanosToSeconds) + endTime[1] ); + var runtimeSeconds = runtimeNanos / nanosToSeconds; + var aTimeoutFromUnifiedToSeconds; + var aTimeoutToUnifiedToSeconds; + + // calculating the timeout in seconds + if(aTimeoutUnit == TimeUnitEnum.MILLIS) { + aTimeoutFromUnifiedToSeconds = aTimeoutFrom / millisToSeconds; + aTimeoutToUnifiedToSeconds = aTimeoutTo / millisToSeconds; + } else if(aTimeoutUnit == TimeUnitEnum.SECONDS) { + aTimeoutFromUnifiedToSeconds = aTimeoutFrom; + aTimeoutToUnifiedToSeconds = aTimeoutTo; + } else if(aTimeoutUnit == TimeUnitEnum.MINUTES) { + aTimeoutFromUnifiedToSeconds = aTimeoutFrom * secondsToMinutes; + aTimeoutToUnifiedToSeconds = aTimeoutTo * secondsToMinutes; + } else if(aTimeoutUnit == TimeUnitEnum.HOURS) { + aTimeoutFromUnifiedToSeconds = aTimeoutFrom * secondsToHours; + aTimeoutToUnifiedToSeconds = aTimeoutTo * secondsToHours; + } else if(aTimeoutUnit == TimeUnitEnum.DAYS) { + aTimeoutFromUnifiedToSeconds = aTimeoutFrom * secondsToDays; + aTimeoutToUnifiedToSeconds = aTimeoutTo * secondsToDays; + } + + if(inBetweenDelays(aTimeoutFromUnifiedToSeconds, aTimeoutToUnifiedToSeconds, runtimeSeconds, GRACE_PERCENTAGE)) { + done(); + } else { + try { + should.fail(null, null, "Delayed runtime seconds " + runtimeSeconds + " was not \"in between enough\" enough to expected values of: " + aTimeoutFromUnifiedToSeconds + " and " + aTimeoutToUnifiedToSeconds); + } catch (err) { + done(err); + } + } + } catch(err) { + done(err); + } + }); + var startTime = process.hrtime(); + delayNode1.receive({payload:"delayMe"}); + }); + } + + it('randomly delays the message in seconds', function(done) { + randomDelayTest(0.4, 0.8, "seconds", done); + }); + + it(' randomly delays the message in milliseconds', function(done) { + randomDelayTest(400, 800, "milliseconds", done); + }); + + it('randomly delays the message in minutes', function(done) { + randomDelayTest(0.0066, 0.0133, "minutes", done); + }); + + it('delays the message in hours', function(done) { + randomDelayTest(0.000111111, 0.000222222, "hours", done); + }); + + it('delays the message in days', function(done) { + randomDelayTest(0.0000046296, 0.0000092593, "days", done); + }); + + it('handles bursts using a buffer', function(done) { + this.timeout(6000); + + var flow = [{"id":"delayNode1","type":"delay","name":"delayNode","pauseType":"rate","timeout":5,"timeoutUnits":"seconds","rate":1000,"rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(delayNode, flow, function() { + var delayNode1 = helper.getNode("delayNode1"); + var helperNode1 = helper.getNode("helperNode1"); + + var sinon = require('sinon'); + + var receivedWarning = false; + var messageBurstSize = 1500; + + // we ensure that we note that a warning is received for buffer growth + sinon.stub(delayNode1, 'warn', function(warning){ + if(warning.indexOf("buffer exceeded 1000 messages" > -1)) { + receivedWarning = true; + } + }); + + // we ensure that the warning is received for buffer size and that we get the last message + helperNode1.on("input", function(msg) { + if(msg.payload === (messageBurstSize - 1) && receivedWarning === true) { + done(); // it will timeout if we don't receive the last message + } + }); + // send 1500 messages as quickly as possible + for(var i = 0; i < messageBurstSize; i++) { + delayNode1.receive({payload:i}); + } + }); + }); + +}); diff --git a/dgbuilder/test/nodes/core/core/90-comment_spec.js b/dgbuilder/test/nodes/core/core/90-comment_spec.js new file mode 100644 index 00000000..54563e9e --- /dev/null +++ b/dgbuilder/test/nodes/core/core/90-comment_spec.js @@ -0,0 +1,36 @@ +/** + * 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 commentNode = require("../../../../nodes/core/core/90-comment.js"); +var helper = require("../../helper.js"); + +describe('comment node', function() { + + afterEach(function() { + helper.unload(); + }); + + it('should be loaded', function(done) { + var flow = [{id:"n1", type:"comment", name: "comment" }]; + helper.load(commentNode, flow, function() { + var n1 = helper.getNode("n1"); + n1.should.have.property('name', 'comment'); + done(); + }); + }); + +}); diff --git a/dgbuilder/test/nodes/core/logic/10-switch_spec.js b/dgbuilder/test/nodes/core/logic/10-switch_spec.js new file mode 100644 index 00000000..d5a42805 --- /dev/null +++ b/dgbuilder/test/nodes/core/logic/10-switch_spec.js @@ -0,0 +1,367 @@ +/** + * 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 switchNode = require("../../../../nodes/core/logic/10-switch.js"); +var helper = require("../../helper.js"); + +describe('SwitchNode', function() { + + beforeEach(function(done) { + helper.startServer(done); + }); + + afterEach(function(done) { + helper.unload(); + helper.stopServer(done); + }); + + it('should be loaded', function(done) { + var flow = [{"id":"switchNode1","type":"switch","name":"switchNode","property":"payload","rules":[{"t":"eq","v":""}],"checkall":"true","outputs":1,"wires":[[]]}]; + helper.load(switchNode, flow, function() { + var switchNode1 = helper.getNode("switchNode1"); + switchNode1.should.have.property('name', 'switchNode'); + done(); + }); + }); + + /** + * Test a switch node where one argument is consumed by the rule (such as greater than). + * @param rule - the switch rule (see 10-switc.js) string we're using + * @param ruleWith - whatever the rule should be executed with (say greater than 5) + * @param aCheckall - whether the switch flow should have the checkall flag set to true/false + * @param shouldReceive - whether the helper node should receive a payload + * @param sendPayload - the payload message we're sending + * @param done - callback when done + */ + function genericSwitchTest(rule, ruleWith, aCheckall, shouldReceive, sendPayload, done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":rule,"v":ruleWith}],checkall:aCheckall,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + customFlowSwitchTest(flow, shouldReceive, sendPayload, done); + } + + /** + * Test a switch node where NO arguments are consumed by the rule (such as TRUE/FALSE) + * @param rule - the switch rule (see 10-switc.js) string we're using + * @param aCheckall - whether the switch flow should have the checkall flag set to true/false + * @param shouldReceive - whether the helper node should receive a payload + * @param sendPayload - the payload message we're sending + * @param done - callback when done + */ + function singularSwitchTest(rule, aCheckall, shouldReceive, sendPayload, done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":rule}],checkall:aCheckall,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + customFlowSwitchTest(flow, shouldReceive, sendPayload, done); + } + + /** + * Test a switch node where two arguments are consumed by the rule (such as between). + * @param rule - the switch rule (see 10-switc.js) string we're using + * @param ruleWith - whatever the rule should be executed with (say between 5...) + * @param ruleWith2 - whatever the rule should be executed with (say ...and 5) + * @param aCheckall - whether the switch flow should have the checkall flag set to true/false + * @param shouldReceive - whether the helper node should receive a payload + * @param sendPayload - the payload message we're sending + * @param done - callback when done + */ + function twoFieldSwitchTest(rule, ruleWith, ruleWith2, aCheckall, shouldReceive, sendPayload, done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":rule,"v":ruleWith,"v2":ruleWith2}],checkall:aCheckall,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + customFlowSwitchTest(flow, shouldReceive, sendPayload, done); + } + + /** + * Execute a switch test. Can specify whether the should node is expected to send a payload onwards to the helper node. + * The flow and the payload can be customised + * @param flow - the custom flow to be tested => must contain a switch node (switchNode1) wiring a helper node (helperNode1) + * @param shouldReceive - whether the helper node should receive a payload + * @param sendPayload - the payload message we're sending + * @param done - callback when done + */ + function customFlowSwitchTest(flow, shouldReceive, sendPayload, done) { + helper.load(switchNode, flow, function() { + var switchNode1 = helper.getNode("switchNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + if(shouldReceive === true) { + msg.payload.should.equal(sendPayload); + done(); + } else { + should.fail(null, null, "We should never get an input!"); + } + } catch(err) { + done(err); + } + }); + switchNode1.receive({payload:sendPayload}); + if(shouldReceive === false) { + setTimeout(function() { + done(); + }, 200); + } + }); + } + + it('should check if payload equals given value', function(done) { + genericSwitchTest("eq", "Hello", true, true, "Hello", done); + }); + + it('should return nothing when the payload doesn\'t equal to desired string', function(done) { + genericSwitchTest("eq", "Hello", true, false, "Hello!", done); + }); + + it('should check if payload NOT equals given value', function(done) { + genericSwitchTest("neq", "Hello", true, true, "HEllO", done); + }); + + it('should return nothing when the payload does equal to desired string', function(done) { + genericSwitchTest("neq", "Hello", true, false, "Hello", done); + }); + + it('should check if payload equals given numeric value', function(done) { + genericSwitchTest("eq", 3, true, true, 3, done); + }); + + it('should return nothing when the payload doesn\'t equal to desired numeric value', function(done) { + genericSwitchTest("eq", 2, true, false, 4, done); + }); + + it('should check if payload NOT equals given numeric value', function(done) { + genericSwitchTest("neq", 55667744, true, true, -1234, done); + }); + + it('should return nothing when the payload does equal to desired numeric value', function(done) { + genericSwitchTest("neq", 10, true, false, 10, done); + }); + + it('should check if payload is less than given value', function(done) { + genericSwitchTest("lt", 3, true, true, 2, done); + }); + + it('should return nothing when the payload is not less than desired string', function(done) { + genericSwitchTest("lt", 3, true, false, 4, done); + }); + + it('should check if payload less than equals given value', function(done) { + genericSwitchTest("lte", 3, true, true, 3, done); + }); + + it('should check if payload is greater than given value', function(done) { + genericSwitchTest("gt", 3, true, true, 6, done); + }); + + it('should return nothing when the payload is not greater than desired string', function(done) { + genericSwitchTest("gt", 3, true, false, -1, done); + }); + + it('should check if payload is greater than/equals given value', function(done) { + genericSwitchTest("gte", 3, true, true, 3, done); + }); + + it('should return nothing when the payload is not greater than desired string', function(done) { + genericSwitchTest("gt", 3, true, false, -1, done); + }); + + it('should check if payload is greater than/equals given value', function(done) { + genericSwitchTest("gte", 3, true, true, 3, done); + }); + + it('should check if payload is between given values', function(done) { + twoFieldSwitchTest("btwn", 3, 5, true, true, 4, done); + }); + + it('should check if payload is not between given values', function(done) { + twoFieldSwitchTest("btwn", 3, 5, true, false, 12, done); + }); + + it('should check if payload contains given value', function(done) { + genericSwitchTest("cont", "Hello", true, true, "Hello World!", done); + }); + + it('should return nothing when the payload doesn\'t contain desired string', function(done) { + genericSwitchTest("cont", "Hello", true, false, "This is not a greeting!", done); + }); + + it('should match regex', function(done) { + genericSwitchTest("regex", "[abc]+", true, true, "abbabac", done); + }); + + it('should return nothing when the payload doesn\'t match regex', function(done) { + genericSwitchTest("regex", "\\d+", true, false, "This is not a digit", done); + }); + + it('should return nothing when the payload doesn\'t contain desired string', function(done) { + genericSwitchTest("cont", "Hello", true, false, "This is not a greeting!", done); + }); + + it('should check if input is true', function(done) { + singularSwitchTest(true, true, true, true, done); + }); + + it('sends nothing when input is false and checking for true', function(done) { + singularSwitchTest(true, true, false, false, done); + }); + + it('should check if input is indeed false', function(done) { + singularSwitchTest(false, true, true, false, done); + }); + + it('sends nothing when input is false and checking for true', function(done) { + singularSwitchTest(false, true, false, true, done); + }); + + it('should check if input is indeed null', function(done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"null"}],checkall:true,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + + + helper.load(switchNode, flow, function() { + var switchNode1 = helper.getNode("switchNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + if(msg.payload) { + try { + should.fail(null, null, "msg.payload should be undefined!"); + } catch (err) { + done(err); + } + } else { + done(); + } + }); + switchNode1.receive({payload:undefined}); + }); + }); + + it('should check if input is indeed not null', function(done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"nnull"}],checkall:false,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + + + helper.load(switchNode, flow, function() { + var switchNode1 = helper.getNode("switchNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + if(msg.payload) { + done(); + } else { + try { + msg.payload.should.equal("Anything here"); + } catch (err) { + done(err); + } + } + }); + switchNode1.receive({payload:"Anything here"}); + }); + }); + + it('sends a message when the "else/otherwise" statement is selected' , function(done) { + singularSwitchTest("else", true, true, 123456, done); + }); + + it('handles more than one switch statement' , function(done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"eq","v":"Hello"},{"t":"cont","v":"ello"}, {"t":"else"}],checkall:true,outputs:3,wires:[["helperNode1"], ["helperNode2"], ["helperNode3"]]}, + {id:"helperNode1", type:"helper", wires:[]}, + {id:"helperNode2", type:"helper", wires:[]}, + {id:"helperNode3", type:"helper", wires:[]}]; + + + helper.load(switchNode, flow, function() { + var switchNode1 = helper.getNode("switchNode1"); + var helperNode1 = helper.getNode("helperNode1"); + var helperNode2 = helper.getNode("helperNode2"); + var helperNode3 = helper.getNode("helperNode3"); + + var nodeHitCount = 0; + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal("Hello"); + nodeHitCount++; + } catch (err) { + done(err); + } + }); + helperNode2.on("input", function(msg) { + try { + msg.payload.should.equal("Hello"); + nodeHitCount++; + if(nodeHitCount == 2) { + done(); + } else { + try { + should.fail(null, null, "Both statements should be triggered!"); + } catch (err) { + done(err); + } + } + } catch (err) { + done(err); + } + }); + helperNode3.on("input", function(msg) { + try { + should.fail(null, null, "The otherwise/else statement should not be triggered here!"); + } catch (err) { + done(err); + } + }); + switchNode1.receive({payload:"Hello"}); + }); + }); + + it('stops after first statement' , function(done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"eq","v":"Hello"},{"t":"cont","v":"ello"}, {"t":"else"}],checkall:"false",outputs:3,wires:[["helperNode1"], ["helperNode2"], ["helperNode3"]]}, + {id:"helperNode1", type:"helper", wires:[]}, + {id:"helperNode2", type:"helper", wires:[]}, + {id:"helperNode3", type:"helper", wires:[]}]; + + + helper.load(switchNode, flow, function() { + var switchNode1 = helper.getNode("switchNode1"); + var helperNode1 = helper.getNode("helperNode1"); + var helperNode2 = helper.getNode("helperNode2"); + var helperNode3 = helper.getNode("helperNode3"); + + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal("Hello"); + done(); + } catch (err) { + done(err); + } + }); + helperNode2.on("input", function(msg) { + try { + should.fail(null, null, "The otherwise/else statement should not be triggered here!"); + } catch (err) { + done(err); + } + }); + helperNode3.on("input", function(msg) { + try { + should.fail(null, null, "The otherwise/else statement should not be triggered here!"); + } catch (err) { + done(err); + } + }); + switchNode1.receive({payload:"Hello"}); + }); + }); + +}); diff --git a/dgbuilder/test/nodes/core/logic/15-change_spec.js b/dgbuilder/test/nodes/core/logic/15-change_spec.js new file mode 100644 index 00000000..5072dfce --- /dev/null +++ b/dgbuilder/test/nodes/core/logic/15-change_spec.js @@ -0,0 +1,194 @@ +/** + * 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 changeNode = require("../../../../nodes/core/logic/15-change.js"); +var helper = require("../../helper.js"); + +describe('ChangeNode', function() { + + beforeEach(function(done) { + helper.startServer(done); + }); + + afterEach(function(done) { + helper.unload(); + helper.stopServer(done); + }); + + it('should be loaded', function(done) { + var flow = [{"id":"changeNode1","type":"change","action":"replace","property":"payload","from":"","to":"","reg":false,"name":"changeNode","wires":[[]]}]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + changeNode1.should.have.property('name', 'changeNode'); + done(); + }); + }); + + it('sets the value of the message property', function(done) { + var flow = [{"id":"changeNode1","type":"change","action":"replace","property":"payload","from":"","to":"changed","reg":false,"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal("changed"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"changeMe"}); + }); + }); + + it('changes the value of the message property', function(done) { + var flow = [{"id":"changeNode1","type":"change","action":"change","property":"payload","from":"Hello","to":"Goodbye","reg":false,"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal("Goodbye World!"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"Hello World!"}); + }); + }); + + it('changes the value of the message property based on a regex', function(done) { + var flow = [{"id":"changeNode1","type":"change","action":"change","property":"payload","from":"\\d+","to":"NUMBER","reg":true,"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal("Replace all numbers NUMBER and NUMBER"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"Replace all numbers 12 and 14"}); + }); + }); + + it('supports regex groups', function(done) { + var flow = [{"id":"changeNode1","type":"change","action":"change","property":"payload","from":"(Hello)","to":"$1-$1-$1","reg":true,"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal("Hello-Hello-Hello World"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"Hello World"}); + }); + }); + + it('Reports invalid regex', function(done) { + + var sinon = require('sinon'); + + var flow = [{"id":"changeNode1","type":"change","action":"change","property":"payload","from":"\\+**+","to":"NUMBER","reg":true,"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + + sinon.stub(changeNode1, 'error', function(error) { + if(error.indexOf("regular expression" > -1)) { + done(); + } else { + try { + should.fail(null, null, "An error should be reported for an invalid regex"); + } catch (err) { + done(err); + } + } + }); + changeNode1.receive({payload:"This is irrelevant"}); + }); + }); + + it('deletes the value of the message property', function(done) { + var flow = [{"id":"changeNode1","type":"change","action":"delete","property":"payload","from":"","to":"","reg":false,"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.should.not.have.property('payload'); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"This won't get through!"}); + }); + }); + +// TODO confirm the behaviour of the change node later,apparently calling eval such that makeNew( msg, node.property.split("."), eval(node.to) ); is incorrect +// it('changes the property name of the message object', function(done) { +// var flow = [{"id":"changeNode1","type":"change","action":"replace","property":"payload","from":"","to":"msg.otherProp=10","reg":false,"name":"changeNode","wires":[["helperNode1"]]}, +// {id:"helperNode1", type:"helper", wires:[]}]; +// helper.load(changeNode, flow, function() { +// var changeNode1 = helper.getNode("changeNode1"); +// var helperNode1 = helper.getNode("helperNode1"); +// helperNode1.on("input", function(msg) { +// try { +// msg.otherProp.should.equal(10); +// done(); +// } catch(err) { +// done(err); +// } +// }); +// changeNode1.receive({payload:"changeMe"}); +// }); +// }); + + it('splits dot delimited properties into objects', function(done) { + var flow = [{"id":"changeNode1","type":"change","action":"replace","property":"pay.load","from":"","to":"10","reg":false,"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.pay.load.should.equal("10"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({"pay.load":"changeMe"}); + }); + }); +}); + diff --git a/dgbuilder/test/nodes/core/logic/16-range_spec.js b/dgbuilder/test/nodes/core/logic/16-range_spec.js new file mode 100644 index 00000000..abb6c646 --- /dev/null +++ b/dgbuilder/test/nodes/core/logic/16-range_spec.js @@ -0,0 +1,131 @@ +/** + * 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 rangeNode = require("../../../../nodes/core/logic/16-range.js"); +var helper = require("../../helper.js"); + +describe('RangeNode', function() { + + beforeEach(function(done) { + helper.startServer(done); + }); + + afterEach(function(done) { + helper.unload(); + helper.stopServer(done); + }); + + it('should be loaded', function(done) { + var flow = [{"id":"rangeNode1","type":"range","minin":"0","maxin":"99","minout":"100","maxout":"199","action":"range","round":true,"name":"rangeNode","wires":[[]]}]; + helper.load(rangeNode, flow, function() { + var rangeNode1 = helper.getNode("rangeNode1"); + rangeNode1.should.have.property('name', 'rangeNode'); + done(); + }); + }); + + /** + * Run a generic range test + * @param action - scale/clamp (range limit)/roll (modulo): what action to choose + * @param minin - map from minimum value + * @param maxin - map from maximum value + * @param minout - map to minimum value + * @param maxout - map to maximum value + * @param round - whether to round the result to the nearest integer + * @param aPayload - what payload to send to the range node + * @param expectedResult - what result we're expecting + * @param done - the callback to call when test done + */ + function genericRangeTest(action, minin, maxin, minout, maxout, round, aPayload, expectedResult, done) { + var flow = [{"id":"rangeNode1","type":"range","minin":minin,"maxin":maxin,"minout":minout,"maxout":maxout,"action":action,"round":round,"name":"rangeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(rangeNode, flow, function() { + var rangeNode1 = helper.getNode("rangeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal(expectedResult); + done(); + } catch(err) { + done(err); + } + }); + rangeNode1.receive({payload:aPayload}); + }); + } + + it('ranges numbers up tenfold', function(done) { + genericRangeTest("scale", 0, 100, 0, 1000, false, 50, 500, done); + }); + + it('ranges numbers down such as centimetres to metres', function(done) { + genericRangeTest("scale", 0, 100, 0, 1, false, 55, 0.55, done); + }); + + it('wraps numbers down say for degree/rotation reading 1/2', function(done) { + genericRangeTest("roll", 0, 10, 0, 360, true, 15, 180, done); // 1/2 around wrap => "one and a half turns" + }); + + it('wraps numbers around say for degree/rotation reading 1/3', function(done) { + genericRangeTest("roll", 0, 10, 0, 360, true, 13.3333, 120, done); // 1/3 around wrap => "one and a third turns" + }); + + it('wraps numbers around say for degree/rotation reading 1/4', function(done) { + genericRangeTest("roll", 0, 10, 0, 360, true, 12.5, 90, done); // 1/4 around wrap => "one and a quarter turns" + }); + + it('wraps numbers around say for degree/rotation reading 1/4', function(done) { + genericRangeTest("roll", 0, 10, 0, 360, true, 12.5, 90, done); // 1/4 around wrap => "one and a quarter turns" + }); + + it('wraps numbers down say for degree/rotation reading 1/4', function(done) { + genericRangeTest("roll", 0, 10, 0, 360, true, -12.5, 270, done); // 1/4 backwards wrap => "one and a quarter turns backwards" + }); + + it('clamps numbers within a range - over max', function(done) { + genericRangeTest("clamp", 0, 10, 0, 1000, false, 111, 1000, done); + }); + + it('clamps numbers within a range - below min', function(done) { + genericRangeTest("clamp", 0, 10, 0, 1000, false, -1, 0, done); + }); + + it('reports if input is not a number', function(done) { + var flow = [{"id":"rangeNode1","type":"range","minin":0,"maxin":0,"minout":0,"maxout":0,"action":"scale","round":true,"name":"rangeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(rangeNode, flow, function() { + var rangeNode1 = helper.getNode("rangeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + + var sinon = require('sinon'); + sinon.stub(rangeNode1, 'log', function(log) { + if(log.indexOf("Not a number") > -1) { + done(); + } else { + try { + should.fail(null, null, "Non-number inputs should be reported!"); + } catch (err) { + done(err); + } + } + }); + + rangeNode1.receive({payload:"NOT A NUMBER"}); + }); + }); +}); diff --git a/dgbuilder/test/nodes/core/parsers/70-HTML_spec.js b/dgbuilder/test/nodes/core/parsers/70-HTML_spec.js new file mode 100644 index 00000000..e7e4cbb5 --- /dev/null +++ b/dgbuilder/test/nodes/core/parsers/70-HTML_spec.js @@ -0,0 +1,211 @@ +/** + * 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 sinon = require("sinon"); +var path = require("path"); +var fs = require('fs-extra'); + +var htmlNode = require("../../../../nodes/core/parsers/70-HTML.js"); +var helper = require("../../helper.js"); + +describe('html node', function() { + + var resourcesDir = __dirname+ path.sep + ".." + path.sep + ".." + path.sep + ".." + path.sep + "resources" + path.sep; + var file = path.join(resourcesDir, "70-HTML-test-file.html"); + + before(function(done) { + helper.startServer(done); + }); + + beforeEach(function() { + fs.existsSync(file).should.be.true; + }); + + afterEach(function() { + helper.unload(); + }); + + it('should be loaded', function(done) { + var flow = [{id:"htmlNode1", type:"html", name: "htmlNode" }]; + helper.load(htmlNode, flow, function() { + var htmlNode1 = helper.getNode("htmlNode1"); + htmlNode1.should.have.property('name', 'htmlNode'); + done(); + }); + }); + + it('should retrieve header contents as default', function(done) { + fs.readFile(file, 'utf8', function(err, data) { + var flow = [{id:"n1",type:"html",wires:[["n2"]],func:"return msg;"}, + {id:"n2", type:"helper"}]; + + helper.load(htmlNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + should.equal(msg.payload, 'This is a test page for node 70-HTML'); + done(); + }); + n1.receive({payload:data,topic: "bar"}); + }); + }); + }); + + it('should retrieve paragraph contents when specified', function(done) { + fs.readFile(file, 'utf8', function(err, data) { + var flow = [{id:"n1",type:"html",wires:[["n2"]],ret:"text",tag:"p"}, + {id:"n2", type:"helper"}]; + + helper.load(htmlNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + should.equal(msg.payload, 'There\'s nothing to read here.'); + done(); + }); + n1.receive({payload:data,topic: "bar"}); + }); + }); + }); + + it('should retrieve list contents as an array of html as default', function(done) { + fs.readFile(file, 'utf8', function(err, data) { + var flow = [{id:"n1",type:"html",wires:[["n2"]],tag:"ol"}, + {id:"n2", type:"helper"}]; + + helper.load(htmlNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.payload[0].indexOf("<li>Blue</li>").should.be.above(-1); + msg.payload[0].indexOf("<li>Red</li>").should.be.above(-1); + done(); + }); + n1.receive({payload:data,topic: "bar"}); + }); + }); + }); + + it('should retrieve list contents as an array of text', function(done) { + fs.readFile(file, 'utf8', function(err, data) { + var flow = [{id:"n1",type:"html",wires:[["n2"]],tag:"ol",ret:"text"}, + {id:"n2", type:"helper"}]; + + helper.load(htmlNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.payload.should.be.instanceof(Array).and.have.lengthOf(1); + msg.payload[0].indexOf("Blue").should.be.above(-1); + msg.payload[0].indexOf("Red").should.be.above(-1); + done(); + }); + n1.receive({payload:data,topic: "bar"}); + }); + }); + }); + + it('should log on error', function(done) { + fs.readFile(file,function(err, data) { + var flow = [{id:"n1",type:"html",wires:[["n2"]],tag:"p"}, + {id:"n2", type:"helper"}]; + + helper.load(htmlNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n1.on("log", function(msg) { + msg.should.have.property('msg'); + msg.msg.indexOf("Error:").should.be.above(-1); + msg.msg.should.startWith("Error:"); + done(); + }); + n1.receive({payload:null,topic: "bar"}); + }); + }); + }); + + describe('multiple messages', function(){ + var cnt = 0; + + afterEach(function() { + cnt.should.be.exactly(2); + cnt = 0; + }); + + it('should retrieve list contents as html as default with output as multiple msgs ', function(done) { + fs.readFile(file, 'utf8', function(err, data) { + var flow = [{id:"n1",type:"html",wires:[["n2"]],tag:"ul",as:"multi"}, + {id:"n2", type:"helper"}]; + + helper.load(htmlNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + cnt++; + msg.should.have.property('topic', 'bar'); + if (cnt !== 1 && cnt !== 2) { + return false; + } + if (cnt === 1) { + msg.payload.indexOf("<li>Apple</li>").should.be.above(-1); + msg.payload.indexOf("<li>Pear</li>").should.be.above(-1); + } else if (cnt === 2) { + msg.payload.indexOf("<li>Potato</li>").should.be.above(-1); + msg.payload.indexOf("<li>Parsnip</li>").should.be.above(-1); + done(); + } + }); + n1.receive({payload:data,topic: "bar"}); + }); + }); + }); + + it('should retrieve list contents as text with output as multiple msgs ', function(done) { + fs.readFile(file, 'utf8', function(err, data) { + var flow = [{id:"n1",type:"html",wires:[["n2"]],tag:"ul",ret:"text",as:"multi"}, + {id:"n2", type:"helper"}]; + + helper.load(htmlNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + cnt++; + msg.should.have.property('topic', 'bar'); + if (cnt !== 1 && cnt !== 2) { + return false; + } + if (cnt === 1) { + msg.payload.indexOf("Apple").should.be.above(-1); + msg.payload.indexOf("Pear").should.be.above(-1); + } else if (cnt === 2) { + msg.payload.indexOf("Potato").should.be.above(-1); + msg.payload.indexOf("Parsnip").should.be.above(-1); + done(); + } + }); + n1.receive({payload:data,topic: "bar"}); + }); + }); + }); + + }); + +}); diff --git a/dgbuilder/test/nodes/core/parsers/70-JSON_spec.js b/dgbuilder/test/nodes/core/parsers/70-JSON_spec.js new file mode 100644 index 00000000..b2ebe719 --- /dev/null +++ b/dgbuilder/test/nodes/core/parsers/70-JSON_spec.js @@ -0,0 +1,104 @@ +/** + * 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 jsonNode = require("../../../../nodes/core/parsers/70-JSON.js"); +var helper = require("../../helper.js"); + +describe('JSON node', function() { + + before(function(done) { + helper.startServer(done); + }); + + afterEach(function() { + helper.unload(); + }); + + it('should be loaded', function(done) { + var flow = [{id:"jsonNode1", type:"json", name: "jsonNode" }]; + helper.load(jsonNode, flow, function() { + var jsonNode1 = helper.getNode("jsonNode1"); + jsonNode1.should.have.property('name', 'jsonNode'); + done(); + }); + }); + + it('should convert a valid json string to a javascript object', function(done) { + var flow = [{id:"jn1",type:"json",wires:[["jn2"]],func:"return msg;"}, + {id:"jn2", type:"helper"}]; + helper.load(jsonNode, flow, function() { + var jn1 = helper.getNode("jn1"); + var jn2 = helper.getNode("jn2"); + jn2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.payload.should.have.property('employees'); + msg.payload.employees[0].should.have.property('firstName', 'John'); + msg.payload.employees[0].should.have.property('lastName', 'Smith'); + done(); + }); + var jsonString = '{"employees":[{"firstName":"John", "lastName":"Smith"}]}'; + jn1.receive({payload:jsonString,topic: "bar"}); + }); + }); + + it('should convert a javascript object to a json string', function(done) { + var flow = [{id:"jn1",type:"json",wires:[["jn2"]],func:"return msg;"}, + {id:"jn2", type:"helper"}]; + helper.load(jsonNode, flow, function() { + var jn1 = helper.getNode("jn1"); + var jn2 = helper.getNode("jn2"); + jn2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + should.equal(msg.payload, '{"employees":[{"firstName":"John","lastName":"Smith"}]}'); + done(); + }); + var obj = {employees:[{firstName:"John", lastName:"Smith"}]}; + jn1.receive({payload:obj,topic: "bar"}); + }); + }); + + it('should log an error if asked to parse an invalid json string', function(done) { + var flow = [{id:"jn1",type:"json",wires:[["jn2"]],func:"return msg;"}, + {id:"jn2", type:"helper"}]; + helper.load(jsonNode, flow, function() { + var jn1 = helper.getNode("jn1"); + var jn2 = helper.getNode("jn2"); + jn1.on("log", function(msg) { + msg.should.have.property('msg'); + should.deepEqual("SyntaxError: Unexpected token o"+ "\nfoo", msg.msg); + done(); + }); + jn1.receive({payload:'foo',topic: "bar"}); + }); + }); + + it('should log an error if asked to parse something thats not json or js', function(done) { + var flow = [{id:"jn1",type:"json",wires:[["jn2"]],func:"return msg;"}, + {id:"jn2", type:"helper"}]; + helper.load(jsonNode, flow, function() { + var jn1 = helper.getNode("jn1"); + var jn2 = helper.getNode("jn2"); + jn1.on("log", function(msg) { + msg.should.have.property('msg'); + should.deepEqual("dropped: 1", msg.msg); + done(); + }); + jn1.receive({payload:1,topic: "bar"}); + }); + }); + +}); diff --git a/dgbuilder/test/nodes/core/parsers/70-XML_spec.js b/dgbuilder/test/nodes/core/parsers/70-XML_spec.js new file mode 100644 index 00000000..8bf269d1 --- /dev/null +++ b/dgbuilder/test/nodes/core/parsers/70-XML_spec.js @@ -0,0 +1,107 @@ +/** + * 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 xmlNode = require("../../../../nodes/core/parsers/70-XML.js"); +var helper = require("../../helper.js"); + +describe('XML node', function() { + + before(function(done) { + helper.startServer(done); + }); + + afterEach(function() { + helper.unload(); + }); + + it('should be loaded', function(done) { + var flow = [{id:"xmlNode1", type:"xml", name: "xmlNode" }]; + helper.load(xmlNode, flow, function() { + var xmlNode1 = helper.getNode("xmlNode1"); + xmlNode1.should.have.property('name', 'xmlNode'); + done(); + }); + }); + + it('should convert a valid xml string to a javascript object', function(done) { + var flow = [{id:"n1",type:"xml",wires:[["n2"]],func:"return msg;"}, + {id:"n2", type:"helper"}]; + helper.load(xmlNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.payload.should.have.property('employees'); + msg.payload.employees.should.have.property('firstName'); + should.equal(msg.payload.employees.firstName[0], 'John'); + msg.payload.employees.should.have.property('lastName'); + should.equal(msg.payload.employees.lastName[0], 'Smith'); + done(); + }); + var string = '<employees><firstName>John</firstName><lastName>Smith</lastName></employees>'; + n1.receive({payload:string,topic: "bar"}); + }); + }); + + it('should convert a javascript object to an xml string', function(done) { + var flow = [{id:"n1",type:"xml",wires:[["n2"]],func:"return msg;"}, + {id:"n2", type:"helper"}]; + helper.load(xmlNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + console.log(msg.payload); + var index = msg.payload.indexOf('<employees><firstName>John</firstName><lastName>Smith</lastName></employees>'); + index.should.be.above(-1); + done(); + }); + var obj = {"employees":{"firstName":["John"],"lastName":["Smith"] }}; + n1.receive({payload:obj,topic: "bar"}); + }); + }); + + it('should log an error if asked to parse an invalid xml string', function(done) { + var flow = [{id:"n1",type:"xml",wires:[["n2"]],func:"return msg;"}, + {id:"n2", type:"helper"}]; + helper.load(xmlNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n1.on("log", function(msg) { + should.deepEqual("error", msg.level); + done(); + }); + n1.receive({payload:'<not valid xml>',topic: "bar"}); + }); + }); + + it('should log an error if asked to parse something thats not xml or js', function(done) { + var flow = [{id:"n1",type:"xml",wires:[["n2"]],func:"return msg;"}, + {id:"n2", type:"helper"}]; + helper.load(xmlNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n1.on("log", function(msg) { + msg.should.have.property('msg'); + should.deepEqual("This node only handles xml strings or js objects.", msg.msg); + done(); + }); + n1.receive({payload:1,topic: "bar"}); + }); + }); + +}); diff --git a/dgbuilder/test/nodes/core/storage/28-tail_spec.js b/dgbuilder/test/nodes/core/storage/28-tail_spec.js new file mode 100644 index 00000000..06124576 --- /dev/null +++ b/dgbuilder/test/nodes/core/storage/28-tail_spec.js @@ -0,0 +1,165 @@ +/** + * 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 path = require('path'); +var fs = require('fs-extra'); +var mkdirp = require('mkdirp'); + +var tailNode = require("../../../../nodes/core/storage/28-tail.js"); +var helper = require("../../helper.js"); + +describe('TailNode', function() { + + var resourcesDir = path.join(__dirname,"..","..","..","resources"); + var fileToTail = path.join(resourcesDir,"28-tail-test-file.txt"); + + beforeEach(function(done) { + fs.writeFileSync(fileToTail, "Tail message line 1\nTail message line 2\n"); + helper.startServer(done); + }); + + afterEach(function(done) { + helper.unload().then(function() { + fs.unlinkSync(fileToTail); + helper.stopServer(done); + }); + }); + + it('should be loaded', function(done) { + var flow = [{id:"tailNode1", type:"tail", name: "tailNode", "split":true, "filename":fileToTail}]; + helper.load(tailNode, flow, function() { + var tailNode1 = helper.getNode("tailNode1"); + tailNode1.should.have.property('name', 'tailNode'); + done(); + }); + }); + + it('tail should tail a file', function(done) { + var flow = [{id:"tailNode1", type:"tail", name: "tailNode", "split":true, "filename":fileToTail, "wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(tailNode, flow, function() { + var tailNode1 = helper.getNode("tailNode1"); + var helperNode1 = helper.getNode("helperNode1"); + var inputCounter = 0; + helperNode1.on("input", function(msg) { + //console.log(msg); + msg.should.have.property('topic', fileToTail); + msg.payload.should.equal("Tail message line " + (++inputCounter + 2)); + if (inputCounter === 2) { + done(); + } + }); + setTimeout( function() { + fs.appendFileSync(fileToTail, "Tail message line 3\n"); + fs.appendFileSync(fileToTail, "Tail message line 4\n"); + },100); + }); + }); + + it('tail should work in non-split mode', function(done) { + var flow = [{id:"tailNode1", type:"tail", name: "tailNode", "split":false, "filename":fileToTail, "wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(tailNode, flow, function() { + var tailNode1 = helper.getNode("tailNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + //console.log(msg); + msg.should.have.property('topic', fileToTail); + msg.payload.should.equal("Tail message line 5\nTail message line 6\n"); + done(); + }); + setTimeout( function() { + fs.appendFileSync(fileToTail, "Tail message line 5\nTail message line 6\n"); + },150); + }); + }); + + it('tail should handle a non-existent file', function(done) { + fs.unlinkSync(fileToTail); + var flow = [{id:"tailNode1", type:"tail", name: "tailNode", "split":true, "filename":fileToTail, "wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(tailNode, flow, function() { + var tailNode1 = helper.getNode("tailNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + msg.should.have.property('topic', fileToTail); + msg.payload.should.equal("Tail message line"); + done(); + }); + setTimeout( function() { + fs.writeFileSync(fileToTail, "Tail message line\n"); + },150); + }); + }); + /* + it('tail should handle file truncation', function(done) { + var flow = [{id:"tailNode1", type:"tail", name: "tailNode", "split":true, "filename":fileToTail, "wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(tailNode, flow, function() { + var tailNode1 = helper.getNode("tailNode1"); + var helperNode1 = helper.getNode("helperNode1"); + var inputCounter = 0; + var warned = false; + tailNode1.on("log", function(msg) { + if (msg.level == "warn") { warned = true; } + }); + helperNode1.on("input", function(msg) { + console.log("inputCounter =",inputCounter); + console.log(msg); + msg.should.have.property('topic', fileToTail); + inputCounter++; + if (inputCounter === 1) { + warned.should.be.false; + msg.payload.should.equal("Tail message line append"); + } else if (inputCounter === 2) { + msg.payload.should.equal("Tail message line truncate"); + } else { + msg.payload.should.equal("Tail message line append "+inputCounter); + } + + if (inputCounter === 5) { + setTimeout(function() { + warned.should.be.true; + done(); + },100); + } + }); + var actions = [ + function() { fs.appendFileSync(fileToTail, "Tail message line append\n");}, + function() { fs.writeFileSync(fileToTail, "Tail message line truncate\n");}, + function() { fs.appendFileSync(fileToTail, "Tail message line append 3\n");}, + function() { fs.appendFileSync(fileToTail, "Tail message line append 4\n");}, + function() { fs.appendFileSync(fileToTail, "Tail message line append 5\n");} + ]; + + function processAction() { + var action = actions.shift(); + action(); + if (actions.length > 0) { + setTimeout(function() { + processAction(); + },250); + } + } + setTimeout( function() { + processAction(); + },150); + }); + }); + */ + +}); diff --git a/dgbuilder/test/nodes/helper.js b/dgbuilder/test/nodes/helper.js new file mode 100644 index 00000000..9b371ca4 --- /dev/null +++ b/dgbuilder/test/nodes/helper.js @@ -0,0 +1,128 @@ +/** + * 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 when = require("when"); +var request = require('supertest'); +var nock; +if (!process.version.match(/^v0\.[0-9]\./)) { + // only set nock for node >= 0.10 + try { + nock = require('nock'); + } catch (err) { + // nevermind, will skip nock tests + nock = null; + } +} +var RED = require("../../red/red.js"); +var redNodes = require("../../red/nodes"); +var flows = require("../../red/nodes/flows"); +var credentials = require("../../red/nodes/credentials"); +var comms = require("../../red/comms.js"); + +var http = require('http'); +var express = require('express'); +var app = express(); + +var address = '127.0.0.1'; +var listenPort = 0; // use ephemeral port +var port; +var url; + +var server; + +function helperNode(n) { + RED.nodes.createNode(this, n); +} + +module.exports = { + load: function(testNode, testFlows, testCredentials, cb) { + if (typeof testCredentials === 'function') { + cb = testCredentials; + testCredentials = {}; + } + + var storage = { + getFlows: function() { + var defer = when.defer(); + defer.resolve(testFlows); + return defer.promise; + }, + getCredentials: function() { + var defer = when.defer(); + defer.resolve(testCredentials); + return defer.promise; + }, + saveCredentials: function() { + // do nothing + }, + }; + var settings = { + available: function() { return false; } + }; + + redNodes.init(settings, storage); + credentials.init(storage); + RED.nodes.registerType("helper", helperNode); + testNode(RED); + flows.load().then(function() { + should.deepEqual(testFlows, flows.getFlows()); + cb(); + }); + }, + unload: function() { + // TODO: any other state to remove between tests? + redNodes.clearRegistry(); + return flows.stopFlows(); + }, + + getNode: function(id) { + return flows.get(id); + }, + + credentials: credentials, + + clearFlows: function() { + return flows.clear(); + }, + + request: function() { + return request(RED.httpAdmin); + }, + + startServer: function(done) { + server = http.createServer(function(req,res){app(req,res);}); + RED.init(server, {}); + server.listen(listenPort, address); + server.on('listening', function() { + port = server.address().port; + url = 'http://' + address + ':' + port; + comms.start(); + done(); + }); + }, + //TODO consider saving TCP handshake/server reinit on start/stop/start sequences + stopServer: function(done) { + if(server) { + server.close(done); + } + }, + + url: function() { return url; }, + + nock: nock, + +}; diff --git a/dgbuilder/test/red/cli/lib/config_spec.js b/dgbuilder/test/red/cli/lib/config_spec.js new file mode 100644 index 00000000..68e960af --- /dev/null +++ b/dgbuilder/test/red/cli/lib/config_spec.js @@ -0,0 +1,53 @@ +/** + * 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 sinon = require("sinon"); +var fs = require("fs"); + +var config = require("../../../../red/cli/lib/config"); + +describe("cli config", function() { + afterEach(function() { + config.unload(); + }); + it('loads preferences when target referenced', sinon.test(function() { + this.stub(fs,"readFileSync",function() { + return '{"target":"http://example.com:1880"}' + }); + config.target.should.eql("http://example.com:1880"); + })); + it('provide default value for target', sinon.test(function() { + this.stub(fs,"readFileSync",function() { + return '{}' + }); + config.target.should.eql("http://localhost:1880"); + })); + it('saves preferences when target set', sinon.test(function() { + this.stub(fs,"readFileSync",function() { + return '{"target":"http://another.example.com:1880"}' + }); + this.stub(fs,"writeFileSync",function() {}); + + config.target.should.eql("http://another.example.com:1880"); + config.target = "http://final.example.com:1880"; + + fs.readFileSync.calledOnce.should.be.true; + fs.writeFileSync.calledOnce.should.be.true; + + })); + +});
\ No newline at end of file diff --git a/dgbuilder/test/red/cli/lib/request_spec.js b/dgbuilder/test/red/cli/lib/request_spec.js new file mode 100644 index 00000000..7d2b5ac7 --- /dev/null +++ b/dgbuilder/test/red/cli/lib/request_spec.js @@ -0,0 +1,46 @@ +/** + * 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 sinon = require("sinon"); +var fs = require("fs"); +var request = require("request"); + +var apiRequest = require("../../../../red/cli/lib/request"); +var config = require("../../../../red/cli/lib/config"); + +describe("cli request", function() { + var sandbox = sinon.sandbox.create(); + before(function() { + sandbox.stub(fs,"readFileSync",function() { + return '{"target":"http://example.com:1880"}' + }); + }); + after(function() { + sandbox.restore(); + }); + + it('returns the json response to a get', sinon.test(function(done) { + this.stub(request, 'get').yields(null, {statusCode:200}, JSON.stringify({a: "b"})); + + apiRequest("/foo",{}).then(function(res) { + res.should.eql({a:"b"}); + done(); + }).otherwise(function(err) { + done(err); + }); + })); +});
\ No newline at end of file diff --git a/dgbuilder/test/red/cli/nr-cli_spec.js b/dgbuilder/test/red/cli/nr-cli_spec.js new file mode 100644 index 00000000..59a5c641 --- /dev/null +++ b/dgbuilder/test/red/cli/nr-cli_spec.js @@ -0,0 +1,15 @@ +/** + * 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. + **/
\ No newline at end of file diff --git a/dgbuilder/test/red/comms_spec.js b/dgbuilder/test/red/comms_spec.js new file mode 100644 index 00000000..dce4d83a --- /dev/null +++ b/dgbuilder/test/red/comms_spec.js @@ -0,0 +1,189 @@ +/** + * 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 http = require('http'); +var express = require('express'); +var app = express(); +var WebSocket = require('ws'); + +var comms = require("../../red/comms.js"); +var address = '127.0.0.1'; +var listenPort = 0; // use ephemeral port + +describe("comms", function() { + describe("with default keepalive", function() { + var server; + var url; + var port; + before(function(done) { + server = http.createServer(function(req,res){app(req,res)}); + comms.init(server, {}); + server.listen(listenPort, address); + server.on('listening', function() { + port = server.address().port; + url = 'http://' + address + ':' + port + '/comms'; + comms.start(); + done(); + }); + }); + + after(function() { + comms.stop(); + }); + + it('accepts connection', function(done) { + var ws = new WebSocket(url); + ws.on('open', function() { + ws.close(); + done(); + }); + }); + + it('publishes message after subscription', function(done) { + var ws = new WebSocket(url); + ws.on('open', function() { + ws.send('{"subscribe":"topic1"}'); + comms.publish('topic1', 'foo'); + }); + ws.on('message', function(msg) { + msg.should.equal('{"topic":"topic1","data":"foo"}'); + ws.close(); + done(); + }); + }); + + it('publishes retained message for subscription', function(done) { + comms.publish('topic2', 'bar', true); + var ws = new WebSocket(url); + ws.on('open', function() { + ws.send('{"subscribe":"topic2"}'); + }); + ws.on('message', function(msg) { + msg.should.equal('{"topic":"topic2","data":"bar"}'); + ws.close(); + done(); + }); + }); + + it('retained message is deleted by non-retained message', function(done) { + comms.publish('topic3', 'retained', true); + comms.publish('topic3', 'non-retained'); + var ws = new WebSocket(url); + ws.on('open', function() { + ws.send('{"subscribe":"topic3"}'); + comms.publish('topic3', 'new'); + }); + ws.on('message', function(msg) { + msg.should.equal('{"topic":"topic3","data":"new"}'); + ws.close(); + done(); + }); + }); + + it('malformed messages are ignored',function(done) { + var ws = new WebSocket(url); + ws.on('open', function() { + ws.send('not json'); + ws.send('[]'); + ws.send('{"subscribe":"topic3"}'); + comms.publish('topic3', 'correct'); + }); + ws.on('message', function(msg) { + msg.should.equal('{"topic":"topic3","data":"correct"}'); + ws.close(); + done(); + }); + }); + + // The following test currently fails due to minimum viable + // implementation. More test should be written to test topic + // matching once this one is passing + + if (0) { + it('receives message on correct topic', function(done) { + var ws = new WebSocket(url); + ws.on('open', function() { + ws.send('{"subscribe":"topic4"}'); + comms.publish('topic5', 'foo'); + comms.publish('topic4', 'bar'); + }); + ws.on('message', function(msg) { + msg.should.equal('{"topic":"topic4","data":"bar"}'); + ws.close(); + done(); + }); + }); + } + }); + + describe("keep alives", function() { + var server; + var url; + var port; + before(function(done) { + server = http.createServer(function(req,res){app(req,res)}); + comms.init(server, {webSocketKeepAliveTime: 100}); + server.listen(listenPort, address); + server.on('listening', function() { + port = server.address().port; + url = 'http://' + address + ':' + port + '/comms'; + comms.start(); + done(); + }); + }); + after(function() { + comms.stop(); + }); + it('are sent', function(done) { + var ws = new WebSocket(url); + var count = 0; + ws.on('message', function(data) { + var msg = JSON.parse(data); + msg.should.have.property('topic','hb'); + msg.should.have.property('data').be.a.Number; + count++; + if (count == 3) { + ws.close(); + done(); + } + }); + }); + it('are not sent if other messages are sent', function(done) { + var ws = new WebSocket(url); + var count = 0; + var interval; + ws.on('open', function() { + ws.send('{"subscribe":"foo"}'); + interval = setInterval(function() { + comms.publish('foo', 'bar'); + }, 50); + }); + ws.on('message', function(data) { + var msg = JSON.parse(data); + msg.should.have.property('topic', 'foo'); + msg.should.have.property('data', 'bar'); + count++; + if (count == 5) { + clearInterval(interval); + ws.close(); + done(); + } + }); + }); + }); + +}); diff --git a/dgbuilder/test/red/events_spec.js b/dgbuilder/test/red/events_spec.js new file mode 100644 index 00000000..24759262 --- /dev/null +++ b/dgbuilder/test/red/events_spec.js @@ -0,0 +1,22 @@ +/** + * 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"); + +describe("red/events", function() { + it('can be required without errors', function() { + require("../../red/events"); + }); +}); diff --git a/dgbuilder/test/red/library_spec.js b/dgbuilder/test/red/library_spec.js new file mode 100644 index 00000000..52255287 --- /dev/null +++ b/dgbuilder/test/red/library_spec.js @@ -0,0 +1,237 @@ +/** + * 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 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"); + +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(); }); + }); + }); + }); + + 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() { + it('returns empty result', function(done) { + request(RED.httpAdmin) + .get('/library/flows') + .expect(200) + .end(function(err,res) { + if (err) { + throw err; + } + res.body.should.not.have.property('f'); + done(); + }); + }); + + it('returns 404 for non-existent entry', function(done) { + request(RED.httpAdmin) + .get('/library/flows/foo') + .expect(404) + .end(done); + }); + + it('can store and retrieve item', function(done) { + var flow = '[]'; + request(RED.httpAdmin) + .post('/library/flows/foo') + .set('Content-Type', 'text/plain') + .send(flow) + .expect(204).end(function (err, res) { + if (err) { + throw err; + } + request(RED.httpAdmin) + .get('/library/flows/foo') + .expect(200) + .end(function(err,res) { + if (err) { + throw err; + } + res.text.should.equal(flow); + done(); + }); + }); + }); + + 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(); + }); + }); + }); + + it('returns 403 for malicious access attempt', function(done) { + // 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) + .get('/library/flows/../../../../../package') + .expect(403) + .end(done); + }); + + it('returns 403 for malicious access attempt', function(done) { + // 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) + .post('/library/flows/../../../../../package') + .expect(403) + .end(done); + }); + + }); + + describe("type", function() { + before(function() { + RED.library.register('test'); + }); + + it('returns empty result', function(done) { + request(RED.httpAdmin) + .get('/library/test') + .expect(200) + .end(function(err,res) { + if (err) { + throw err; + } + res.body.should.not.have.property('f'); + done(); + }); + }); + + it('returns 404 for non-existent entry', function(done) { + request(RED.httpAdmin) + .get('/library/test/foo') + .expect(404) + .end(done); + }); + + it('can store and retrieve item', function(done) { + var flow = '[]'; + request(RED.httpAdmin) + .post('/library/test/foo') + .set('Content-Type', 'text/plain') + .send(flow) + .expect(204).end(function (err, res) { + if (err) { + throw err; + } + request(RED.httpAdmin) + .get('/library/test/foo') + .expect(200) + .end(function(err,res) { + if (err) { + throw err; + } + res.text.should.equal(flow); + done(); + }); + }); + }); + + 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(); + }); + }); + }); + + + it('returns 403 for malicious access attempt', function(done) { + request(RED.httpAdmin) + .get('/library/test/../../../../../../../../../../etc/passwd') + .expect(403) + .end(done); + }); + + it('returns 403 for malicious access attempt', function(done) { + request(RED.httpAdmin) + .get('/library/test/..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\etc\\passwd') + .expect(403) + .end(done); + }); + + it('returns 403 for malicious access attempt', function(done) { + request(RED.httpAdmin) + .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/dgbuilder/test/red/log_spec.js b/dgbuilder/test/red/log_spec.js new file mode 100644 index 00000000..0fb0aafd --- /dev/null +++ b/dgbuilder/test/red/log_spec.js @@ -0,0 +1,22 @@ +/** + * 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"); + +describe("red/log", function() { + it('can be required without errors', function() { + require("../../red/log"); + }); +}); diff --git a/dgbuilder/test/red/nodes/Node_spec.js b/dgbuilder/test/red/nodes/Node_spec.js new file mode 100644 index 00000000..6ac54bd2 --- /dev/null +++ b/dgbuilder/test/red/nodes/Node_spec.js @@ -0,0 +1,297 @@ +/** + * 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 sinon = require('sinon'); +var RedNode = require("../../../red/nodes/Node"); +var comms = require('../../../red/comms'); + +describe('Node', function() { + describe('#constructor',function() { + it('is called with an id and a type',function() { + var n = new RedNode({id:'123',type:'abc'}); + n.should.have.property('id','123'); + n.should.have.property('type','abc'); + n.should.not.have.property('name'); + n.wires.should.be.empty; + }); + + it('is called with an id, a type and a name',function() { + var n = new RedNode({id:'123',type:'abc',name:'barney'}); + n.should.have.property('id','123'); + n.should.have.property('type','abc'); + n.should.have.property('name','barney'); + n.wires.should.be.empty; + }); + + it('is called with an id, a type and some wires',function() { + var n = new RedNode({id:'123',type:'abc',wires:['123','456']}); + n.should.have.property('id','123'); + n.should.have.property('type','abc'); + n.should.not.have.property('name'); + n.wires.should.have.length(2); + }); + + }); + + describe('#close', function() { + it('emits close event when closed',function(done) { + var n = new RedNode({id:'123',type:'abc'}); + n.on('close',function() { + done(); + }); + var p = n.close(); + should.not.exist(p); + }); + + it('returns a promise when provided a callback with a done parameter',function(testdone) { + var n = new RedNode({id:'123',type:'abc'}); + n.on('close',function(done) { + setTimeout(function() { + done(); + },200); + }); + var p = n.close(); + should.exist(p); + p.then(function() { + testdone(); + }); + }); + }); + + + describe('#receive', function() { + it('emits input event when called', function(done) { + var n = new RedNode({id:'123',type:'abc'}); + var message = {payload:"hello world"}; + n.on('input',function(msg) { + should.deepEqual(msg,message); + done(); + }); + n.receive(message); + }); + }); + + describe('#send', function() { + + it('emits a single message', function(done) { + var n1 = new RedNode({id:'n1',type:'abc',wires:[['n2']]}); + var n2 = new RedNode({id:'n2',type:'abc'}); + var message = {payload:"hello world"}; + + n2.on('input',function(msg) { + // msg equals message, but is a new copy + should.deepEqual(msg,message); + should.notStrictEqual(msg,message); + done(); + }); + + n1.send(message); + }); + + it('emits multiple messages on a single output', function(done) { + var n1 = new RedNode({id:'n1',type:'abc',wires:[['n2']]}); + var n2 = new RedNode({id:'n2',type:'abc'}); + + var messages = [ + {payload:"hello world"}, + {payload:"hello world again"} + ]; + + var rcvdCount = 0; + + n2.on('input',function(msg) { + should.deepEqual(msg,messages[rcvdCount]); + should.notStrictEqual(msg,messages[rcvdCount]); + rcvdCount += 1; + if (rcvdCount == 2) { + done(); + } + }); + n1.send([messages]); + }); + + it('emits messages to multiple outputs', function(done) { + var n1 = new RedNode({id:'n1',type:'abc',wires:[['n2'],['n3'],['n4','n5']]}); + var n2 = new RedNode({id:'n2',type:'abc'}); + var n3 = new RedNode({id:'n3',type:'abc'}); + var n4 = new RedNode({id:'n4',type:'abc'}); + var n5 = new RedNode({id:'n5',type:'abc'}); + + var messages = [ + {payload:"hello world"}, + null, + {payload:"hello world again"} + ]; + + var rcvdCount = 0; + + n2.on('input',function(msg) { + should.deepEqual(msg,messages[0]); + should.notStrictEqual(msg,messages[0]); + rcvdCount += 1; + if (rcvdCount == 3) { + done(); + } + }); + + n3.on('input',function(msg) { + should.fail(null,null,"unexpected message"); + }); + + n4.on('input',function(msg) { + should.deepEqual(msg,messages[2]); + should.notStrictEqual(msg,messages[2]); + rcvdCount += 1; + if (rcvdCount == 3) { + done(); + } + }); + + n5.on('input',function(msg) { + should.deepEqual(msg,messages[2]); + should.notStrictEqual(msg,messages[2]); + rcvdCount += 1; + if (rcvdCount == 3) { + done(); + } + }); + + n1.send(messages); + }); + + it('emits no messages', function(done) { + var n1 = new RedNode({id:'n1',type:'abc',wires:[['n2']]}); + var n2 = new RedNode({id:'n2',type:'abc'}); + + n2.on('input',function(msg) { + should.fail(null,null,"unexpected message"); + }); + + setTimeout(function() { + done(); + }, 200); + + n1.send(); + }); + + it('emits messages ignoring non-existent nodes', function(done) { + var n1 = new RedNode({id:'n1',type:'abc',wires:[['n9'],['n2']]}); + var n2 = new RedNode({id:'n2',type:'abc'}); + + var messages = [ + {payload:"hello world"}, + {payload:"hello world again"} + ]; + + n2.on('input',function(msg) { + should.deepEqual(msg,messages[1]); + should.notStrictEqual(msg,messages[1]); + done(); + }); + + n1.send(messages); + }); + + it('emits messages without cloning req or res', function(done) { + var n1 = new RedNode({id:'n1',type:'abc',wires:[['n2']]}); + var n2 = new RedNode({id:'n2',type:'abc'}); + + var req = {}; + var res = {}; + var cloned = {}; + var message = {payload: "foo", cloned: cloned, req: req, res: res}; + + n2.on('input',function(msg) { + should.deepEqual(msg, message); + msg.cloned.should.not.be.exactly(message.cloned); + msg.req.should.be.exactly(message.req); + msg.res.should.be.exactly(message.res); + done(); + }); + + n1.send(message); + }); + + }); + + describe('#log', function() { + it('emits a log message', function(done) { + var n = new RedNode({id:'123',type:'abc'}); + n.on('log',function(obj) { + should.deepEqual({level:"log", id:n.id, + type:n.type, msg:"a log message"}, obj); + done(); + }); + n.log("a log message"); + }); + }); + + describe('#log', function() { + it('emits a log message with a name', function(done) { + var n = new RedNode({id:'123', type:'abc', name:"barney"}); + n.on('log',function(obj) { + should.deepEqual({level:"log", id:n.id, name: "barney", + type:n.type, msg:"a log message"}, obj); + done(); + }); + n.log("a log message"); + }); + }); + + describe('#warn', function() { + it('emits a warning', function(done) { + var n = new RedNode({id:'123',type:'abc'}); + n.on('log',function(obj) { + should.deepEqual({level:"warn", id:n.id, + type:n.type, msg:"a warning"}, obj); + done(); + }); + n.warn("a warning"); + }); + }); + + describe('#error', function() { + it('emits an error message', function(done) { + var n = new RedNode({id:'123',type:'abc'}); + n.on('log',function(obj) { + should.deepEqual({level:"error", id:n.id, + type:n.type, msg:"an error message"}, obj); + done(); + }); + n.error("an error message"); + }); + }); + + describe('#status', function() { + after(function() { + comms.publish.restore(); + }); + it('publishes status', function(done) { + var n = new RedNode({id:'123',type:'abc'}); + var status = {fill:"green",shape:"dot",text:"connected"}; + sinon.stub(comms, 'publish', function(topic, message, retain) { + topic.should.equal('status/123'); + message.should.equal(status); + retain.should.be.true; + done(); + }); + + n.status(status); + }); + }); + +}); diff --git a/dgbuilder/test/red/nodes/credentials_spec.js b/dgbuilder/test/red/nodes/credentials_spec.js new file mode 100644 index 00000000..3d10461f --- /dev/null +++ b/dgbuilder/test/red/nodes/credentials_spec.js @@ -0,0 +1,497 @@ +/** + * 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 sinon = require("sinon"); +var when = require("when"); +var util = require("util"); + +var index = require("../../../red/nodes/index"); +var credentials = require("../../../red/nodes/credentials"); + +describe('Credentials', function() { + + afterEach(function() { + index.clearRegistry(); + }); + + it('loads from storage',function(done) { + + var storage = { + getCredentials: function() { + return when.promise(function(resolve,reject) { + resolve({"a":{"b":1,"c":2}}); + }); + } + }; + + credentials.init(storage); + + credentials.load().then(function() { + + credentials.get("a").should.have.property('b',1); + credentials.get("a").should.have.property('c',2); + + done(); + }); + }); + + + it('saves to storage', function(done) { + var storage = { + getCredentials: function() { + return when.promise(function(resolve,reject) { + resolve({"a":{"b":1,"c":2}}); + }); + }, + saveCredentials: function(creds) { + return when(true); + } + }; + sinon.spy(storage,"saveCredentials"); + credentials.init(storage); + credentials.load().then(function() { + should.not.exist(credentials.get("b")) + credentials.add('b',{"d":3}); + storage.saveCredentials.callCount.should.be.exactly(1); + credentials.get("b").should.have.property('d',3); + storage.saveCredentials.restore(); + done(); + }); + }); + + it('deletes from storage', function(done) { + var storage = { + getCredentials: function() { + return when.promise(function(resolve,reject) { + resolve({"a":{"b":1,"c":2}}); + }); + }, + saveCredentials: function(creds) { + return when(true); + } + }; + sinon.spy(storage,"saveCredentials"); + credentials.init(storage); + credentials.load().then(function() { + should.exist(credentials.get("a")) + credentials.delete('a'); + storage.saveCredentials.callCount.should.be.exactly(1); + should.not.exist(credentials.get("a")); + storage.saveCredentials.restore(); + done(); + }); + + }); + + it('clean up from storage', function(done) { + var storage = { + getCredentials: function() { + return when.promise(function(resolve,reject) { + resolve({"a":{"b":1,"c":2}}); + }); + }, + saveCredentials: function(creds) { + return when(true); + } + }; + sinon.spy(storage,"saveCredentials"); + credentials.init(storage); + credentials.load().then(function() { + should.exist(credentials.get("a")); + credentials.clean(function() { + return false; + }); + storage.saveCredentials.callCount.should.be.exactly(1); + should.not.exist(credentials.get("a")); + storage.saveCredentials.restore(); + done(); + }); + }); + + it('handle error loading from storage', function(done) { + var storage = { + getCredentials: function() { + return when.promise(function(resolve,reject) { + reject("test forcing failure"); + }); + }, + saveCredentials: function(creds) { + return when(true); + } + }; + var logmsg = 'no errors yet'; + sinon.stub(util, 'log', function(msg) { + logmsg = msg; + }); + + credentials.init(storage); + credentials.load().then(function() { + should.equal('[red] Error loading credentials : test forcing failure', logmsg); + util.log.restore(); + done(); + }).otherwise(function(err){ + util.log.restore(); + done(err); + }); + }); + + it('credential type is not registered when extract', function(done) { + var testFlows = [{"type":"test","id":"tab1","label":"Sheet 1"}]; + var storage = { + getFlows: function() { + var defer = when.defer(); + defer.resolve(testFlows); + return defer.promise; + }, + getCredentials: function() { + return when.promise(function(resolve,reject) { + resolve({"tab1":{"b":1,"c":2}}); + }); + }, + saveFlows: function(conf) { + var defer = when.defer(); + defer.resolve(); + should.deepEqual(testFlows, conf); + return defer.promise; + }, + saveCredentials: function(creds) { + return when(true); + }, + getSettings: function() { + return when({}); + }, + saveSettings: function(s) { + return when(); + } + }; + function TestNode(n) { + index.createNode(this, n); + + this.id = 'tab1'; + this.type = 'test'; + this.name = 'barney'; + var node = this; + + this.on("log", function() { + // do nothing + }); + } + var logmsg = 'nothing logged yet'; + sinon.stub(util, 'log', function(msg) { + logmsg = msg; + }); + var settings = { + available: function() { return false;} + } + index.init(settings, storage); + index.registerType('test', TestNode); + index.loadFlows().then(function() { + var testnode = new TestNode({id:'tab1',type:'test',name:'barney'}); + credentials.extract(testnode); + should.equal(logmsg, 'Credential Type test is not registered.'); + util.log.restore(); + done(); + }).otherwise(function(err){ + util.log.restore(); + done(err); + }); + }); + + describe('extract and store credential updates in the provided node', function() { + var path = require('path'); + var fs = require('fs-extra'); + var http = require('http'); + var express = require('express'); + var server = require("../../../red/server"); + var localfilesystem = require("../../../red/storage/localfilesystem"); + var app = express(); + var RED = require("../../../red/red.js"); + + var userDir = path.join(__dirname,".testUserHome"); + before(function(done) { + fs.remove(userDir,function(err) { + fs.mkdir(userDir,function() { + sinon.stub(index, 'load', function() { + return when.promise(function(resolve,reject){ + resolve([]); + }); + }); + sinon.stub(localfilesystem, 'getCredentials', function() { + return when.promise(function(resolve,reject) { + resolve({"tab1":{"foo": 2, "pswd":'sticks'}}); + }); + }) ; + RED.init(http.createServer(function(req,res){app(req,res)}), + {userDir: userDir}); + server.start().then(function () { + done(); + }); + }); + }); + }); + + after(function(done) { + fs.remove(userDir,done); + server.stop(); + index.load.restore(); + localfilesystem.getCredentials.restore(); + }); + + function TestNode(n) { + index.createNode(this, n); + var node = this; + this.on("log", function() { + // do nothing + }); + } + + it(': credential updated with good value', function(done) { + index.registerType('test', TestNode, { + credentials: { + foo: {type:"test"} + } + }); + index.loadFlows().then(function() { + var testnode = new TestNode({id:'tab1',type:'test',name:'barney'}); + credentials.extract(testnode); + should.exist(credentials.get('tab1')); + credentials.get('tab1').should.have.property('foo',2); + + // set credentials to be an updated value and checking this is extracted properly + testnode.credentials = {"foo": 3}; + credentials.extract(testnode); + should.exist(credentials.get('tab1')); + credentials.get('tab1').should.not.have.property('foo',2); + credentials.get('tab1').should.have.property('foo',3); + done(); + }).otherwise(function(err){ + done(err); + }); + }); + + it(': credential updated with empty value', function(done) { + index.registerType('test', TestNode, { + credentials: { + foo: {type:"test"} + } + }); + index.loadFlows().then(function() { + var testnode = new TestNode({id:'tab1',type:'test',name:'barney'}); + // setting value of "foo" credential to be empty removes foo as a property + testnode.credentials = {"foo": ''}; + credentials.extract(testnode); + should.exist(credentials.get('tab1')); + credentials.get('tab1').should.not.have.property('foo',2); + credentials.get('tab1').should.not.have.property('foo'); + done(); + }).otherwise(function(err){ + done(err); + }); + }); + + it(': undefined credential updated', function(done) { + index.registerType('test', TestNode, { + credentials: { + foo: {type:"test"} + } + }); + index.loadFlows().then(function() { + var testnode = new TestNode({id:'tab1',type:'test',name:'barney'}); + // setting value of an undefined credential should not change anything + testnode.credentials = {"bar": 4}; + credentials.extract(testnode); + should.exist(credentials.get('tab1')); + credentials.get('tab1').should.have.property('foo',2); + credentials.get('tab1').should.not.have.property('bar'); + done(); + }).otherwise(function(err){ + done(err); + }); + }); + + it(': password credential updated', function(done) { + index.registerType('password', TestNode, { + credentials: { + pswd: {type:"password"} + } + }); + index.loadFlows().then(function() { + var testnode = new TestNode({id:'tab1',type:'password',name:'barney'}); + // setting value of password credential should update password + testnode.credentials = {"pswd": 'fiddle'}; + credentials.extract(testnode); + should.exist(credentials.get('tab1')); + credentials.get('tab1').should.have.property('pswd','fiddle'); + credentials.get('tab1').should.not.have.property('pswd','sticks'); + done(); + }).otherwise(function(err){ + done(err); + }); + }); + + it(': password credential not updated', function(done) { + index.registerType('password', TestNode, { + credentials: { + pswd: {type:"password"} + } + }); + index.loadFlows().then(function() { + var testnode = new TestNode({id:'tab1',type:'password',name:'barney'}); + // setting value of password credential should update password + testnode.credentials = {"pswd": '__PWRD__'}; + credentials.extract(testnode); + should.exist(credentials.get('tab1')); + credentials.get('tab1').should.have.property('pswd','sticks'); + credentials.get('tab1').should.not.have.property('pswd','__PWRD__'); + done(); + }).otherwise(function(err){ + done(err); + }); + }); + + }) + + describe('registerEndpoint', function() { + var path = require('path'); + var fs = require('fs-extra'); + var http = require('http'); + var express = require('express'); + var request = require('supertest'); + + var server = require("../../../red/server"); + var localfilesystem = require("../../../red/storage/localfilesystem"); + var app = express(); + var RED = require("../../../red/red.js"); + + var userDir = path.join(__dirname,".testUserHome"); + before(function(done) { + fs.remove(userDir,function(err) { + fs.mkdir(userDir,function() { + sinon.stub(index, 'load', function() { + return when.promise(function(resolve,reject){ + resolve([]); + }); + }); + sinon.stub(localfilesystem, 'getCredentials', function() { + return when.promise(function(resolve,reject) { + resolve({"tab1":{"foo": 2, "pswd":'sticks'}}); + }); + }) ; + RED.init(http.createServer(function(req,res){app(req,res)}), + {userDir: userDir}); + server.start().then(function () { + done(); + }); + }); + }); + }); + + after(function(done) { + fs.remove(userDir,done); + server.stop(); + index.load.restore(); + localfilesystem.getCredentials.restore(); + }); + + function TestNode(n) { + index.createNode(this, n); + var node = this; + this.on("log", function() { + // do nothing + }); + } + + it(': valid credential type', function(done) { + index.registerType('test', TestNode, { + credentials: { + foo: {type:"test"} + } + }); + index.loadFlows().then(function() { + var testnode = new TestNode({id:'tab1',type:'foo',name:'barney'}); + request(RED.httpAdmin).get('/credentials/test/tab1').expect(200).end(function(err,res) { + if (err) { + done(err); + } + res.body.should.have.property('foo', 2); + done(); + }); + }).otherwise(function(err){ + done(err); + }); + }); + + it(': password credential type', function(done) { + index.registerType('password', TestNode, { + credentials: { + pswd: {type:"password"} + } + }); + index.loadFlows().then(function() { + var testnode = new TestNode({id:'tab1',type:'pswd',name:'barney'}); + request(RED.httpAdmin).get('/credentials/password/tab1').expect(200).end(function(err,res) { + if (err) { + done(err); + } + res.body.should.have.property('has_pswd', true); + res.body.should.not.have.property('pswd'); + done(); + }); + }).otherwise(function(err){ + done(err); + }); + }); + + it(': returns 404 for undefined credential type', function(done) { + index.registerType('test', TestNode, { + credentials: { + foo: {type:"test"} + } + }); + index.loadFlows().then(function() { + var testnode = new TestNode({id:'tab1',type:'foo',name:'barney'}); + request(RED.httpAdmin).get('/credentials/unknownType/tab1').expect(404).end(done); + }).otherwise(function(err){ + done(err); + }); + }); + + it(': undefined nodeID', function(done) { + index.registerType('test', TestNode, { + credentials: { + foo: {type:"test"} + } + }); + index.loadFlows().then(function() { + var testnode = new TestNode({id:'tab1',type:'foo',name:'barney'}); + request(RED.httpAdmin).get('/credentials/test/unknownNode').expect(200).end(function(err,res) { + if (err) { + done(err); + } + var b = res.body; + res.body.should.not.have.property('foo'); + done(); + }); + }).otherwise(function(err){ + done(err); + }); + }); + + }) + +}) + diff --git a/dgbuilder/test/red/nodes/flows_spec.js b/dgbuilder/test/red/nodes/flows_spec.js new file mode 100644 index 00000000..091bf409 --- /dev/null +++ b/dgbuilder/test/red/nodes/flows_spec.js @@ -0,0 +1,134 @@ +/** + * 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 sinon = require("sinon"); +var when = require("when"); +var flows = require("../../../red/nodes/flows"); +var RedNode = require("../../../red/nodes/Node"); +var RED = require("../../../red/nodes"); +var events = require("../../../red/events"); +var typeRegistry = require("../../../red/nodes/registry"); + + +var settings = { + available: function() { return false; } +} + +function loadFlows(testFlows, cb) { + var storage = { + getFlows: function() { + return when.resolve(testFlows); + }, + getCredentials: function() { + return when.resolve({}); + } + }; + RED.init(settings, storage); + flows.load().then(function() { + should.deepEqual(testFlows, flows.getFlows()); + cb(); + }); +} + +describe('flows', function() { + + describe('#add',function() { + it('should be called by node constructor',function(done) { + var n = new RedNode({id:'123',type:'abc'}); + should.deepEqual(n, flows.get("123")); + flows.clear().then(function() { + done(); + }); + }); + }); + + describe('#each',function() { + it('should "visit" all nodes',function(done) { + var nodes = [ + new RedNode({id:'n0'}), + new RedNode({id:'n1'}) + ]; + var count = 0; + flows.each(function(node) { + should.deepEqual(nodes[count], node); + count += 1; + if (count == 2) { + done(); + } + }); + }); + }); + + describe('#load',function() { + it('should load nothing when storage is empty',function(done) { + loadFlows([], done); + }); + + it('should load and start an empty tab flow',function(done) { + loadFlows([{"type":"tab","id":"tab1","label":"Sheet 1"}], function() {}); + events.once('nodes-started', function() { done(); }); + }); + + it('should load and start a registered node type', function(done) { + RED.registerType('debug', function() {}); + var typeRegistryGet = sinon.stub(typeRegistry,"get",function(nt) { + return function() {}; + }); + loadFlows([{"id":"n1","type":"debug"}], function() { }); + events.once('nodes-started', function() { + typeRegistryGet.restore(); + done(); + }); + }); + + it('should load and start when node type is registered', function(done) { + var typeRegistryGet = sinon.stub(typeRegistry,"get"); + typeRegistryGet.onCall(0).returns(null); + typeRegistryGet.returns(function(){}); + + loadFlows([{"id":"n2","type":"inject"}], function() { + events.emit('type-registered','inject'); + }); + events.once('nodes-started', function() { + typeRegistryGet.restore(); + done(); + }); + }); + }); + + describe('#setFlows',function() { + it('should save and start an empty tab flow',function(done) { + var saved = 0; + var testFlows = [{"type":"tab","id":"tab1","label":"Sheet 1"}]; + var storage = { + saveFlows: function(conf) { + var defer = when.defer(); + defer.resolve(); + should.deepEqual(testFlows, conf); + return defer.promise; + }, + saveCredentials: function (creds) { + return when(true); + } + }; + RED.init(settings, storage); + flows.setFlows(testFlows); + events.once('nodes-started', function() { done(); }); + }); + }); + +}); diff --git a/dgbuilder/test/red/nodes/index_spec.js b/dgbuilder/test/red/nodes/index_spec.js new file mode 100644 index 00000000..dcb866e9 --- /dev/null +++ b/dgbuilder/test/red/nodes/index_spec.js @@ -0,0 +1,255 @@ +/** + * 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 fs = require('fs-extra'); +var path = require('path'); +var when = require("when"); +var sinon = require('sinon'); + +var index = require("../../../red/nodes/index"); + +describe("red/nodes/index", function() { + + afterEach(function() { + index.clearRegistry(); + }); + + var testFlows = [{"type":"test","id":"tab1","label":"Sheet 1"}]; + var storage = { + getFlows: function() { + return when(testFlows); + }, + getCredentials: function() { + return when({"tab1":{"b":1,"c":2}}); + }, + saveFlows: function(conf) { + should.deepEqual(testFlows, conf); + return when(); + }, + saveCredentials: function(creds) { + return when(true); + } + }; + + var settings = { + available: function() { return false } + }; + + function TestNode(n) { + index.createNode(this, n); + var node = this; + this.on("log", function() { + // do nothing + }); + } + + it('nodes are initialised with credentials',function(done) { + + index.init(settings, storage); + index.registerType('test', TestNode); + index.loadFlows().then(function() { + var testnode = new TestNode({id:'tab1',type:'test',name:'barney'}); + testnode.credentials.should.have.property('b',1); + testnode.credentials.should.have.property('c',2); + done(); + }).otherwise(function(err) { + done(err); + }); + + }); + + it('flows should be initialised',function(done) { + index.init(settings, storage); + index.loadFlows().then(function() { + should.deepEqual(testFlows, index.getFlows()); + done(); + }).otherwise(function(err) { + done(err); + }); + + }); + + describe("registerType should register credentials definition", function() { + var http = require('http'); + var express = require('express'); + var app = express(); + var server = require("../../../red/server"); + var credentials = require("../../../red/nodes/credentials"); + var localfilesystem = require("../../../red/storage/localfilesystem"); + var RED = require("../../../red/red.js"); + + var userDir = path.join(__dirname,".testUserHome"); + before(function(done) { + fs.remove(userDir,function(err) { + fs.mkdir(userDir,function() { + sinon.stub(index, 'load', function() { + return when.promise(function(resolve,reject){ + resolve([]); + }); + }); + sinon.stub(localfilesystem, 'getCredentials', function() { + return when.promise(function(resolve,reject) { + resolve({"tab1":{"b":1,"c":2}}); + }); + }) ; + RED.init(http.createServer(function(req,res){app(req,res)}), + {userDir: userDir}); + server.start().then(function () { + done(); + }); + }); + }); + }); + + after(function(done) { + fs.remove(userDir,done); + server.stop(); + index.load.restore(); + localfilesystem.getCredentials.restore(); + }); + + it(': definition defined',function(done) { + index.registerType('test', TestNode, { + credentials: { + foo: {type:"test"} + } + }); + var testnode = new TestNode({id:'tab1',type:'test',name:'barney'}); + credentials.getDefinition("test").should.have.property('foo'); + done(); + }); + + }); + + describe('allows nodes to be added/remove/enabled/disabled from the registry', function() { + var registry = require("../../../red/nodes/registry"); + var randomNodeInfo = {id:"5678",types:["random"]}; + + before(function() { + sinon.stub(registry,"getNodeInfo",function(id) { + if (id == "test") { + return {id:"1234",types:["test"]}; + } else if (id == "doesnotexist") { + return null; + } else { + return randomNodeInfo; + } + }); + sinon.stub(registry,"removeNode",function(id) { + return randomNodeInfo; + }); + sinon.stub(registry,"disableNode",function(id) { + return randomNodeInfo; + }); + }); + after(function() { + registry.getNodeInfo.restore(); + registry.removeNode.restore(); + registry.disableNode.restore(); + }); + + it(': allows an unused node type to be removed',function(done) { + index.init(settings, storage); + index.registerType('test', TestNode); + index.loadFlows().then(function() { + var info = index.removeNode("5678"); + registry.removeNode.calledOnce.should.be.true; + registry.removeNode.calledWith("5678").should.be.true; + info.should.eql(randomNodeInfo); + done(); + }).otherwise(function(err) { + done(err); + }); + }); + + it(': allows an unused node type to be disabled',function(done) { + index.init(settings, storage); + index.registerType('test', TestNode); + index.loadFlows().then(function() { + var info = index.disableNode("5678"); + registry.disableNode.calledOnce.should.be.true; + registry.disableNode.calledWith("5678").should.be.true; + info.should.eql(randomNodeInfo); + done(); + }).otherwise(function(err) { + done(err); + }); + }); + + it(': prevents removing a node type that is in use',function(done) { + index.init(settings, storage); + index.registerType('test', TestNode); + index.loadFlows().then(function() { + /*jshint immed: false */ + (function() { + index.removeNode("test"); + }).should.throw(); + + done(); + }).otherwise(function(err) { + done(err); + }); + }); + + it(': prevents disabling a node type that is in use',function(done) { + index.init(settings, storage); + index.registerType('test', TestNode); + index.loadFlows().then(function() { + /*jshint immed: false */ + (function() { + index.disabledNode("test"); + }).should.throw(); + + done(); + }).otherwise(function(err) { + done(err); + }); + }); + + it(': prevents removing a node type that is unknown',function(done) { + index.init(settings, storage); + index.registerType('test', TestNode); + index.loadFlows().then(function() { + /*jshint immed: false */ + (function() { + index.removeNode("doesnotexist"); + }).should.throw(); + + done(); + }).otherwise(function(err) { + done(err); + }); + }); + it(': prevents disabling a node type that is unknown',function(done) { + index.init(settings, storage); + index.registerType('test', TestNode); + index.loadFlows().then(function() { + /*jshint immed: false */ + (function() { + index.disableNode("doesnotexist"); + }).should.throw(); + + done(); + }).otherwise(function(err) { + done(err); + }); + }); + + }); + + +}); diff --git a/dgbuilder/test/red/nodes/registry_spec.js b/dgbuilder/test/red/nodes/registry_spec.js new file mode 100644 index 00000000..81c1a2ce --- /dev/null +++ b/dgbuilder/test/red/nodes/registry_spec.js @@ -0,0 +1,808 @@ +/** + * 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 sinon = require("sinon"); +var path = require("path"); +var when = require("when"); + +var RedNodes = require("../../../red/nodes"); +var RedNode = require("../../../red/nodes/Node"); +var typeRegistry = require("../../../red/nodes/registry"); +var events = require("../../../red/events"); + +afterEach(function() { + typeRegistry.clear(); +}); + +describe('NodeRegistry', function() { + + var resourcesDir = __dirname+ path.sep + "resources" + path.sep; + + function stubSettings(s,available) { + s.available = function() {return available;} + s.set = function(s,v) { return when.resolve()}, + s.get = function(s) { return null;} + return s + } + var settings = stubSettings({},false); + var settingsWithStorage = stubSettings({},true); + + it('automatically registers new nodes',function() { + var testNode = RedNodes.getNode('123'); + should.not.exist(n); + var n = new RedNode({id:'123',type:'abc'}); + + var newNode = RedNodes.getNode('123'); + + should.strictEqual(n,newNode); + }); + + it('handles nodes that export a function', function(done) { + typeRegistry.init(settings); + typeRegistry.load(resourcesDir + "TestNode1",true).then(function() { + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(1); + list[0].should.have.property("id"); + list[0].should.have.property("name","TestNode1.js"); + list[0].should.have.property("types",["test-node-1"]); + list[0].should.have.property("enabled",true); + list[0].should.not.have.property("err"); + + var nodeConstructor = typeRegistry.get("test-node-1"); + nodeConstructor.should.be.type("function"); + + done(); + }).catch(function(e) { + done(e); + }); + + }); + + + it('handles nodes that export a function returning a resolving promise', function(done) { + typeRegistry.init(settings); + typeRegistry.load(resourcesDir + "TestNode2",true).then(function() { + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(1); + list[0].should.have.property("id"); + list[0].should.have.property("name","TestNode2.js"); + list[0].should.have.property("types",["test-node-2"]); + list[0].should.have.property("enabled",true); + list[0].should.not.have.property("err"); + var nodeConstructor = typeRegistry.get("test-node-2"); + nodeConstructor.should.be.type("function"); + + done(); + }).catch(function(e) { + done(e); + }); + + }); + + it('handles nodes that export a function returning a rejecting promise', function(done) { + typeRegistry.init(settings); + typeRegistry.load(resourcesDir + "TestNode3",true).then(function() { + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(1); + list[0].should.have.property("id"); + list[0].should.have.property("name","TestNode3.js"); + list[0].should.have.property("types",["test-node-3"]); + list[0].should.have.property("enabled",true); + list[0].should.have.property("err","fail"); + + var nodeConstructor = typeRegistry.get("test-node-3"); + (nodeConstructor === null).should.be.true; + + done(); + }).catch(function(e) { + done(e); + }); + + }); + + it('handles files containing multiple nodes', function(done) { + typeRegistry.init(settings); + typeRegistry.load(resourcesDir + "MultipleNodes1",true).then(function() { + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(1); + list[0].should.have.property("id"); + list[0].should.have.property("name","MultipleNodes1.js"); + list[0].should.have.property("types",["test-node-multiple-1a","test-node-multiple-1b"]); + list[0].should.have.property("enabled",true); + list[0].should.not.have.property("err"); + + var nodeConstructor = typeRegistry.get("test-node-multiple-1a"); + nodeConstructor.should.be.type("function"); + + nodeConstructor = typeRegistry.get("test-node-multiple-1b"); + nodeConstructor.should.be.type("function"); + + done(); + }).catch(function(e) { + done(e); + }); + }); + + it('handles nested directories', function(done) { + typeRegistry.init(settings); + typeRegistry.load(resourcesDir + "NestedDirectoryNode",true).then(function() { + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(1); + list[0].should.have.property("id"); + list[0].should.have.property("name","NestedNode.js"); + list[0].should.have.property("types",["nested-node-1"]); + list[0].should.have.property("enabled",true); + list[0].should.not.have.property("err"); + done(); + }).catch(function(e) { + done(e); + }); + }); + + it('emits type-registered and node-icon-dir events', function(done) { + var eventEmitSpy = sinon.spy(events,"emit"); + typeRegistry.init(settings); + typeRegistry.load(resourcesDir + "NestedDirectoryNode",true).then(function() { + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(1); + list[0].should.have.property("name","NestedNode.js"); + list[0].should.have.property("types",["nested-node-1"]); + list[0].should.have.property("enabled",true); + list[0].should.not.have.property("err"); + + eventEmitSpy.callCount.should.equal(2); + + eventEmitSpy.firstCall.args[0].should.be.equal("node-icon-dir"); + eventEmitSpy.firstCall.args[1].should.be.equal( + resourcesDir + "NestedDirectoryNode" + path.sep + "NestedNode" + path.sep + "icons"); + + eventEmitSpy.secondCall.args[0].should.be.equal("type-registered"); + eventEmitSpy.secondCall.args[1].should.be.equal("nested-node-1"); + + done(); + }).catch(function(e) { + done(e); + }).finally(function() { + eventEmitSpy.restore(); + }); + }); + + it('rejects a duplicate node type registration', function(done) { + + typeRegistry.init(stubSettings({ + nodesDir:[resourcesDir + "TestNode1",resourcesDir + "DuplicateTestNode"] + },false)); + typeRegistry.load("wontexist",true).then(function() { + var list = typeRegistry.getNodeList(); + + list.should.be.an.Array.and.have.lengthOf(2); + list[0].should.have.property("id"); + list[0].should.have.property("name","TestNode1.js"); + list[0].should.have.property("types",["test-node-1"]); + list[0].should.have.property("enabled",true); + list[0].should.not.have.property("err"); + + list[1].should.have.property("id"); + list[1].id.should.not.equal(list[0].id); + + list[1].should.have.property("name","TestNode1.js"); + list[1].should.have.property("types",["test-node-1"]); + list[1].should.have.property("enabled",true); + list[1].should.have.property("err"); + /already registered/.test(list[1].err).should.be.true; + + var nodeConstructor = typeRegistry.get("test-node-1"); + // Verify the duplicate node hasn't replaced the original one + nodeConstructor.name.should.be.equal("TestNode"); + + done(); + }).catch(function(e) { + done(e); + }); + }); + + it('handles nodesDir as a string', function(done) { + + typeRegistry.init(stubSettings({ + nodesDir :resourcesDir + "TestNode1" + },false)); + typeRegistry.load("wontexist",true).then(function(){ + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(1); + list[0].should.have.property("types",["test-node-1"]); + done(); + }).catch(function(e) { + done("Loading of non-existing nodesDir should succeed"); + }); + + }); + + it('handles invalid nodesDir',function(done) { + + typeRegistry.init(stubSettings({ + nodesDir : "wontexist" + },false)); + typeRegistry.load("wontexist",true).then(function(){ + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.be.empty; + done(); + }).catch(function(e) { + done("Loading of non-existing nodesDir should succeed"); + }); + }); + + it('returns nothing for an unregistered type config', function() { + typeRegistry.init(settings); + typeRegistry.load("wontexist",true).then(function(){ + var config = typeRegistry.getNodeConfig("imaginary-shark"); + (config === null).should.be.true; + }).catch(function(e) { + done(e); + }); + }); + + it('excludes node files listed in nodesExcludes',function(done) { + typeRegistry.init(stubSettings({ + nodesExcludes: [ "TestNode1.js" ], + nodesDir:[resourcesDir + "TestNode1",resourcesDir + "TestNode2"] + },false)); + typeRegistry.load("wontexist",true).then(function() { + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(1); + list[0].should.have.property("types",["test-node-2"]); + done(); + }).catch(function(e) { + done(e); + }); + }); + + it('returns the node configurations', function(done) { + typeRegistry.init(stubSettings({ + nodesDir:[resourcesDir + "TestNode1",resourcesDir + "TestNode2"] + },false)); + typeRegistry.load("wontexist",true).then(function() { + var list = typeRegistry.getNodeList(); + + var nodeConfigs = typeRegistry.getNodeConfigs(); + + // TODO: this is brittle... + nodeConfigs.should.equal("<script type=\"text/x-red\" data-template-name=\"test-node-1\"></script>\n<script type=\"text/x-red\" data-help-name=\"test-node-1\"></script>\n<script type=\"text/javascript\">RED.nodes.registerType('test-node-1',{});</script>\n<style></style>\n<p>this should be filtered out</p>\n<script type=\"text/x-red\" data-template-name=\"test-node-2\"></script>\n<script type=\"text/x-red\" data-help-name=\"test-node-2\"></script>\n<script type=\"text/javascript\">RED.nodes.registerType('test-node-2',{});</script>\n<style></style>\n"); + + var nodeId = list[0].id; + var nodeConfig = typeRegistry.getNodeConfig(nodeId); + nodeConfig.should.equal("<script type=\"text/x-red\" data-template-name=\"test-node-1\"></script>\n<script type=\"text/x-red\" data-help-name=\"test-node-1\"></script>\n<script type=\"text/javascript\">RED.nodes.registerType('test-node-1',{});</script>\n<style></style>\n<p>this should be filtered out</p>\n"); + done(); + }).catch(function(e) { + done(e); + }); + }); + + it('stores the node list', function(done) { + var settings = { + nodesDir:[resourcesDir + "TestNode1",resourcesDir + "TestNode2",resourcesDir + "TestNode3"], + available: function() { return true; }, + set: function(s,v) {return when.resolve();}, + get: function(s) { return null;} + } + var settingsSave = sinon.spy(settings,"set"); + typeRegistry.init(settings); + typeRegistry.load("wontexist",true).then(function() { + var list = typeRegistry.getNodeList(); + list.should.be.Array.and.have.length(3); + + settingsSave.callCount.should.equal(1); + settingsSave.firstCall.args[0].should.be.equal("nodes"); + var savedList = settingsSave.firstCall.args[1]; + + savedList[list[0].id].name == list[0].name; + savedList[list[1].id].name == list[1].name; + savedList[list[2].id].name == list[2].name; + + savedList[list[0].id].should.not.have.property("err"); + savedList[list[1].id].should.not.have.property("err"); + savedList[list[2].id].should.not.have.property("err"); + + done(); + }).catch(function(e) { + done(e); + }).finally(function() { + settingsSave.restore(); + }); + + }); + + it('allows nodes to be added by filename', function(done) { + var settings = { + available: function() { return true; }, + set: function(s,v) {return when.resolve();}, + get: function(s) { return null;} + } + typeRegistry.init(settings); + typeRegistry.load("wontexist",true).then(function(){ + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.be.empty; + + typeRegistry.addNode(resourcesDir + "TestNode1/TestNode1.js").then(function(node) { + list = typeRegistry.getNodeList(); + list[0].should.have.property("id"); + list[0].should.have.property("name","TestNode1.js"); + list[0].should.have.property("types",["test-node-1"]); + list[0].should.have.property("enabled",true); + list[0].should.not.have.property("err"); + + node.should.be.an.Array.and.have.lengthOf(1); + node.should.eql(list); + + done(); + }).catch(function(e) { + done(e); + }); + + }).catch(function(e) { + done(e); + }); + }); + + it('fails to add non-existent filename', function(done) { + typeRegistry.init(settingsWithStorage); + typeRegistry.load("wontexist",true).then(function(){ + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.be.empty; + typeRegistry.addNode(resourcesDir + "DoesNotExist/DoesNotExist.js").then(function(nodes) { + nodes.should.be.an.Array.and.have.lengthOf(1); + nodes[0].should.have.property("id"); + nodes[0].should.have.property("types",[]); + nodes[0].should.have.property("err"); + done(); + }).otherwise(function(e) { + done(e); + }); + + }).catch(function(e) { + done(e); + }); + }); + + it('returns node info by type or id', function(done) { + typeRegistry.init(settings); + typeRegistry.load(resourcesDir + "TestNode1",true).then(function() { + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(1); + + var id = list[0].id; + var type = list[0].types[0]; + + list[0].should.have.property("id"); + list[0].should.have.property("name","TestNode1.js"); + list[0].should.have.property("types",["test-node-1"]); + list[0].should.have.property("enabled",true); + list[0].should.not.have.property("err"); + + var info = typeRegistry.getNodeInfo(id); + list[0].should.eql(info); + + var info2 = typeRegistry.getNodeInfo(type); + list[0].should.eql(info2); + + done(); + }).catch(function(e) { + done(e); + }); + + }); + + + it('rejects adding duplicate nodes', function(done) { + typeRegistry.init(settingsWithStorage); + typeRegistry.load(resourcesDir + "TestNode1",true).then(function(){ + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(1); + + typeRegistry.addNode({file:resourcesDir + "TestNode1" + path.sep + "TestNode1.js"}).then(function(node) { + done(new Error("duplicate node loaded")); + }).otherwise(function(e) { + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(1); + done(); + }); + + }).catch(function(e) { + done(e); + }); + }); + + it('removes nodes from the registry', function(done) { + typeRegistry.init(settingsWithStorage); + typeRegistry.load(resourcesDir + "TestNode1",true).then(function() { + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(1); + list[0].should.have.property("id"); + list[0].should.have.property("name","TestNode1.js"); + list[0].should.have.property("types",["test-node-1"]); + list[0].should.have.property("enabled",true); + list[0].should.have.property("loaded",true); + + typeRegistry.getNodeConfigs().length.should.be.greaterThan(0); + + var info = typeRegistry.removeNode(list[0].id); + + info.should.have.property("id",list[0].id); + info.should.have.property("enabled",false); + info.should.have.property("loaded",false); + + typeRegistry.getNodeList().should.be.an.Array.and.be.empty; + typeRegistry.getNodeConfigs().length.should.equal(0); + + var nodeConstructor = typeRegistry.get("test-node-1"); + (typeof nodeConstructor).should.be.equal("undefined"); + + + done(); + }).catch(function(e) { + done(e); + }); + }); + + it('rejects removing unknown nodes from the registry', function(done) { + typeRegistry.init(settings); + typeRegistry.load("wontexist",true).then(function() { + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.be.empty; + + + /*jshint immed: false */ + (function() { + typeRegistry.removeNode("1234"); + }).should.throw(); + + done(); + }).catch(function(e) { + done(e); + }); + }); + + it('scans the node_modules path for node files', function(done) { + var fs = require("fs"); + var path = require("path"); + + var eventEmitSpy = sinon.spy(events,"emit"); + var pathJoin = (function() { + var _join = path.join; + return sinon.stub(path,"join",function() { + if (arguments.length == 3 && arguments[2] == "package.json") { + return _join(resourcesDir,"TestNodeModule" + path.sep + "node_modules" + path.sep,arguments[1],arguments[2]); + } + if (arguments.length == 2 && arguments[1] == "TestNodeModule") { + return _join(resourcesDir,"TestNodeModule" + path.sep + "node_modules" + path.sep,arguments[1]); + } + return _join.apply(this,arguments); + }); + })(); + + var readdirSync = (function() { + var originalReaddirSync = fs.readdirSync; + var callCount = 0; + return sinon.stub(fs,"readdirSync",function(dir) { + var result = []; + if (callCount == 1) { + result = originalReaddirSync(resourcesDir + "TestNodeModule" + path.sep + "node_modules"); + } + callCount++; + return result; + }); + })(); + + typeRegistry.init(settings); + typeRegistry.load("wontexist",false).then(function(){ + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(2); + list[0].should.have.property("id"); + list[0].should.have.property("name","TestNodeModule:TestNodeMod1"); + list[0].should.have.property("types",["test-node-mod-1"]); + list[0].should.have.property("enabled",true); + list[0].should.not.have.property("err"); + + list[1].should.have.property("id"); + list[1].should.have.property("name","TestNodeModule:TestNodeMod2"); + list[1].should.have.property("types",["test-node-mod-2"]); + list[1].should.have.property("enabled",true); + list[1].should.have.property("err"); + + + eventEmitSpy.callCount.should.equal(2); + + eventEmitSpy.firstCall.args[0].should.be.equal("node-icon-dir"); + eventEmitSpy.firstCall.args[1].should.be.equal( + resourcesDir + "TestNodeModule" + path.sep+ "node_modules" + path.sep + "TestNodeModule" + path.sep + "icons"); + + eventEmitSpy.secondCall.args[0].should.be.equal("type-registered"); + eventEmitSpy.secondCall.args[1].should.be.equal("test-node-mod-1"); + + done(); + }).catch(function(e) { + done(e); + }).finally(function() { + readdirSync.restore(); + pathJoin.restore(); + eventEmitSpy.restore(); + }); + }); + + it('allows nodes to be added by module name', function(done) { + var fs = require("fs"); + var path = require("path"); + + var pathJoin = (function() { + var _join = path.join; + return sinon.stub(path,"join",function() { + if (arguments.length == 3 && arguments[2] == "package.json") { + return _join(resourcesDir,"TestNodeModule" + path.sep + "node_modules" + path.sep,arguments[1],arguments[2]); + } + if (arguments.length == 2 && arguments[1] == "TestNodeModule") { + return _join(resourcesDir,"TestNodeModule" + path.sep + "node_modules" + path.sep,arguments[1]); + } + return _join.apply(this,arguments); + }); + })(); + + var readdirSync = (function() { + var originalReaddirSync = fs.readdirSync; + var callCount = 0; + return sinon.stub(fs,"readdirSync",function(dir) { + var result = []; + if (callCount == 1) { + result = originalReaddirSync(resourcesDir + "TestNodeModule" + path.sep + "node_modules"); + } + callCount++; + return result; + }); + })(); + typeRegistry.init(settingsWithStorage); + typeRegistry.load("wontexist",true).then(function(){ + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.be.empty; + + typeRegistry.addModule("TestNodeModule").then(function(node) { + list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(2); + list[0].should.have.property("id"); + list[0].should.have.property("name","TestNodeModule:TestNodeMod1"); + list[0].should.have.property("types",["test-node-mod-1"]); + list[0].should.have.property("enabled",true); + list[0].should.not.have.property("err"); + + list[1].should.have.property("id"); + list[1].should.have.property("name","TestNodeModule:TestNodeMod2"); + list[1].should.have.property("types",["test-node-mod-2"]); + list[1].should.have.property("enabled",true); + list[1].should.have.property("err"); + + node.should.eql(list); + + done(); + }).catch(function(e) { + done(e); + }); + + }).catch(function(e) { + done(e); + }).finally(function() { + readdirSync.restore(); + pathJoin.restore(); + }); + }); + + + it('rejects adding duplicate node modules', function(done) { + var fs = require("fs"); + var path = require("path"); + + var pathJoin = (function() { + var _join = path.join; + return sinon.stub(path,"join",function() { + if (arguments.length == 3 && arguments[2] == "package.json") { + return _join(resourcesDir,"TestNodeModule" + path.sep + "node_modules" + path.sep,arguments[1],arguments[2]); + } + if (arguments.length == 2 && arguments[1] == "TestNodeModule") { + return _join(resourcesDir,"TestNodeModule" + path.sep + "node_modules" + path.sep,arguments[1]); + } + return _join.apply(this,arguments); + }); + })(); + + var readdirSync = (function() { + var originalReaddirSync = fs.readdirSync; + var callCount = 0; + return sinon.stub(fs,"readdirSync",function(dir) { + var result = []; + if (callCount == 1) { + result = originalReaddirSync(resourcesDir + "TestNodeModule" + path.sep + "node_modules"); + } + callCount++; + return result; + }); + })(); + + typeRegistry.init(settingsWithStorage); + typeRegistry.load('wontexist',false).then(function(){ + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(2); + typeRegistry.addModule("TestNodeModule").then(function(node) { + done(new Error("addModule resolved")); + }).otherwise(function(err) { + done(); + }); + }).catch(function(e) { + done(e); + }).finally(function() { + readdirSync.restore(); + pathJoin.restore(); + }); + }); + + + it('fails to add non-existent module name', function(done) { + typeRegistry.init(settingsWithStorage); + typeRegistry.load("wontexist",true).then(function(){ + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.be.empty; + + typeRegistry.addModule("DoesNotExistModule").then(function(node) { + done(new Error("ENOENT not thrown")); + }).otherwise(function(e) { + e.code.should.eql("MODULE_NOT_FOUND"); + done(); + }); + + }).catch(function(e) { + done(e); + }); + }); + + it('removes nodes from the registry by module', function(done) { + var fs = require("fs"); + var path = require("path"); + + var pathJoin = (function() { + var _join = path.join; + return sinon.stub(path,"join",function() { + if (arguments.length == 3 && arguments[2] == "package.json") { + return _join(resourcesDir,"TestNodeModule" + path.sep + "node_modules" + path.sep,arguments[1],arguments[2]); + } + if (arguments.length == 2 && arguments[1] == "TestNodeModule") { + return _join(resourcesDir,"TestNodeModule" + path.sep + "node_modules" + path.sep,arguments[1]); + } + return _join.apply(this,arguments); + }); + })(); + + var readdirSync = (function() { + var originalReaddirSync = fs.readdirSync; + var callCount = 0; + return sinon.stub(fs,"readdirSync",function(dir) { + var result = []; + if (callCount == 1) { + result = originalReaddirSync(resourcesDir + "TestNodeModule" + path.sep + "node_modules"); + } + callCount++; + return result; + }); + })(); + + typeRegistry.init(settingsWithStorage); + typeRegistry.load('wontexist',false).then(function(){ + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(2); + var res = typeRegistry.removeModule("TestNodeModule"); + + res.should.be.an.Array.and.have.lengthOf(2); + res[0].should.have.a.property("id",list[0].id); + res[1].should.have.a.property("id",list[1].id); + + list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.be.empty; + + done(); + }).catch(function(e) { + done(e); + }).finally(function() { + readdirSync.restore(); + pathJoin.restore(); + }); + + }); + + it('fails to remove non-existent module name', function(done) { + typeRegistry.init(settings); + typeRegistry.load("wontexist",true).then(function(){ + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.be.empty; + + /*jshint immed: false */ + (function() { + typeRegistry.removeModule("DoesNotExistModule"); + }).should.throw(); + + done(); + + }).catch(function(e) { + done(e); + }); + }); + + + it('allows nodes to be enabled and disabled', function(done) { + typeRegistry.init(settingsWithStorage); + typeRegistry.load(resourcesDir+path.sep+"TestNode1",true).then(function() { + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.have.lengthOf(1); + list[0].should.have.property("id"); + list[0].should.have.property("name","TestNode1.js"); + list[0].should.have.property("enabled",true); + + var nodeConfig = typeRegistry.getNodeConfigs(); + nodeConfig.length.should.be.greaterThan(0); + + var info = typeRegistry.disableNode(list[0].id); + info.should.have.property("id",list[0].id); + info.should.have.property("enabled",false); + + var list2 = typeRegistry.getNodeList(); + list2.should.be.an.Array.and.have.lengthOf(1); + list2[0].should.have.property("enabled",false); + + typeRegistry.getNodeConfigs().length.should.equal(0); + + var info2 = typeRegistry.enableNode(list[0].id); + info2.should.have.property("id",list[0].id); + info2.should.have.property("enabled",true); + + var list3 = typeRegistry.getNodeList(); + list3.should.be.an.Array.and.have.lengthOf(1); + list3[0].should.have.property("enabled",true); + + var nodeConfig2 = typeRegistry.getNodeConfigs(); + nodeConfig2.should.eql(nodeConfig); + + done(); + }).catch(function(e) { + done(e); + }); + }); + + it('fails to enable/disable non-existent nodes', function(done) { + typeRegistry.init(settings); + typeRegistry.load("wontexist",true).then(function() { + var list = typeRegistry.getNodeList(); + list.should.be.an.Array.and.be.empty; + + /*jshint immed: false */ + (function() { + typeRegistry.disableNode("123"); + }).should.throw(); + + /*jshint immed: false */ + (function() { + typeRegistry.enableNode("123"); + }).should.throw(); + + done(); + }).catch(function(e) { + done(e); + }); + }); +}); diff --git a/dgbuilder/test/red/nodes/resources/DuplicateTestNode/TestNode1.html b/dgbuilder/test/red/nodes/resources/DuplicateTestNode/TestNode1.html new file mode 100644 index 00000000..b637ede2 --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/DuplicateTestNode/TestNode1.html @@ -0,0 +1,3 @@ +<script type="text/x-red" data-template-name="test-node-1"></script> +<script type="text/x-red" data-help-name="test-node-1"></script> +<script type="text/javascript">RED.nodes.registerType('test-node-1',{});</script> diff --git a/dgbuilder/test/red/nodes/resources/DuplicateTestNode/TestNode1.js b/dgbuilder/test/red/nodes/resources/DuplicateTestNode/TestNode1.js new file mode 100644 index 00000000..e8121416 --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/DuplicateTestNode/TestNode1.js @@ -0,0 +1,5 @@ +// A test node that exports a function +module.exports = function(RED) { + function DuplicateTestNode(n) {} + RED.nodes.registerType("test-node-1",DuplicateTestNode); +} diff --git a/dgbuilder/test/red/nodes/resources/MultipleNodes1/MultipleNodes1.html b/dgbuilder/test/red/nodes/resources/MultipleNodes1/MultipleNodes1.html new file mode 100644 index 00000000..5359644e --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/MultipleNodes1/MultipleNodes1.html @@ -0,0 +1,6 @@ +<script type="text/x-red" data-template-name="test-node-multiple-1a"></script> +<script type="text/x-red" data-help-name="test-node-multiple-1a"></script> +<script type="text/javascript">RED.nodes.registerType('test-node-multiple-1a',{});</script> +<script type="text/x-red" data-template-name="test-node-multiple-1b"></script> +<script type="text/x-red" data-help-name="test-node-multiple-1b"></script> +<script type="text/javascript">RED.nodes.registerType('test-node-multiple-1b',{});</script> diff --git a/dgbuilder/test/red/nodes/resources/MultipleNodes1/MultipleNodes1.js b/dgbuilder/test/red/nodes/resources/MultipleNodes1/MultipleNodes1.js new file mode 100644 index 00000000..55747c0b --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/MultipleNodes1/MultipleNodes1.js @@ -0,0 +1,7 @@ +// A test node that exports a function +module.exports = function(RED) { + function TestNode1(n) {} + RED.nodes.registerType("test-node-multiple-1a",TestNode1); + function TestNode2(n) {} + RED.nodes.registerType("test-node-multiple-1b",TestNode2); +} diff --git a/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/NestedNode.html b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/NestedNode.html new file mode 100644 index 00000000..abc823e8 --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/NestedNode.html @@ -0,0 +1,4 @@ +<script type="text/x-red" data-template-name="nested-node-1"></script> +<script type="text/x-red" data-help-name="nested-node-1"></script> +<script type="text/javascript">RED.nodes.registerType('nested-node-1',{});</script> +<style></style> diff --git a/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/NestedNode.js b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/NestedNode.js new file mode 100644 index 00000000..cd3148a5 --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/NestedNode.js @@ -0,0 +1,5 @@ +// A test node that exports a function +module.exports = function(RED) { + function TestNode(n) {} + RED.nodes.registerType("nested-node-1",TestNode); +} diff --git a/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/icons/file.txt b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/icons/file.txt new file mode 100644 index 00000000..59a29af1 --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/icons/file.txt @@ -0,0 +1,3 @@ +This file exists just to ensure the 'icons' directory is in the repository. +TODO: a future test needs to ensure the right icon files are loaded - this + directory can be used for that diff --git a/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/lib/ShouldNotLoad.html b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/lib/ShouldNotLoad.html new file mode 100644 index 00000000..ac9235d2 --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/lib/ShouldNotLoad.html @@ -0,0 +1,4 @@ +<script type="text/x-red" data-template-name="should-not-load-1"></script> +<script type="text/x-red" data-help-name="should-not-load-1"></script> +<script type="text/javascript">RED.nodes.registerType('should-not-load-1',{});</script> +<style></style> diff --git a/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/lib/ShouldNotLoad.js b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/lib/ShouldNotLoad.js new file mode 100644 index 00000000..8af249b1 --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/lib/ShouldNotLoad.js @@ -0,0 +1,5 @@ +// A test node that exports a function +module.exports = function(RED) { + function TestNode(n) {} + RED.nodes.registerType("should-not-load-1",TestNode); +} diff --git a/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/test/ShouldNotLoad.html b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/test/ShouldNotLoad.html new file mode 100644 index 00000000..4212fd58 --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/test/ShouldNotLoad.html @@ -0,0 +1,4 @@ +<script type="text/x-red" data-template-name="should-not-load-3"></script> +<script type="text/x-red" data-help-name="should-not-load-3"></script> +<script type="text/javascript">RED.nodes.registerType('should-not-load-3',{});</script> +<style></style> diff --git a/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/test/ShouldNotLoad.js b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/test/ShouldNotLoad.js new file mode 100644 index 00000000..5856adad --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/NestedDirectoryNode/NestedNode/test/ShouldNotLoad.js @@ -0,0 +1,5 @@ +// A test node that exports a function +module.exports = function(RED) { + function TestNode(n) {} + RED.nodes.registerType("should-not-load-3",TestNode); +} diff --git a/dgbuilder/test/red/nodes/resources/TestNode1/TestNode1.html b/dgbuilder/test/red/nodes/resources/TestNode1/TestNode1.html new file mode 100644 index 00000000..97dbf171 --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/TestNode1/TestNode1.html @@ -0,0 +1,5 @@ +<script type="text/x-red" data-template-name="test-node-1"></script> +<script type="text/x-red" data-help-name="test-node-1"></script> +<script type="text/javascript">RED.nodes.registerType('test-node-1',{});</script> +<style></style> +<p>this should be filtered out</p> diff --git a/dgbuilder/test/red/nodes/resources/TestNode1/TestNode1.js b/dgbuilder/test/red/nodes/resources/TestNode1/TestNode1.js new file mode 100644 index 00000000..bfa3b65b --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/TestNode1/TestNode1.js @@ -0,0 +1,5 @@ +// A test node that exports a function +module.exports = function(RED) { + function TestNode(n) {} + RED.nodes.registerType("test-node-1",TestNode); +} diff --git a/dgbuilder/test/red/nodes/resources/TestNode2/TestNode2.html b/dgbuilder/test/red/nodes/resources/TestNode2/TestNode2.html new file mode 100644 index 00000000..66b65909 --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/TestNode2/TestNode2.html @@ -0,0 +1,4 @@ +<script type="text/x-red" data-template-name="test-node-2"></script> +<script type="text/x-red" data-help-name="test-node-2"></script> +<script type="text/javascript">RED.nodes.registerType('test-node-2',{});</script> +<style></style> diff --git a/dgbuilder/test/red/nodes/resources/TestNode2/TestNode2.js b/dgbuilder/test/red/nodes/resources/TestNode2/TestNode2.js new file mode 100644 index 00000000..faf61a8f --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/TestNode2/TestNode2.js @@ -0,0 +1,10 @@ +// A test node that exports a function which returns a resolving promise + +var when = require("when"); +module.exports = function(RED) { + return when.promise(function(resolve,reject) { + function TestNode(n) {} + RED.nodes.registerType("test-node-2",TestNode); + resolve(); + }); +} diff --git a/dgbuilder/test/red/nodes/resources/TestNode3/TestNode3.html b/dgbuilder/test/red/nodes/resources/TestNode3/TestNode3.html new file mode 100644 index 00000000..9a0f6f7e --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/TestNode3/TestNode3.html @@ -0,0 +1,3 @@ +<script type="text/x-red" data-template-name="test-node-3"></script> +<script type="text/x-red" data-help-name="test-node-3"></script> +<script type="text/javascript">RED.nodes.registerType('test-node-3',{});</script> diff --git a/dgbuilder/test/red/nodes/resources/TestNode3/TestNode3.js b/dgbuilder/test/red/nodes/resources/TestNode3/TestNode3.js new file mode 100644 index 00000000..756dc139 --- /dev/null +++ b/dgbuilder/test/red/nodes/resources/TestNode3/TestNode3.js @@ -0,0 +1,8 @@ +// A test node that exports a function which returns a rejecting promise + +var when = require("when"); +module.exports = function(RED) { + return when.promise(function(resolve,reject) { + reject("fail"); + }); +} diff --git a/dgbuilder/test/red/red_spec.js b/dgbuilder/test/red/red_spec.js new file mode 100644 index 00000000..f61fd55b --- /dev/null +++ b/dgbuilder/test/red/red_spec.js @@ -0,0 +1,22 @@ +/** + * 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"); + +describe("red/red", function() { + it('can be required without errors', function() { + require("../../red/red"); + }); +}); diff --git a/dgbuilder/test/red/server_spec.js b/dgbuilder/test/red/server_spec.js new file mode 100644 index 00000000..b20249ca --- /dev/null +++ b/dgbuilder/test/red/server_spec.js @@ -0,0 +1,22 @@ +/** + * 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"); + +describe("red/server", function() { + it('can be required without errors', function() { + require("../../red/server"); + }); +}); diff --git a/dgbuilder/test/red/settings_spec.js b/dgbuilder/test/red/settings_spec.js new file mode 100644 index 00000000..fb4cbade --- /dev/null +++ b/dgbuilder/test/red/settings_spec.js @@ -0,0 +1,114 @@ +/** + * 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 when = require("when"); + +var settings = require("../../red/settings"); + + +describe("red/settings", function() { + + afterEach(function() { + settings.reset(); + }); + + it('wraps the user settings as read-only properties', function() { + var userSettings = { + a: 123, + b: "test", + c: [1,2,3] + } + settings.init(userSettings); + + settings.available().should.be.false; + + settings.a.should.equal(123); + settings.b.should.equal("test"); + settings.c.should.be.an.Array.with.lengthOf(3); + + settings.get("a").should.equal(123); + settings.get("b").should.equal("test"); + settings.get("c").should.be.an.Array.with.lengthOf(3); + + /*jshint immed: false */ + (function() { + settings.a = 456; + }).should.throw(); + + settings.c.push(5); + settings.c.should.be.an.Array.with.lengthOf(4); + + /*jshint immed: false */ + (function() { + settings.set("a",456); + }).should.throw(); + + /*jshint immed: false */ + (function() { + settings.set("a",456); + }).should.throw(); + + /*jshint immed: false */ + (function() { + settings.get("unknown"); + }).should.throw(); + + /*jshint immed: false */ + (function() { + settings.set("unknown",456); + }).should.throw(); + + }); + + it('loads global settings from storage', function(done) { + var userSettings = { + a: 123, + b: "test", + c: [1,2,3] + } + var savedSettings = null; + var storage = { + getSettings: function() { + return when.resolve({globalA:789}); + }, + saveSettings: function(settings) { + savedSettings = settings; + return when.resolve(); + } + } + settings.init(userSettings); + + settings.available().should.be.false; + + /*jshint immed: false */ + (function() { + settings.get("unknown"); + }).should.throw(); + + settings.load(storage).then(function() { + settings.available().should.be.true; + settings.get("globalA").should.equal(789); + settings.set("globalA","abc").then(function() { + savedSettings.globalA.should.equal("abc"); + done(); + }); + }).otherwise(function(err) { + done(err); + }); + + + }); +}); diff --git a/dgbuilder/test/red/storage/index_spec.js b/dgbuilder/test/red/storage/index_spec.js new file mode 100644 index 00000000..4b60ba83 --- /dev/null +++ b/dgbuilder/test/red/storage/index_spec.js @@ -0,0 +1,128 @@ +/** + * 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"); + +describe("red/storage/index", function() { + + it('rejects the promise when settings suggest loading a bad module', function(done) { + + var wrongModule = { + storageModule : "thisaintloading" + }; + + var storage = require("../../../red/storage/index"); + storage.init(wrongModule).then( function() { + var one = 1; + var zero = 0; + try { + zero.should.equal(one, "The initialization promise should never get resolved"); + } catch(err) { + done(err); + } + }).catch(function(e) { + done(); //successfully rejected promise + }); + }); + + it('non-string storage module', function(done) { + var initSetsMeToTrue = false; + + var moduleWithBooleanSettingInit = { + init : function() { + initSetsMeToTrue = true; + } + }; + + var setsBooleanModule = { + storageModule : moduleWithBooleanSettingInit + }; + + var storage = require("../../../red/storage/index"); + storage.init(setsBooleanModule); + initSetsMeToTrue.should.be.true; + done(); + }); + + it('respects storage interface', function(done) { + var calledFlagGetFlows = false; + var calledFlagGetCredentials = false; + var calledFlagGetAllFlows = false; + var calledInit = false; + + var interfaceCheckerModule = { + init : function (settings) { + settings.should.be.an.Object; + calledInit = true; + }, + getFlows : function() { + calledFlagGetFlows = true; + }, + saveFlows : function (flows) { + flows.should.be.true; + }, + getCredentials : function() { + calledFlagGetCredentials = true; + }, + saveCredentials : function(credentials) { + credentials.should.be.true; + }, + getAllFlows : function() { + calledFlagGetAllFlows = true; + }, + getFlow : function(fn) { + fn.should.equal("name"); + }, + saveFlow : function(fn, data) { + fn.should.equal("name"); + data.should.be.true; + }, + getLibraryEntry : function(type, path) { + type.should.be.true; + path.should.equal("name"); + }, + saveLibraryEntry : function(type, path, meta, body) { + type.should.be.true; + path.should.equal("name"); + meta.should.be.true; + body.should.be.true; + } + }; + + var moduleToLoad = { + storageModule : interfaceCheckerModule + }; + var storage = require("../../../red/storage/index"); + + storage.init(moduleToLoad); + storage.getFlows(); + storage.saveFlows(true); + storage.getCredentials(); + storage.saveCredentials(true); + storage.getAllFlows(); + storage.getFlow("name"); + storage.saveFlow("name", true); + storage.getLibraryEntry(true, "name"); + storage.saveLibraryEntry(true, "name", true, true); + + calledInit.should.be.true; + calledFlagGetFlows.should.be.true; + calledFlagGetCredentials.should.be.true; + calledFlagGetAllFlows.should.be.true; + + done(); + }); + +}); diff --git a/dgbuilder/test/red/storage/localfilesystem_spec.js b/dgbuilder/test/red/storage/localfilesystem_spec.js new file mode 100644 index 00000000..a1311709 --- /dev/null +++ b/dgbuilder/test/red/storage/localfilesystem_spec.js @@ -0,0 +1,367 @@ +/** + * Copyright 2013, 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 fs = require('fs-extra'); +var path = require('path'); + +var localfilesystem = require("../../../red/storage/localfilesystem"); + +describe('LocalFileSystem', function() { + var userDir = path.join(__dirname,".testUserHome"); + var testFlow = [{"type":"tab","id":"d8be2a6d.2741d8","label":"Sheet 1"}]; + beforeEach(function(done) { + fs.remove(userDir,function(err) { + fs.mkdir(userDir,done); + }); + }); + afterEach(function(done) { + fs.remove(userDir,done); + }); + + it('should initialise the user directory',function(done) { + localfilesystem.init({userDir:userDir}).then(function() { + fs.existsSync(path.join(userDir,"lib")).should.be.true; + fs.existsSync(path.join(userDir,"lib",'flows')).should.be.true; + done(); + }).otherwise(function(err) { + done(err); + }); + }); + + it('should handle missing flow file',function(done) { + localfilesystem.init({userDir:userDir}).then(function() { + var flowFile = 'flows_'+require('os').hostname()+'.json'; + var flowFilePath = path.join(userDir,flowFile); + fs.existsSync(flowFilePath).should.be.false; + localfilesystem.getFlows().then(function(flows) { + flows.should.eql([]); + done(); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }); + + it('should save flows to the default file',function(done) { + localfilesystem.init({userDir:userDir}).then(function() { + var flowFile = 'flows_'+require('os').hostname()+'.json'; + var flowFilePath = path.join(userDir,flowFile); + var flowFileBackupPath = path.join(userDir,"flows.backup"); + fs.existsSync(flowFilePath).should.be.false; + fs.existsSync(flowFileBackupPath).should.be.false; + localfilesystem.saveFlows(testFlow).then(function() { + fs.existsSync(flowFilePath).should.be.true; + fs.existsSync(flowFileBackupPath).should.be.false; + localfilesystem.getFlows().then(function(flows) { + flows.should.eql(testFlow); + done(); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }); + + it('should save flows to the specified file',function(done) { + var defaultFlowFile = 'flows_'+require('os').hostname()+'.json'; + var defaultFlowFilePath = path.join(userDir,defaultFlowFile); + var flowFile = 'test.json'; + var flowFilePath = path.join(userDir,flowFile); + + localfilesystem.init({userDir:userDir, flowFile:flowFilePath}).then(function() { + fs.existsSync(defaultFlowFilePath).should.be.false; + fs.existsSync(flowFilePath).should.be.false; + + localfilesystem.saveFlows(testFlow).then(function() { + fs.existsSync(defaultFlowFilePath).should.be.false; + fs.existsSync(flowFilePath).should.be.true; + localfilesystem.getFlows().then(function(flows) { + flows.should.eql(testFlow); + done(); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }); + + it('should backup the flows file', function(done) { + var defaultFlowFile = 'flows_'+require('os').hostname()+'.json'; + var defaultFlowFilePath = path.join(userDir,defaultFlowFile); + var flowFile = 'test.json'; + var flowFilePath = path.join(userDir,flowFile); + var flowFileBackupPath = path.join(userDir,"flows.backup"); + + localfilesystem.init({userDir:userDir, flowFile:flowFilePath}).then(function() { + fs.existsSync(defaultFlowFilePath).should.be.false; + fs.existsSync(flowFilePath).should.be.false; + fs.existsSync(flowFileBackupPath).should.be.false; + + localfilesystem.saveFlows(testFlow).then(function() { + fs.existsSync(flowFileBackupPath).should.be.false; + fs.existsSync(defaultFlowFilePath).should.be.false; + fs.existsSync(flowFilePath).should.be.true; + var content = fs.readFileSync(flowFilePath,'utf8'); + var testFlow2 = [{"type":"tab","id":"bc5672ad.2741d8","label":"Sheet 2"}]; + + localfilesystem.saveFlows(testFlow2).then(function() { + fs.existsSync(flowFileBackupPath).should.be.true; + fs.existsSync(defaultFlowFilePath).should.be.false; + fs.existsSync(flowFilePath).should.be.true; + var backupContent = fs.readFileSync(flowFileBackupPath,'utf8'); + content.should.equal(backupContent); + var content2 = fs.readFileSync(flowFilePath,'utf8'); + content2.should.not.equal(backupContent); + done(); + + }).otherwise(function(err) { + done(err); + }); + + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + + + }); + + it('should handle missing credentials', function(done) { + var flowFile = 'test.json'; + var flowFilePath = path.join(userDir,flowFile); + var credFile = path.join(userDir,"test_cred.json"); + localfilesystem.init({userDir:userDir, flowFile:flowFilePath}).then(function() { + fs.existsSync(credFile).should.be.false; + + localfilesystem.getCredentials().then(function(creds) { + creds.should.eql({}); + done(); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }); + + it('should handle credentials', function(done) { + var flowFile = 'test.json'; + var flowFilePath = path.join(userDir,flowFile); + var credFile = path.join(userDir,"test_cred.json"); + + localfilesystem.init({userDir:userDir, flowFile:flowFilePath}).then(function() { + + fs.existsSync(credFile).should.be.false; + + var credentials = {"abc":{"type":"creds"}}; + + localfilesystem.saveCredentials(credentials).then(function() { + fs.existsSync(credFile).should.be.true; + localfilesystem.getCredentials().then(function(creds) { + creds.should.eql(credentials); + done(); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }); + + it('should return an empty list of library flows',function(done) { + localfilesystem.init({userDir:userDir}).then(function() { + localfilesystem.getAllFlows().then(function(flows) { + flows.should.eql({}); + done(); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }); + + it('should return a valid list of library flows',function(done) { + localfilesystem.init({userDir:userDir}).then(function() { + var flowLib = path.join(userDir,"lib","flows"); + fs.closeSync(fs.openSync(path.join(flowLib,"A.json"),"w")); + fs.closeSync(fs.openSync(path.join(flowLib,"B.json"),"w")); + fs.mkdirSync(path.join(flowLib,"C")); + fs.closeSync(fs.openSync(path.join(flowLib,"C","D.json"),"w")); + var testFlowsList = {"d":{"C":{"f":["D"]}},"f":["A","B"]}; + + localfilesystem.getAllFlows().then(function(flows) { + flows.should.eql(testFlowsList); + done(); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }); + + it('should fail a non-existent flow', function(done) { + localfilesystem.init({userDir:userDir}).then(function() { + localfilesystem.getFlow("a/b/c.json").then(function(flow) { + should.fail(flow,"No flow","Flow found"); + }).otherwise(function(err) { + // err should be null, so this will pass + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }); + + it('should return a flow',function(done) { + localfilesystem.init({userDir:userDir}).then(function() { + var testflowString = JSON.stringify(testFlow); + localfilesystem.saveFlow("a/b/c/d.json",testflowString).then(function() { + localfilesystem.getFlow("a/b/c/d.json").then(function(flow) { + flow.should.eql(testflowString); + done(); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }); + + it('should return an empty list of library objects',function(done) { + localfilesystem.init({userDir:userDir}).then(function() { + localfilesystem.getLibraryEntry('object','').then(function(flows) { + flows.should.eql({}); + done(); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }); + + it('should return an error for a non-existent library object',function(done) { + localfilesystem.init({userDir:userDir}).then(function() { + localfilesystem.getLibraryEntry('object','A/B').then(function(flows) { + should.fail(null,null,"non-existent flow"); + }).otherwise(function(err) { + should.exist(err); + done(); + }); + }).otherwise(function(err) { + done(err); + }); + }); + + function createObjectLibrary() { + var objLib = path.join(userDir,"lib","object"); + fs.mkdirSync(objLib); + fs.mkdirSync(path.join(objLib,"A")); + fs.mkdirSync(path.join(objLib,"B")); + fs.mkdirSync(path.join(objLib,"B","C")); + fs.writeFileSync(path.join(objLib,"file1.js"),"// abc: def\n// not a metaline \n\n Hi",'utf8'); + fs.writeFileSync(path.join(objLib,"B","file2.js"),"// ghi: jkl\n// not a metaline \n\n Hi",'utf8'); + } + + it('should return a directory listing of library objects',function(done) { + localfilesystem.init({userDir:userDir}).then(function() { + createObjectLibrary(); + + localfilesystem.getLibraryEntry('object','').then(function(flows) { + flows.should.eql([ 'A', 'B', { abc: 'def', fn: 'file1.js' } ]); + localfilesystem.getLibraryEntry('object','B').then(function(flows) { + flows.should.eql([ 'C', { ghi: 'jkl', fn: 'file2.js' } ]); + localfilesystem.getLibraryEntry('object','B/C').then(function(flows) { + flows.should.eql([]); + done(); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }); + + it('should return a library object',function(done) { + localfilesystem.init({userDir:userDir}).then(function() { + createObjectLibrary(); + localfilesystem.getLibraryEntry('object','B/file2.js').then(function(body) { + body.should.eql("// not a metaline \n\n Hi"); + done(); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }); + + it('should return a newly saved library object',function(done) { + localfilesystem.init({userDir:userDir}).then(function() { + createObjectLibrary(); + localfilesystem.getLibraryEntry('object','B').then(function(flows) { + flows.should.eql([ 'C', { ghi: 'jkl', fn: 'file2.js' } ]); + localfilesystem.saveLibraryEntry('object','B/D/file3.js',{mno:'pqr'},"// another non meta line\n\n Hi There").then(function() { + localfilesystem.getLibraryEntry('object','B/D').then(function(flows) { + flows.should.eql([ { mno: 'pqr', fn: 'file3.js' } ]); + localfilesystem.getLibraryEntry('object','B/D/file3.js').then(function(body) { + body.should.eql("// another non meta line\n\n Hi There"); + done(); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }).otherwise(function(err) { + done(err); + }); + }); + +}); diff --git a/dgbuilder/test/red/ui_spec.js b/dgbuilder/test/red/ui_spec.js new file mode 100644 index 00000000..b9de7bff --- /dev/null +++ b/dgbuilder/test/red/ui_spec.js @@ -0,0 +1,177 @@ +/** + * 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(); + }); + + }); +}); diff --git a/dgbuilder/test/red/util_spec.js b/dgbuilder/test/red/util_spec.js new file mode 100644 index 00000000..5200ef12 --- /dev/null +++ b/dgbuilder/test/red/util_spec.js @@ -0,0 +1,70 @@ +/** + * 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 util = require("../../red/util"); + +describe("red/util", function() { + describe('ensureString', function() { + it('strings are preserved', function() { + util.ensureString('string').should.equal('string'); + }); + it('Buffer is converted', function() { + var s = util.ensureString(new Buffer('foo')); + s.should.equal('foo'); + (typeof s).should.equal('string'); + }); + it('Object is converted to JSON', function() { + var s = util.ensureString({foo: "bar"}); + (typeof s).should.equal('string'); + should.deepEqual(JSON.parse(s), {foo:"bar"}); + }); + it('stringifies other things', function() { + var s = util.ensureString(123); + (typeof s).should.equal('string'); + s.should.equal('123'); + }); + }); + + describe('ensureBuffer', function() { + it('Buffers are preserved', function() { + var b = new Buffer(''); + util.ensureBuffer(b).should.equal(b); + }); + it('string is converted', function() { + var b = util.ensureBuffer('foo'); + var expected = new Buffer('foo'); + for (var i = 0; i < expected.length; i++) { + b[i].should.equal(expected[i]); + } + Buffer.isBuffer(b).should.equal(true); + }); + it('Object is converted to JSON', function() { + var obj = {foo: "bar"} + var b = util.ensureBuffer(obj); + Buffer.isBuffer(b).should.equal(true); + should.deepEqual(JSON.parse(b), obj); + }); + it('stringifies other things', function() { + var b = util.ensureBuffer(123); + Buffer.isBuffer(b).should.equal(true); + var expected = new Buffer('123'); + for (var i = 0; i < expected.length; i++) { + b[i].should.equal(expected[i]); + } + }); + }); +}); + diff --git a/dgbuilder/test/resources/70-HTML-test-file.html b/dgbuilder/test/resources/70-HTML-test-file.html new file mode 100644 index 00000000..a0932e15 --- /dev/null +++ b/dgbuilder/test/resources/70-HTML-test-file.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<body> + +<h1>This is a test page for node 70-HTML</h1> + +<p>There's nothing to read here.</p> + +<ol id="colours"> + <li>Blue</li> + <li>Red</li> +</ol> + +<ul id="fruits"> + <li>Apple</li> + <li>Pear</li> +</ul> + +<ul id="vegetables"> + <li>Potato</li> + <li>Parsnip</li> +</ul> + +</body> +</html>
\ No newline at end of file |