Repository: node-red/node-red (js)

Issue:
nodesDir crashes Node-RED 3.0
### Current Behavior

I’m developing custom nodes in a directory specified with the **nodesDir** property in the settings.js configuration file. This set-up used to work up to Node-RED 2.2.2. However, I’ve just upgraded to Node-RED 3.0.2, and now the server crashes immediately on start-up, using a settings.js file that used to work before the upgrade.

### Expected Behavior

When the **nodesDir** property is used in the settings.js to specify a custom nodes directory, the server should not crash. If a problem is encountered, then the nature of this problem shall be described in the log output.

### Steps To Reproduce

If I use the following [settings.js](https://www.celtrio.com/varenne/node-red/ok-settings.js), Node-RED 3.0.2 starts normally. Please note that some values have been redacted in this file for confidentiality reasons. This first settings.js file works with Node-RED 2.2.2

However, if I use this second [settings.js](https://www.celtrio.com/varenne/node-red/ko-settings.js) file, then the Node-RED 3.0.2 server crashes on start-up with the following output:

```
Welcome to Node-RED
===================
1 Sep 20:18:04 - [info] Node-RED version: v3.0.2
1 Sep 20:18:04 - [info] Node.js  version: v16.17.0
1 Sep 20:18:04 - [info] Linux 5.10.0-9-amd64 x64 LE
1 Sep 20:18:04 - [info] Loading palette nodes
1 Sep 20:18:04 - [debug] Module: varenne-nodes 1.0beta1 /home/node-red/varenne-nodes
1 Sep 20:18:04 - [error] Failed to start server:
1 Sep 20:18:04 - [error] TypeError: Cannot read properties of undefined (reading 'forEach')
    at Object.addModule (/usr/lib/node_modules/node-red/node_modules/@node-red/registry/lib/registry.js:195:27)
    at /usr/lib/node_modules/node-red/node_modules/@node-red/registry/lib/loader.js:153:34
```

The only difference between these two settings.js files is the following line, which is present when the server crashes.

`nodesDir: '/home/node-red/varenne-nodes'`


### Example flow

With **nodesDir** set, Node-RED 3.0.2 crashes on start-up, even with an empty flow.


### Environment

- Node-RED version: 3.0.2
- Node.js version: v16.17.0
- npm version: 8.19.0
- Platform/OS: Linux 5.10.0-9-amd64 #1 SMP Debian 5.10.70-1 (2021-09-30) x86_64 GNU/Linux
- Browser: Not browser related


Pull request:
Fix nodesDir scan when node package has js/html in sub dir to package.json
fixes #3861 

## Types of changes

- [x] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)

First, please read through the issue detailed in [this comment](https://github.com/node-red/node-red/issues/3861#issuecomment-1236124417)

## Proposed changes

* Normalise path before splitting (`loader.js`)
* Skip calling statSync on empty string  (`loader.js`)
* Shortcut scanning of a directories files/folders if the directory contains a package.json (`localfilesystem.js`)
* Prevent crashing node-red when `set.types` is not set  (`registry.js`)
* Print out which node caused the error 
    * BEFORE: 
    ```
    TypeError: Cannot read properties of undefined (reading 'forEach')
    at Object.addModule (/usr/lib/node_modules/node-red/node_modules/@node-red/registry/lib/registry.js:195:27)
    at /usr/lib/node_modules/node-red/node_modules/@node-red/registry/lib/loader.js:153:34
    ```
    * AFTER:    (Updated 2022/09/29, after commit e6b3793)
    ```
    29 Sep 21:25:17 - [warn] [node-red/lower-case] Set does not have any types. name: 'lower-case', module: 'node-red', file: 'C:\Users\user\repos\_node-red-issue-3861\lower-case.js'
    packages/node_modules/@node-red/util/lib/log.js:94
    29 Sep 21:25:31 - [warn] [node-red-node-google/google-api-config] 'google-api-config' already registered by module node-red-contrib-google-calendar
    packages/node_modules/@node-red/util/lib/log.js:94
    29 Sep 21:25:31 - [warn] [node-red-node-rbe/rbe] 'rbe' already registered by module node-red
    packages/node_modules/@node-red/util/lib/log.js:94
    29 Sep 21:25:31 - [warn] [@flowforge/nr-project-nodes/project-link] Error: Project Link nodes cannot be loaded outside of flowforge EE environment
    packages/node_modules/@node-red/util/lib/log.js:94
    29 Sep 21:25:57 - [warn] [lower-case/lower-case] Set does not have any types. name: 'lower-case', module: 'lower-case', file: 'C:\Users\user\repos\_node-red-issue-3861\lower-case\lower-case\lower-case.js'
    packages/node_modules/@node-red/util/lib/log.js:94
    29 Sep 21:26:06 - [warn] [@lowercase/lower-case2/lower-case] Set does not have any types. name: 'lower-case', module: '@lowercase/lower-case2', file: 'C:\Users\user\repos\_node-red-issue-3861\@lower-case\lower-case2\lower-case.js'
    ```

* Add more tests that load nodes with js/html files in a sub directory (`registry.js`)

## Checklist

- [x] I have read the [contribution guidelines](https://github.com/node-red/node-red/blob/master/CONTRIBUTING.md)
- [ ] For non-bugfix PRs, I have discussed this change on the forum/slack team.
- [x] I have run `grunt` to verify the unit tests pass
- [x] I have added suitable unit tests to cover the new/changed functionality


Patch:
diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json
index dc7858250..fb3458eed 100644
--- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json
+++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json
@@ -1168,7 +1168,8 @@
         "takeATour": "ツアーを開始",
         "start": "開始",
         "next": "次へ",
-        "welcomeTours": "ウェルカムツアー"
+        "welcomeTours": "ウェルカムツアー",
+        "tours": "ツアー"
     },
     "diagnostics": {
         "title": "システム情報"
diff --git a/packages/node_modules/@node-red/nodes/core/network/06-httpproxy.html b/packages/node_modules/@node-red/nodes/core/network/06-httpproxy.html
index 446cbd324..7819ab8ec 100644
--- a/packages/node_modules/@node-red/nodes/core/network/06-httpproxy.html
+++ b/packages/node_modules/@node-red/nodes/core/network/06-httpproxy.html
@@ -101,7 +101,6 @@
                             hostField.val(data.host);
                         }
                     },
-                    sortable: true,
                     removable: true
                 });
             if (this.noproxy) {
diff --git a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js
index 50b1d690f..9e6195b5d 100644
--- a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js
+++ b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js
@@ -474,6 +474,7 @@ module.exports = function(RED) {
             setIfHasProperty(opts, node, "protocolVersion", init);
             setIfHasProperty(opts, node, "keepalive", init);
             setIfHasProperty(opts, node, "cleansession", init);
+            setIfHasProperty(opts, node, "sessionExpiry", init);
             setIfHasProperty(opts, node, "topicAliasMaximum", init);
             setIfHasProperty(opts, node, "maximumPacketSize", init);
             setIfHasProperty(opts, node, "receiveMaximum", init);
@@ -483,11 +484,6 @@ module.exports = function(RED) {
             } else if (hasProperty(opts, "userProps")) {
                 node.userProperties = opts.userProps;
             }
-            if (hasProperty(opts, "sessionExpiry")) {
-                node.sessionExpiryInterval = opts.sessionExpiry;
-            } else if (hasProperty(opts, "sessionExpiryInterval")) {
-                node.sessionExpiryInterval = opts.sessionExpiryInterval
-            }
 
             function createLWT(topic, payload, qos, retain, v5opts, v5SubPropName) {
                 let message = undefined;
@@ -786,9 +782,7 @@ module.exports = function(RED) {
 
                         // Send any birth message
                         if (node.birthMessage) {
-                            setTimeout(() => {
-                                node.publish(node.birthMessage);
-                            }, 1);
+                            node.publish(node.birthMessage);
                         }
                     });
                     node._clientOn("reconnect", function() {
diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httpin.html b/packages/node_modules/@node-red/nodes/core/network/21-httpin.html
index f828077a1..ce2f81e12 100644
--- a/packages/node_modules/@node-red/nodes/core/network/21-httpin.html
+++ b/packages/node_modules/@node-red/nodes/core/network/21-httpin.html
@@ -227,7 +227,6 @@
                         }
                     });
                 },
