diff --git a/public/red/ui/editor.js b/public/red/ui/editor.js index 746f780bb..d41bd2a9a 100644 --- a/public/red/ui/editor.js +++ b/public/red/ui/editor.js @@ -236,7 +236,12 @@ RED.editor = (function() { //TODO: move this to RED.library var flowName = $("#node-input-filename").val(); if (!/^\s*$/.test(flowName)) { - $.post('library/flows/'+flowName,$("#node-input-filename").attr('nodes'),function() { + $.ajax({ + url:'library/flows/'+flowName, + type: "POST", + data: $("#node-input-filename").attr('nodes'), + contentType: "application/json; charset=utf-8" + }).done(function() { RED.library.loadFlowLibrary(); RED.notify("Saved nodes","success"); }); diff --git a/red/api/flows.js b/red/api/flows.js new file mode 100644 index 000000000..9be6763b0 --- /dev/null +++ b/red/api/flows.js @@ -0,0 +1,38 @@ +/** + * 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 express = require('express'); +var fs = require("fs"); +var events = require("../events"); +var path = require("path"); +var util = require("util"); + +var redNodes = require("../nodes"); +var settings = require("../settings"); + +module.exports = { + get: function(req,res) { + res.json(redNodes.getFlows()); + }, + post: function(req,res) { + var flows = req.body; + redNodes.setFlows(flows).then(function() { + res.send(204); + }).otherwise(function(err) { + util.log("[red] Error saving flows : "+err); + res.send(500,err.message); + }); + } +} diff --git a/red/api/index.js b/red/api/index.js new file mode 100644 index 000000000..f0958b3b0 --- /dev/null +++ b/red/api/index.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 express = require("express"); +var util = require('util'); + +var ui = require("./ui"); +var nodes = require("./nodes"); +var flows = require("./flows"); +var library = require("./library"); + +var settings = require("../settings"); + +var errorHandler = function(err,req,res,next) { + //TODO: standardize json response + res.send(400,err.toString()); +} + +function init(adminApp) { + + adminApp.use(express.json()); + + library.init(adminApp); + + // Editor + if (!settings.disableEditor) { + adminApp.get("/",ui.ensureSlash); + adminApp.get("/icons/:icon",ui.icon); + adminApp.get("/settings",ui.settings); + adminApp.use("/",ui.editor); + } + + // Flows + adminApp.get("/flows",flows.get); + adminApp.post("/flows",flows.post); + + // Nodes + adminApp.get("/nodes",nodes.getAll); + adminApp.post("/nodes",nodes.post); + + adminApp.get("/nodes/:id",nodes.get); + adminApp.put("/nodes/:id",nodes.put); + adminApp.delete("/nodes/:id",nodes.delete); + + // Library + adminApp.post(new RegExp("/library/flows\/(.*)"),library.post); + adminApp.get("/library/flows",library.getAll); + adminApp.get(new RegExp("/library/flows\/(.*)"),library.get); + + + // Error Handler + adminApp.use(errorHandler); +} + +module.exports = { + init: init +} diff --git a/red/api/library.js b/red/api/library.js new file mode 100644 index 000000000..ec0bdcaf6 --- /dev/null +++ b/red/api/library.js @@ -0,0 +1,106 @@ +/** + * 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 util = require("util"); + +var redApp = null; +var storage = require("../storage"); + +function createLibrary(type) { + if (redApp) { + redApp.get(new RegExp("/library/"+type+"($|\/(.*))"),function(req,res) { + var path = req.params[1]||""; + storage.getLibraryEntry(type,path).then(function(result) { + if (typeof result === "string") { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write(result); + res.end(); + } else { + res.json(result); + } + }).otherwise(function(err) { + if (err) { + util.log("[red] Error loading library entry '"+path+"' : "+err); + if (err.message.indexOf('forbidden') === 0) { + res.send(403); + return; + } + } + res.send(404); + }); + }); + + redApp.post(new RegExp("/library/"+type+"\/(.*)"),function(req,res) { + var path = req.params[0]; + var fullBody = ''; + req.on('data', function(chunk) { + fullBody += chunk.toString(); + }); + req.on('end', function() { + storage.saveLibraryEntry(type,path,req.query,fullBody).then(function() { + res.send(204); + }).otherwise(function(err) { + util.log("[red] Error saving library entry '"+path+"' : "+err); + if (err.message.indexOf('forbidden') === 0) { + res.send(403); + return; + } + res.send(500); + }); + }); + }); + } +} +module.exports = { + init: function(app) { + redApp = app; + }, + register: createLibrary, + + getAll: function(req,res) { + storage.getAllFlows().then(function(flows) { + res.json(flows); + }); + }, + get: function(req,res) { + storage.getFlow(req.params[0]).then(function(data) { + res.set('Content-Type', 'application/json'); + res.send(data); + }).otherwise(function(err) { + if (err) { + util.log("[red] Error loading flow '"+req.params[0]+"' : "+err); + if (err.message.indexOf('forbidden') === 0) { + res.send(403); + return; + } + } + res.send(404); + }); + }, + post: function(req,res) { + var flow = JSON.stringify(req.body); + storage.saveFlow(req.params[0],flow).then(function() { + res.send(204); + }).otherwise(function(err) { + util.log("[red] Error loading flow '"+req.params[0]+"' : "+err); + if (err.message.indexOf('forbidden') === 0) { + res.send(403); + return; + } + res.send(500); + }); + } +} diff --git a/red/api/nodes.js b/red/api/nodes.js new file mode 100644 index 000000000..a55e295d8 --- /dev/null +++ b/red/api/nodes.js @@ -0,0 +1,155 @@ +/** + * 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 express = require('express'); +var fs = require("fs"); +var path = require("path"); +var when = require('when'); +var util = require('util'); + +var events = require("../events"); +var redNodes = require("../nodes"); +var comms = require("../comms"); +var server = require("../server"); + +var settings = require("../settings"); + +module.exports = { + getAll: function(req,res) { + if (req.get("accept") == "application/json") { + res.json(redNodes.getNodeList()); + } else { + res.send(redNodes.getNodeConfigs()); + } + }, + post: function(req,res) { + if (!settings.available()) { + res.send(400,new Error("Settings unavailable").toString()); + return; + } + var node = req.body; + var promise; + if (node.file) { + promise = redNodes.addNode(node.file).then(server.reportAddedModules); + } else if (node.module) { + var module = redNodes.getNodeModuleInfo(node.module); + if (module) { + res.send(400,"Module already loaded"); + return; + } + promise = server.installModule(node.module); + } else { + res.send(400,"Invalid request"); + return; + } + promise.then(function(info) { + res.json(info); + }).otherwise(function(err) { + if (err.code === 404) { + res.send(404); + } else { + res.send(400,err.toString()); + } + }); + }, + + delete: function(req,res) { + if (!settings.available()) { + res.send(400,new Error("Settings unavailable").toString()); + return; + } + var id = req.params.id; + var removedNodes = []; + try { + var node = redNodes.getNodeInfo(id); + var promise = null; + if (!node) { + var module = redNodes.getNodeModuleInfo(id); + if (!module) { + res.send(404); + return; + } else { + promise = server.uninstallModule(id); + } + } else { + promise = when.resolve([redNodes.removeNode(id)]).then(server.reportRemovedModules); + } + + promise.then(function(removedNodes) { + res.json(removedNodes); + }).otherwise(function(err) { + res.send(400,err.toString()); + }); + } catch(err) { + res.send(400,err.toString()); + } + }, + + get: function(req,res) { + var id = req.params.id; + var result = null; + if (req.get("accept") == "application/json") { + result = redNodes.getNodeInfo(id); + } else { + result = redNodes.getNodeConfig(id); + } + if (result) { + res.send(result); + } else { + res.send(404); + } + }, + + put: function(req,res) { + if (!settings.available()) { + res.send(400,new Error("Settings unavailable").toString()); + return; + } + var body = req.body; + if (!body.hasOwnProperty("enabled")) { + res.send(400,"Invalid request"); + return; + } + try { + var info; + var id = req.params.id; + var node = redNodes.getNodeInfo(id); + if (!node) { + res.send(404); + } else if (!node.err && node.enabled === body.enabled) { + res.json(node); + } else { + if (body.enabled) { + info = redNodes.enableNode(id); + } else { + info = redNodes.disableNode(id); + } + if (info.enabled == body.enabled && !info.err) { + comms.publish("node/"+(body.enabled?"enabled":"disabled"),info,false); + util.log("[red] "+(body.enabled?"Enabled":"Disabled")+" node types:"); + for (var i=0;i 0) { - util.log("[red] Added node types:"); - for (var i=0;i 0) { + util.log("[red] Added node types:"); + for (var i=0;i"; + }); + request(app) + .get('/nodes') + .set('Accept', 'text/html') + .expect(200) + .expect("") + .end(function(err,res) { + getNodeConfigs.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + it('returns an individual node info', function(done) { + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo', function(id) { + return {"123":{id:"123"}}[id]; + }); + request(app) + .get('/nodes/123') + .set('Accept', 'application/json') + .expect(200) + .end(function(err,res) { + getNodeInfo.restore(); + if (err) { + throw err; + } + res.body.should.have.property("id","123"); + done(); + }); + }); + + it('returns an individual node configs', function(done) { + var getNodeConfig = sinon.stub(redNodes,'getNodeConfig', function(id) { + return {"123":""}[id]; + }); + request(app) + .get('/nodes/123') + .set('Accept', 'text/html') + .expect(200) + .expect("") + .end(function(err,res) { + getNodeConfig.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + it('returns 404 for unknown node', function(done) { + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo', function(id) { + return {"123":{id:"123"}}[id]; + }); + request(app) + .get('/nodes/456') + .set('Accept', 'application/json') + .expect(404) + .end(function(err,res) { + getNodeInfo.restore(); + if (err) { + throw err; + } + done(); + }); + }); + }); + + describe('install', function() { + + it('returns 400 if settings are unavailable', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return false; + }); + request(app) + .post('/nodes') + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + it('returns 400 if request is invalid', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + request(app) + .post('/nodes') + .send({}) + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + describe('by module', function() { + it('installs the module and returns node info', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return null; + }); + var installModule = sinon.stub(server,'installModule', function() { + return when.resolve({id:"123"}); + }); + + request(app) + .post('/nodes') + .send({module: 'foo'}) + .expect(200) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeModuleInfo.restore(); + installModule.restore(); + if (err) { + throw err; + } + res.body.should.have.property("id","123"); + done(); + }); + }); + + it('fails the install if already installed', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return {id:"123"}; + }); + var installModule = sinon.stub(server,'installModule', function() { + return when.resolve({id:"123"}); + }); + + request(app) + .post('/nodes') + .send({module: 'foo'}) + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeModuleInfo.restore(); + installModule.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + it('fails the install if module error', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return null; + }); + var installModule = sinon.stub(server,'installModule', function() { + return when.reject(new Error("test error")); + }); + + request(app) + .post('/nodes') + .send({module: 'foo'}) + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeModuleInfo.restore(); + installModule.restore(); + if (err) { + throw err; + } + res.text.should.equal("Error: test error"); + done(); + }); + }); + it('fails the install if module not found', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return null; + }); + var installModule = sinon.stub(server,'installModule', function() { + var err = new Error("test error"); + err.code = 404; + return when.reject(err); + }); + + request(app) + .post('/nodes') + .send({module: 'foo'}) + .expect(404) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeModuleInfo.restore(); + installModule.restore(); + if (err) { + throw err; + } + done(); + }); + }); + }); + }); + describe('delete', function() { + it('returns 400 if settings are unavailable', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return false; + }); + request(app) + .del('/nodes/123') + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + describe('by module', function() { + it('uninstalls the module and returns node info', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return null; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return {id:"123"}; + }); + var uninstallModule = sinon.stub(server,'uninstallModule', function() { + return when.resolve({id:"123"}); + }); + + request(app) + .del('/nodes/foo') + .expect(200) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + getNodeModuleInfo.restore(); + uninstallModule.restore(); + if (err) { + throw err; + } + res.body.should.have.property("id","123"); + done(); + }); + }); + + it('fails the uninstall if the module is not installed', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return null; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return null; + }); + + request(app) + .del('/nodes/foo') + .expect(404) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + getNodeModuleInfo.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + it('fails the uninstall if the module is not installed', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return null; + }); + var getNodeModuleInfo = sinon.stub(redNodes,'getNodeModuleInfo',function(id) { + return {id:"123"}; + }); + var uninstallModule = sinon.stub(server,'uninstallModule', function() { + return when.reject(new Error("test error")); + }); + + request(app) + .del('/nodes/foo') + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + getNodeModuleInfo.restore(); + uninstallModule.restore(); + if (err) { + throw err; + } + res.text.should.equal("Error: test error"); + done(); + }); + }); + }); + + }); + + describe('enable/disable', function() { + it('returns 400 if settings are unavailable', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return false; + }); + request(app) + .put('/nodes/123') + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + it('returns 400 for invalid payload', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + + request(app) + .put('/nodes/foo') + .send({}) + .expect(400) + .end(function(err,res) { + settingsAvailable.restore(); + if (err) { + throw err; + } + res.text.should.equal("Invalid request"); + + done(); + }); + }); + it('returns 404 for unknown node', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return null; + }); + + request(app) + .put('/nodes/foo') + .send({enabled:false}) + .expect(404) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + if (err) { + throw err; + } + done(); + }); + }); + + it('enables disabled node', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return {id:"123",enabled: false}; + }); + var enableNode = sinon.stub(redNodes,'enableNode',function(id) { + return {id:"123",enabled: true,types:['a']}; + }); + + request(app) + .put('/nodes/foo') + .send({enabled:true}) + .expect(200) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + enableNode.restore(); + if (err) { + throw err; + } + res.body.should.have.property("id","123"); + res.body.should.have.property("enabled",true); + + done(); + }); + }); + it('disables enabled node', function(done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return {id:"123",enabled: true}; + }); + var disableNode = sinon.stub(redNodes,'disableNode',function(id) { + return {id:"123",enabled: false,types:['a']}; + }); + + request(app) + .put('/nodes/foo') + .send({enabled:false}) + .expect(200) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + disableNode.restore(); + if (err) { + throw err; + } + res.body.should.have.property("id","123"); + res.body.should.have.property("enabled",false); + + done(); + }); + }); + describe('no-ops if already in the right state', function() { + function run(state,done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return {id:"123",enabled: state}; + }); + var enableNode = sinon.stub(redNodes,'enableNode',function(id) { + return {id:"123",enabled: true,types:['a']}; + }); + + var disableNode = sinon.stub(redNodes,'disableNode',function(id) { + return {id:"123",enabled: false,types:['a']}; + }); + + request(app) + .put('/nodes/foo') + .send({enabled:state}) + .expect(200) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + var enableNodeCalled = enableNode.called; + var disableNodeCalled = disableNode.called; + enableNode.restore(); + disableNode.restore(); + if (err) { + throw err; + } + enableNodeCalled.should.be.false; + disableNodeCalled.should.be.false; + res.body.should.have.property("id","123"); + res.body.should.have.property("enabled",state); + + done(); + }); + } + it('already enabled', function(done) { + run(true,done); + }); + it('already disabled', function(done) { + run(false,done); + }); + }); + describe('does not no-op if err on node', function() { + function run(state,done) { + var settingsAvailable = sinon.stub(settings,'available', function() { + return true; + }); + var getNodeInfo = sinon.stub(redNodes,'getNodeInfo',function(id) { + return {id:"123",enabled: state, err:"foo" }; + }); + var enableNode = sinon.stub(redNodes,'enableNode',function(id) { + return {id:"123",enabled: true,types:['a']}; + }); + + var disableNode = sinon.stub(redNodes,'disableNode',function(id) { + return {id:"123",enabled: false,types:['a']}; + }); + + request(app) + .put('/nodes/foo') + .send({enabled:state}) + .expect(200) + .end(function(err,res) { + settingsAvailable.restore(); + getNodeInfo.restore(); + var enableNodeCalled = enableNode.called; + var disableNodeCalled = disableNode.called; + enableNode.restore(); + disableNode.restore(); + if (err) { + throw err; + } + enableNodeCalled.should.be.equal(state); + disableNodeCalled.should.be.equal(!state); + res.body.should.have.property("id","123"); + res.body.should.have.property("enabled",state); + + done(); + }); + } + it('already enabled', function(done) { + run(true,done); + }); + it('already disabled', function(done) { + run(false,done); + }); + }); + }); + + +}); \ No newline at end of file diff --git a/test/red/api/ui_spec.js b/test/red/api/ui_spec.js new file mode 100644 index 000000000..139a1aa22 --- /dev/null +++ b/test/red/api/ui_spec.js @@ -0,0 +1,183 @@ +/** + * Copyright 2014 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var should = require("should"); +var request = require("supertest"); +var express = require("express"); +var fs = require("fs"); +var path = require("path"); + +var settings = require("../../../red/settings"); +var events = require("../../../red/events"); +var ui = require("../../../red/api/ui"); + + +describe("ui api", function() { + var app; + + + describe("slash handler", function() { + before(function() { + app = express(); + app.get("/foo",ui.ensureSlash,function(req,res) { res.send(200);}); + }); + it('redirects if the path does not end in a slash',function(done) { + request(app) + .get('/foo') + .expect(301,done); + }); + it('does not redirect if the path ends in a slash',function(done) { + request(app) + .get('/foo/') + .expect(200,done); + }); + }); + + describe("icon handler", function() { + before(function() { + app = express(); + app.get("/icons/:icon",ui.icon); + }); + + function binaryParser(res, callback) { + res.setEncoding('binary'); + res.data = ''; + res.on('data', function (chunk) { + res.data += chunk; + }); + res.on('end', function () { + callback(null, new Buffer(res.data, 'binary')); + }); + } + function compareBuffers(b1,b2) { + b1.length.should.equal(b2.length); + for (var i=0;i