Initial commit for OpenECOMP SDN-C OA&M

Change-Id: I7ab579fd0d206bf356f36d52dcdf4f71f1fa2680
Signed-off-by: Timoney, Daniel (dt5972) <dtimoney@att.com>

Former-commit-id: 2a9f0edd09581f907e62ec4689b5ac94dd5382ba
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 0000000..68e960a
--- /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 0000000..7d2b5ac
--- /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 0000000..59a5c64
--- /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 0000000..dce4d83
--- /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 0000000..2475926
--- /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 0000000..5225528
--- /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 0000000..0fb0aaf
--- /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 0000000..6ac54bd
--- /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 0000000..3d10461
--- /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 0000000..091bf40
--- /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 0000000..dcb866e
--- /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 0000000..81c1a2c
--- /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 0000000..b637ede
--- /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 0000000..e812141
--- /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 0000000..5359644
--- /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 0000000..55747c0
--- /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 0000000..abc823e
--- /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 0000000..cd3148a
--- /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 0000000..59a29af
--- /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 0000000..ac9235d
--- /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 0000000..8af249b
--- /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 0000000..4212fd5
--- /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 0000000..5856ada
--- /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 0000000..97dbf17
--- /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 0000000..bfa3b65
--- /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 0000000..66b6590
--- /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 0000000..faf61a8
--- /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 0000000..9a0f6f7
--- /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 0000000..756dc13
--- /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 0000000..f61fd55
--- /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 0000000..b20249c
--- /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 0000000..fb4cbad
--- /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 0000000..4b60ba8
--- /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 0000000..a131170
--- /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 0000000..b9de7bf
--- /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 0000000..5200ef1
--- /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]);
+            }
+        });
+    });
+});
+