-                sortable: true,
                 removable: true
             });
 
diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html
index eb0aeaf47..b9a44e1b4 100644
--- a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html
+++ b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html
@@ -417,7 +417,6 @@
                     });
 
                 },
-                sortable: true,
                 removable: true
             });
             if (node.headers) {
diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js
index 9eac4da91..b94d59dc7 100644
--- a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js
+++ b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js
@@ -86,7 +86,6 @@ in your Node-RED user directory (${RED.settings.userDir}).
         if (n.paytoqs === true || n.paytoqs === "query") { paytoqs = true; }
         else if (n.paytoqs === "body") { paytobody = true; }
 
-        node.insecureHTTPParser = n.insecureHTTPParser
 
         var prox, noprox;
         if (process.env.http_proxy) { prox = process.env.http_proxy; }
diff --git a/packages/node_modules/@node-red/nodes/locales/ja/messages.json b/packages/node_modules/@node-red/nodes/locales/ja/messages.json
index 76c8f8462..6e16daa6f 100644
--- a/packages/node_modules/@node-red/nodes/locales/ja/messages.json
+++ b/packages/node_modules/@node-red/nodes/locales/ja/messages.json
@@ -554,8 +554,7 @@
         },
         "status": {
             "requesting": "要求中"
-        },
-        "insecureHTTPParser": "厳密なHTTPパース処理を無効化"
+        }
     },
     "websocket": {
         "label": {
diff --git a/packages/node_modules/@node-red/registry/lib/loader.js b/packages/node_modules/@node-red/registry/lib/loader.js
index 61f28ab86..3f0487750 100644
--- a/packages/node_modules/@node-red/registry/lib/loader.js
+++ b/packages/node_modules/@node-red/registry/lib/loader.js
@@ -43,37 +43,40 @@ function load(disableNodePathScan) {
     return loadModuleFiles(modules);
 }
 
+function splitPath(p) {
+    return path.posix.normalize((p || '').replace(/\\/g, path.sep)).split(path.sep)
+}
 
 function loadModuleTypeFiles(module, type) {
     const things = module[type];
-    var first = true;
-    var promises = [];
-    for (var thingName in things) {
+    let first = true;
+    const promises = [];
+    for (let thingName in things) {
         /* istanbul ignore else */
         if (things.hasOwnProperty(thingName)) {
             if (module.name != "node-red" && first) {
                 // Check the module directory exists
                 first = false;
-                var fn = things[thingName].file;
-                var parts = fn.split("/");
-                var i = parts.length-1;
-                for (;i>=0;i--) {
-                    if (parts[i] == "node_modules") {
-                        break;
-                    }
+                let moduleFn = module.path
+                const fn = things[thingName].file
+                const parts = splitPath(fn)
+                const nmi = parts.indexOf('node_modules')
+                if(nmi > -1) {
+                    moduleFn = parts.slice(0,nmi+2).join(path.sep);
+                }
+                if (!moduleFn) {
+                    // shortcut - skip calling statSync on empty string 
+                    break; // Module not found, don't attempt to load its nodes
                 }
-                var moduleFn = parts.slice(0,i+2).join("/");
-
                 try {
-                    var stat = fs.statSync(moduleFn);
+                    const stat = fs.statSync(moduleFn);
                 } catch(err) {
-                    // Module not found, don't attempt to load its nodes
-                    break;
+                    break; // Module not found, don't attempt to load its nodes
                 }
             }
 
             try {
-                var promise;
+                let promise;
                 if (type === "nodes") {
                     promise = loadNodeConfig(things[thingName]);
                 } else if (type === "plugins") {
@@ -82,8 +85,7 @@ function loadModuleTypeFiles(module, type) {
                 promises.push(
                     promise.then(
                         (function() {
-                            var m = module.name;
-                            var n = thingName;
+                            const n = thingName;
                             return function(nodeSet) {
                                 things[n] = nodeSet;
                                 return nodeSet;
@@ -93,7 +95,6 @@ function loadModuleTypeFiles(module, type) {
                 );
             } catch(err) {
                 console.log(err)
-                //
             }
         }
     }
diff --git a/packages/node_modules/@node-red/registry/lib/localfilesystem.js b/packages/node_modules/@node-red/registry/lib/localfilesystem.js
index da4006ecc..edb805572 100644
--- a/packages/node_modules/@node-red/registry/lib/localfilesystem.js
+++ b/packages/node_modules/@node-red/registry/lib/localfilesystem.js
@@ -156,6 +156,16 @@ function scanDirForNodesModules(dir,moduleName,package) {
                 }
             }
         }
+
+        // if we have found a package.json, this IS a node_module, lets see if it is a node-red node
+        if (!isNodeRedModule && files.indexOf('package.json') > -1) {
+            package = getPackageDetails(dir) // get package details
+            if(package && package.isNodeRedModule) {
+                isNodeRedModule = true
+                files = ['package.json'] // shortcut the file scan
+            }
+        }
+
         for (let i=0;i<files.length;i++) {
             let fn = files[i];
             if (!isNodeRedModule && /^@/.test(fn)) {
diff --git a/packages/node_modules/@node-red/registry/lib/registry.js b/packages/node_modules/@node-red/registry/lib/registry.js
index baee42bf5..389285dbc 100644
--- a/packages/node_modules/@node-red/registry/lib/registry.js
+++ b/packages/node_modules/@node-red/registry/lib/registry.js
@@ -185,10 +185,17 @@ function loadNodeConfigs() {
 function addModule(module) {
     moduleNodes[module.name] = [];
     moduleConfigs[module.name] = module;
-    // console.log("registry.js.addModule",module.name,"user?",module.user,"usedBy",module.usedBy,"dependencies",module.dependencies)
-    for (var setName in module.nodes) {
+    for (const setName in module.nodes) {
         if (module.nodes.hasOwnProperty(setName)) {
-            var set = module.nodes[setName];
+            const set = module.nodes[setName];
+            if (!set.types) {
+                const err = new Error("Set has no types")
+                err.code = "set_has_no_types"
+                err.details = {
+                    ...set
+                }
+                set.err = err
+            }
             moduleNodes[module.name].push(set.name);
             nodeList.push(set.id);
             if (!set.err) {
diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js
index 88b3b8293..a886cd2ca 100644
--- a/packages/node_modules/@node-red/runtime/lib/index.js
+++ b/packages/node_modules/@node-red/runtime/lib/index.js
@@ -161,6 +161,8 @@ function start() {
                     for (i=0;i<nodeErrors.length;i+=1) {
                         if (nodeErrors[i].err.code === "type_already_registered") {
                             log.warn("["+nodeErrors[i].id+"] "+log._("server.type-already-registered",{type:nodeErrors[i].err.details.type,module: nodeErrors[i].err.details.moduleA}));
+                        } else if (nodeErrors[i].err.code === "set_has_no_types") {
+                            log.warn("["+nodeErrors[i].id+"] "+log._("server.set-has-no-types", nodeErrors[i].err.details));
                         } else {
                             log.warn("["+nodeErrors[i].id+"] "+nodeErrors[i].err);
                         }
diff --git a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json
index ecd010abb..ede92c876 100644
--- a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json
+++ b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json
@@ -20,6 +20,7 @@
         "errors-help": "Run with -v for details",
         "missing-modules": "Missing node modules:",
         "node-version-mismatch": "Node module cannot be loaded on this version. Requires: __version__ ",
+        "set-has-no-types": "Set does not have any types. name: '__name__', module: '__module__', file: '__file__'",
         "type-already-registered": "'__type__' already registered by module __module__",
         "removing-modules": "Removing modules from config",
         "added-types": "Added node types:",


Test patch:
diff --git a/test/nodes/core/network/21-httprequest_spec.js b/test/nodes/core/network/21-httprequest_spec.js
index 65f8f5814..688776289 100644
--- a/test/nodes/core/network/21-httprequest_spec.js
+++ b/test/nodes/core/network/21-httprequest_spec.js
@@ -31,8 +31,6 @@ var multer = require("multer");
 var RED = require("nr-test-utils").require("node-red/lib/red");
 var fs = require('fs-extra');
 var auth = require('basic-auth');
-const { version } = require("os");
-const net = require('net')
 
 describe('HTTP Request Node', function() {
     var testApp;
@@ -2267,73 +2265,4 @@ describe('HTTP Request Node', function() {
             });
         });
     });
-
-    describe('should parse broken headers', function(done) {
-
-        const versions = process.versions.node.split('.')
-
-        if (( versions[0] == 14 && versions[1] >= 20 ) ||
-            ( versions[0] == 16 && versions[1] >= 16 ) ||
-            ( versions[0] == 18 && versions[1] >= 5 ) ||
-            ( versions[0] > 18)) {
-            // only test if on new enough NodeJS version
-
-            let port = testPort++
-
-            let server;
-        
-            before(function() {
-                server = net.createServer(function (socket) {
-                    socket.write("HTTP/1.0 200\nContent-Type: text/plain\n\nHelloWorld")
-                    socket.end()
-                })
-
-                server.listen(port,'127.0.0.1', function(err) {
-                })
-            });
-            
-            after(function() {
-                server.close()
-            });
-
-            it('should accept broken headers', function (done) {
-                var flow = [{id:'n1',type:'http request',wires:[['n2']],method:'GET',ret:'obj',url:`http://localhost:${port}/`, insecureHTTPParser: true},
-                {id:"n2", type:"helper"}];
-                helper.load(httpRequestNode, flow, function() {
-                    var n1 = helper.getNode("n1");
-                    var n2 = helper.getNode("n2");
-                    n2.on('input', function(msg) {
-                        try {
-                            msg.payload.should.equal('HelloWorld')
-                            done()
-                        } catch (err) {
-                            done(err)
-                        }
-                    })
-                    n1.receive({payload: 'foo'})
-                });
-            });
-
-            it('should reject broken headers', function (done) {
-                var flow = [{id:'n1',type:'http request',wires:[['n2']],method:'GET',ret:'obj',url:`http://localhost:${port}/`},
-                {id:"n2", type:"helper"}];
-                helper.load(httpRequestNode, flow, function() {
-                    var n1 = helper.getNode("n1");
-                    var n2 = helper.getNode("n2");
-                    n2.on('input', function(msg) {
-                        try{
-                            msg.payload.should.equal(`RequestError: Parse Error: Missing expected CR after header value : http://localhost:${port}/`)
-                            done()
-                        } catch (err) {
-                            done(err)
-                        }
-                    })
-                    n1.receive({payload: 'foo'})
-                    
-                });
-            });
-        } else {
-            done()
-        }
-    });
 });
diff --git a/test/nodes/core/network/21-mqtt_spec.js b/test/nodes/core/network/21-mqtt_spec.js
index fd1309b74..655cc9c73 100644
--- a/test/nodes/core/network/21-mqtt_spec.js
+++ b/test/nodes/core/network/21-mqtt_spec.js
@@ -27,7 +27,7 @@ describe('MQTT Nodes', function () {
         } catch (error) { }
     });
 
-    it('should be loaded and have default values (MQTT V4)', function (done) {
+    it('should be loaded and have default values', function (done) {
         this.timeout = 2000;
         const { flow, nodes } = buildBasicMQTTSendRecvFlow({ id: "mqtt.broker", name: "mqtt_broker", autoConnect: false }, { id: "mqtt.in", topic: "in_topic" }, { id: "mqtt.out", topic: "out_topic" });
         helper.load(mqttNodes, flow, function () {
@@ -61,52 +61,6 @@ describe('MQTT Nodes', function () {
                 mqttBroker.options.clientId.should.containEql('nodered_');
                 mqttBroker.options.should.have.property('keepalive').type("number");
                 mqttBroker.options.should.have.property('reconnectPeriod').type("number");
-                //as this is not a v5 connection, ensure v5 properties are not present
-                mqttBroker.options.should.not.have.property('protocolVersion', 5);
-                mqttBroker.options.should.not.have.property('properties');
-                done();
-            } catch (error) {
-                done(error)
-            }
-        });
-    });
-    it('should be loaded and have default values (MQTT V5)', function (done) {
-        this.timeout = 2000;
-        const { flow, nodes } = buildBasicMQTTSendRecvFlow({ id: "mqtt.broker", name: "mqtt_broker", autoConnect: false, cleansession: false, clientid: 'clientid', keepalive: 35, sessionExpiry: '6000', protocolVersion: '5', userProps: {"prop": "val"}}, { id: "mqtt.in", topic: "in_topic" }, { id: "mqtt.out", topic: "out_topic" });
-        helper.load(mqttNodes, flow, function () {
-            try {
-                const mqttIn = helper.getNode("mqtt.in");
-                const mqttOut = helper.getNode("mqtt.out");
-                const mqttBroker = helper.getNode("mqtt.broker");
-
-                should(mqttIn).be.type("object", "mqtt in node should be an object")
-                mqttIn.should.have.property('broker', nodes.mqtt_broker.id); //should be the id of the broker node
-                mqttIn.should.have.property('datatype', 'utf8'); //default: 'utf8'
-                mqttIn.should.have.property('isDynamic', false);  //default: false
-                mqttIn.should.have.property('inputs', 0); //default: 0 
-                mqttIn.should.have.property('qos', 2); //default: 2
-                mqttIn.should.have.property('topic', "in_topic");
-                mqttIn.should.have.property('wires', [["helper.node"]]);
-
-                should(mqttOut).be.type("object", "mqtt out node should be an object")
-                mqttOut.should.have.property('broker', nodes.mqtt_broker.id); //should be the id of the broker node
-                mqttOut.should.have.property('topic', "out_topic");
-
-                should(mqttBroker).be.type("object", "mqtt broker node should be an object")
-                mqttBroker.should.have.property('broker', BROKER_HOST);
-                mqttBroker.should.have.property('port', BROKER_PORT);
-                mqttBroker.should.have.property('brokerurl');
-                // mqttBroker.should.have.property('autoUnsubscribe', true);//default: true 
-                mqttBroker.should.have.property('autoConnect', false);//Set "autoConnect:false" in brokerOptions 
-                mqttBroker.should.have.property('options');
-                mqttBroker.options.should.have.property('clean', false);
-                mqttBroker.options.should.have.property('clientId', 'clientid');
-                mqttBroker.options.should.have.property('keepalive').type("number", 35);
-                mqttBroker.options.should.have.property('reconnectPeriod').type("number");
-                //as this IS a v5 connection, ensure v5 properties are not present
-                mqttBroker.options.should.have.property('protocolVersion', 5);
-                mqttBroker.options.should.have.property('properties');
-                mqttBroker.options.properties.should.have.property('sessionExpiryInterval');
                 done();
             } catch (error) {
                 done(error)
@@ -219,7 +173,7 @@ describe('MQTT Nodes', function () {
             topic: nextTopic(),
             payload: '{prop:"value3", "num":3}', // send invalid JSON ...
         }
-        const hooks = { done: null, beforeLoad: null, afterLoad: null, afterConnect: null }
+        const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null }
         hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => {
             helperNode.on("input", function (msg) {
                 try {
@@ -345,7 +299,7 @@ describe('MQTT Nodes', function () {
             topic: nextTopic(),
             payload: '{prop:"value3", "num":3}', contentType: "application/json", // send invalid JSON ...
         }
-        const hooks = { done: null, beforeLoad: null, afterLoad: null, afterConnect: null }
+        const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null }
         hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => {
             helperNode.on("input", function (msg) {
                 try {
@@ -431,7 +385,7 @@ describe('MQTT Nodes', function () {
         if (skipTests) { return this.skip() }
         this.timeout = 2000;
         const options = {}
-        const hooks = { beforeLoad: null, afterLoad: null, afterConnect: null }
+        const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null }
         hooks.beforeLoad = (flow) => { //add a status node pointed at MQTT Out node (to watch for connection status change)
             flow.push({ "id": "status.node", "type": "status", "name": "status_node", "scope": ["mqtt.out"], "wires": [["helper.node"]] });//add status node to watch mqtt_out
         }
@@ -462,31 +416,18 @@ describe('MQTT Nodes', function () {
         this.timeout = 2000;
         const baseTopic = nextTopic();
         const brokerOptions = {
-            autoConnect: false,
             protocolVersion: 4,
             birthTopic: baseTopic + "/birth",
-            birthPayload: "broker birth",
+            birthPayload: "broker connected",
             birthQos: 2,
         }
-        const expectMsg = {
+        const options = {};
+        const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null };
+        options.expectMsg = {
             topic: brokerOptions.birthTopic,
             payload: brokerOptions.birthPayload,
             qos: brokerOptions.birthQos
         };
-        const options = { };
-        const hooks = { };
-        hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => {
-            helperNode.on("input", function (msg) {
-                try {
-                    compareMsgToExpected(msg, expectMsg);
-                    done();
-                } catch (error) {
-                    done(error)
-                }
-            })
-            mqttIn.receive({ "action": "connect" }); //now request connect action
-            return true; //handled
-        }       
         testSendRecv(brokerOptions, { topic: brokerOptions.birthTopic }, {}, options, hooks);
     });
     itConditional('should publish close message', function (done) {
@@ -725,7 +666,6 @@ function buildMQTTBrokerNode(id, name, brokerHost, brokerPort, options) {
     node.cleansession = String(options.cleansession) == "false" ? false : true;
     node.autoUnsubscribe = String(options.autoUnsubscribe) == "false" ? false : true;
     node.autoConnect = String(options.autoConnect) == "false" ? false : true;
-    node.sessionExpiry = options.sessionExpiry ? options.sessionExpiry : undefined;
 
     if (options.birthTopic) {
         node.birthTopic = options.birthTopic;
@@ -820,8 +760,8 @@ function waitBrokerConnect(broker, timeLimit) {
     let waitConnected = (broker, timeLimit) => {
         const brokers = Array.isArray(broker) ? broker : [broker];
         timeLimit = timeLimit || 1000;
+        let timer, resolved = false;
         return new Promise( (resolve, reject) => {
-            let timer, resolved = false;
             timer = wait();
             function wait() {
                 if (brokers.every(e => e.connected == true)) {
diff --git a/test/unit/@node-red/registry/lib/localfilesystem_spec.js b/test/unit/@node-red/registry/lib/localfilesystem_spec.js
index 3b1bb63f3..3ce74c158 100644
--- a/test/unit/@node-red/registry/lib/localfilesystem_spec.js
+++ b/test/unit/@node-red/registry/lib/localfilesystem_spec.js
@@ -329,17 +329,36 @@ describe("red/nodes/registry/localfilesystem",function() {
             localfilesystem.init({nodesDir:[nodesDir2]});
             const nodeModule = localfilesystem.getModuleFiles();
             const loaded = Object.keys(nodeModule)
-            loaded.should.have.a.property("length", 3)
             loaded.indexOf('@test/testnode').should.greaterThan(-1, "Should load @test/testnode")
+            loaded.indexOf('lower-case').should.greaterThan(-1, "Should load lower-case")
+            loaded.indexOf('@lowercase/lower-case2').should.greaterThan(-1, "Should load @lowercase/lower-case2")
             loaded.indexOf('testnode2').should.greaterThan(-1, "Should load testnode2")
             loaded.indexOf('test-theme2').should.greaterThan(-1, "Should load test-theme2")
+            loaded.should.have.a.property("length", 5)
 
+            // scoped module with nodes in same dir as package.json
             nodeModule['@test/testnode'].should.have.a.property('name','@test/testnode');
             nodeModule['@test/testnode'].should.have.a.property('version','1.0.0');
             nodeModule['@test/testnode'].should.have.a.property('nodes');
             nodeModule['@test/testnode'].should.have.a.property('path');
             nodeModule['@test/testnode'].should.have.a.property('user', false);
 
+            // node-red module with nodes in sub dir
+            nodeModule['@lowercase/lower-case2'].should.have.a.property('name','@lowercase/lower-case2');
+            nodeModule['@lowercase/lower-case2'].should.have.a.property('version','2.0.0');
+            nodeModule['@lowercase/lower-case2'].should.have.a.property('nodes');
+            nodeModule['@lowercase/lower-case2'].nodes.should.have.a.property('lower-case');
+            nodeModule['@lowercase/lower-case2'].should.have.a.property('path');
+            nodeModule['@lowercase/lower-case2'].should.have.a.property('user', false);
+
+            // scoped module with nodes in sub dir
+            nodeModule['lower-case'].should.have.a.property('name', 'lower-case');
+            nodeModule['lower-case'].should.have.a.property('version','1.0.0');
+            nodeModule['lower-case'].should.have.a.property('nodes');
+            nodeModule['lower-case'].nodes.should.have.a.property('lower-case');
+            nodeModule['lower-case'].should.have.a.property('path');
+            nodeModule['lower-case'].should.have.a.property('user', false);
+
             nodeModule['testnode2'].should.have.a.property('name','testnode2');
             nodeModule['testnode2'].should.have.a.property('version','1.0.0');
             nodeModule['testnode2'].should.have.a.property('nodes');
diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/@lower-case2/lower-case2/lower-case.html b/test/unit/@node-red/registry/lib/resources/nodesDir2/@lower-case2/lower-case2/lower-case.html
new file mode 100644
index 000000000..617f48491
--- /dev/null
+++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/@lower-case2/lower-case2/lower-case.html
@@ -0,0 +1,26 @@
+<script type="text/javascript">
+    RED.nodes.registerType('lower-case2',{
+        category: 'function',
+        color: '#a6bbcf',
+        defaults: {
+            name: {value:""}
+        },
+        inputs:1,
+        outputs:1,
+        icon: "file.png",
+        label: function() {
+            return this.name||"lower-case2";
+        }
+    });
+</script>
+
+<script type="text/html" data-template-name="lower-case2">
+    <div class="form-row">
+        <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
+        <input type="text" id="node-input-name" placeholder="Name">
+    </div>
+</script>
+
+<script type="text/html" data-help-name="lower-case2">
+    <p>A simple node that converts the message payloads into all lower-case2 characters</p>
+</script>
\ No newline at end of file
diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/@lower-case2/lower-case2/lower-case.js b/test/unit/@node-red/registry/lib/resources/nodesDir2/@lower-case2/lower-case2/lower-case.js
new file mode 100644
index 000000000..73579ba04
--- /dev/null
+++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/@lower-case2/lower-case2/lower-case.js
@@ -0,0 +1,11 @@
+module.exports = function(RED) {
+function LowerCaseNode(config) {
+        RED.nodes.createNode(this,config);
+        var node = this;
+        node.on('input', function(msg) {
+            msg.payload = msg.payload.toLowerCase();
+            node.send(msg);
+        });
+    }
+    RED.nodes.registerType("lower-case2",LowerCaseNode);
+}
\ No newline at end of file
diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/@lower-case2/package.json b/test/unit/@node-red/registry/lib/resources/nodesDir2/@lower-case2/package.json
new file mode 100644
index 000000000..6b6ce9aa9
--- /dev/null
+++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/@lower-case2/package.json
@@ -0,0 +1,9 @@
+{
+    "name" : "@lowercase/lower-case2",
+    "node-red" : {
+        "nodes": {
+            "lower-case": "lower-case2/lower-case.js"
+        }
+    },
+    "version": "2.0.0"
+}
\ No newline at end of file
diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/lower-case/lower-case/lower-case.html b/test/unit/@node-red/registry/lib/resources/nodesDir2/lower-case/lower-case/lower-case.html
new file mode 100644
index 000000000..e57d51131
--- /dev/null
+++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/lower-case/lower-case/lower-case.html
@@ -0,0 +1,26 @@
+<script type="text/javascript">
+    RED.nodes.registerType('lower-case',{
+        category: 'function',
+        color: '#a6bbcf',
+        defaults: {
+            name: {value:""}
+        },
+        inputs:1,
+        outputs:1,
+        icon: "file.png",
+        label: function() {
+            return this.name||"lower-case";
+        }
+    });
+</script>
+
+<script type="text/html" data-template-name="lower-case">
+    <div class="form-row">
+        <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
+        <input type="text" id="node-input-name" placeholder="Name">
+    </div>
+</script>
+
+<script type="text/html" data-help-name="lower-case">
+    <p>A simple node that converts the message payloads into all lower-case characters</p>
+</script>
diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/lower-case/lower-case/lower-case.js b/test/unit/@node-red/registry/lib/resources/nodesDir2/lower-case/lower-case/lower-case.js
new file mode 100644
index 000000000..006b35eb6
--- /dev/null
+++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/lower-case/lower-case/lower-case.js
@@ -0,0 +1,11 @@
+module.exports = function(RED) {
+    function LowerCaseNode(config) {
+        RED.nodes.createNode(this,config);
+        var node = this;
+        node.on('input', function(msg) {
+            msg.payload = msg.payload.toLowerCase();
+            node.send(msg);
+        });
+    }
+    RED.nodes.registerType("lower-case",LowerCaseNode);
+}
diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/lower-case/package.json b/test/unit/@node-red/registry/lib/resources/nodesDir2/lower-case/package.json
new file mode 100644
index 000000000..a632eaddd
--- /dev/null
+++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/lower-case/package.json
@@ -0,0 +1,9 @@
+{
+    "name" : "lower-case",
+    "node-red" : {
+        "nodes": {
+            "lower-case": "lower-case/lower-case.js"
+        }
+    },
+    "version": "1.0.0"
+}