').addClass(valClass).text(valValue).appendTo(container);
+ if (readOnly) {
+ valueLabel.addClass("readonly")
+ }
valueLabel.on("click", function(evt) {
+ if (readOnly) { return; }
evt.preventDefault();
evt.stopPropagation();
if (valType === 'str') {
@@ -395,17 +404,19 @@
valueLabel.hide();
})
item.gutter = $('');
-
- if (parent) {//red-ui-editor-type-json-editor-item-handle
- $('').appendTo(item.gutter);
- } else {
- $('').appendTo(item.gutter);
+ if(!readOnly) {
+ if (parent) {
+ $('').appendTo(item.gutter);
+ } else {
+ $('').appendTo(item.gutter);
+ }
+ $('').appendTo(item.gutter).on("click", function(evt) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ showObjectMenu($(this), item, readOnly);
+ });
}
- $('').appendTo(item.gutter).on("click", function(evt) {
- evt.preventDefault();
- evt.stopPropagation();
- showObjectMenu($(this), item);
- });
+
item.element = container;
return item;
}
@@ -501,7 +512,25 @@
open: function(tray) {
var trayBody = tray.find('.red-ui-tray-body');
var dialogForm = RED.editor.buildEditForm(tray.find('.red-ui-tray-body'),'dialog-form',type,'editor');
-
+ var toolbarButtons = options.toolbarButtons || [];
+ if (toolbarButtons.length) {
+ toolbarButtons.forEach(function (button) {
+ var element = $('')
+ .insertBefore("#node-input-json-reformat")
+ .on("click", function (evt) {
+ evt.preventDefault();
+ if (button.click !== undefined) {
+ button.click.call(element, evt);
+ }
+ });
+ if (button.id) { element.attr("id", button.id); }
+ if (button.title) { element.attr("title", button.title); }
+ if (button.icon) { element.append($("").attr("class", button.icon)); }
+ if (button.label || button.text) {
+ element.append($("").text(" " + (button.label || button.text)));
+ }
+ });
+ }
var container = $("#red-ui-editor-type-json-tab-ui-container").css({"height":"100%"});
var filterDepth = Infinity;
var list = $('').appendTo(container).treeList({
@@ -531,11 +560,13 @@
})
});
-
expressionEditor = RED.editor.createEditor({
id: 'node-input-json',
+ value: "",
+ mode:"ace/mode/json",
value: value||"",
mode:"ace/mode/json",
+ readOnly: !!options.readOnly,
stateId: options.stateId,
focus: true
});
@@ -576,7 +607,7 @@
var raw = expressionEditor.getValue().trim() ||"{}";
try {
var parsed = JSON.parse(raw);
- rootNode = handleItem(null,parsed,0,null);
+ rootNode = handleItem(null,parsed,0,null,options.readOnly);
rootNode.class = "red-ui-editor-type-json-root-node"
list.treeList('data',[rootNode]);
} catch(err) {
@@ -594,12 +625,12 @@
tabs.addTab({
id: 'json-raw',
- label: RED._('jsonEditor.rawMode'),
+ label: options.readOnly ? RED._('jsonEditor.rawMode-readonly') : RED._('jsonEditor.rawMode'),
content: $("#red-ui-editor-type-json-tab-raw")
});
tabs.addTab({
id: 'json-ui',
- label: RED._('jsonEditor.uiMode'),
+ label: options.readOnly ? RED._('jsonEditor.uiMode-readonly') : RED._('jsonEditor.uiMode'),
content: $("#red-ui-editor-type-json-tab-ui")
});
finishedBuild = true;
diff --git a/packages/node_modules/@node-red/editor-client/src/sass/editor.scss b/packages/node_modules/@node-red/editor-client/src/sass/editor.scss
index dee89035a..75133df8a 100644
--- a/packages/node_modules/@node-red/editor-client/src/sass/editor.scss
+++ b/packages/node_modules/@node-red/editor-client/src/sass/editor.scss
@@ -701,6 +701,10 @@ div.red-ui-button-small.red-ui-color-picker-opacity-slider-handle {
border-color: $list-item-background-hover;
border-style: dashed;
}
+ &.readonly {
+ cursor: pointer;
+ pointer-events: none;
+ }
}
.red-ui-editor-type-json-editor-item-gutter {
width: 48px;
@@ -720,6 +724,10 @@ div.red-ui-button-small.red-ui-color-picker-opacity-slider-handle {
> span, > button {
display: none;
}
+ &.readonly {
+ cursor: pointer;
+ pointer-events: none;
+ }
}
diff --git a/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js
new file mode 100644
index 000000000..0293c7f97
--- /dev/null
+++ b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js
@@ -0,0 +1,202 @@
+
+const os = require('os');
+const fs = require('fs');
+
+let runtime;
+let isContainerCached;
+let isWSLCached;
+
+const isInWsl = () => {
+ if (isWSLCached === undefined) {
+ isWSLCached = getIsInWSL();
+ }
+ return isWSLCached;
+ function getIsInWSL() {
+ if (process.platform !== 'linux') {
+ return false;
+ }
+ try {
+ if (os.release().toLowerCase().includes('microsoft')) {
+ if (isInContainer()) {
+ return false;
+ }
+ return true;
+ }
+ return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft') ? !isInContainer() : false;
+ } catch (_) {
+ return false;
+ }
+ }
+};
+
+const isInContainer = () => {
+ if (isContainerCached === undefined) {
+ isContainerCached = hasDockerEnv() || hasDockerCGroup();
+ }
+ return isContainerCached;
+ function hasDockerEnv() {
+ try {
+ fs.statSync('/.dockerenv');
+ return true;
+ } catch {
+ return false;
+ }
+ }
+ function hasDockerCGroup() {
+ try {
+ const s = fs.readFileSync('/proc/self/cgroup', 'utf8');
+ if (s.includes('docker')) {
+ return "docker"
+ } else if (s.includes('kubepod')) {
+ return "kubepod"
+ } else if (s.includes('lxc')) {
+ return "lxc"
+ }
+ } catch {
+ return false;
+ }
+ }
+}
+
+function buildDiagnosticReport(scope, callback) {
+ const modules = {};
+ const nl = runtime.nodes.getNodeList();
+ for (let i = 0; i < nl.length; i++) {
+ if (modules[nl[i].module]) {
+ continue;
+ }
+ modules[nl[i].module] = nl[i].version
+ }
+
+ const now = new Date();
+ const {locale, timeZone} = Intl.DateTimeFormat().resolvedOptions();
+ const report = {
+ report: "diagnostics",
+ scope: scope,
+ time: {
+ utc: now.toUTCString(),
+ local: now.toLocaleString(),
+ },
+ intl: {
+ locale, timeZone
+ },
+ nodejs: {
+ version: process.version,
+ arch: process.arch,
+ platform: process.platform,
+ memoryUsage: process.memoryUsage(),
+ },
+ os: {
+ containerised: isInContainer(),
+ wsl: isInWsl(),
+ totalmem: os.totalmem(),
+ freemem: os.freemem(),
+ arch: os.arch(),
+ loadavg: os.loadavg(),
+ platform: os.platform(),
+ release: os.release(),
+ type: os.type(),
+ uptime: os.uptime(),
+ version: os.version(),
+ },
+ runtime: {
+ isStarted: runtime.isStarted(),
+ modules: modules,
+ version: runtime.settings.version,
+ settings: {
+ available: runtime.settings.available(),
+ apiMaxLength: runtime.settings.apiMaxLength || "UNSET",
+ //coreNodesDir: runtime.settings.coreNodesDir,
+ disableEditor: runtime.settings.disableEditor,
+ contextStorage: listContextModules(),
+ debugMaxLength: runtime.settings.debugMaxLength || "UNSET",
+ editorTheme: runtime.settings.editorTheme || "UNSET",
+ flowFile: runtime.settings.flowFile || "UNSET",
+ mqttReconnectTime: runtime.settings.mqttReconnectTime || "UNSET",
+ serialReconnectTime: runtime.settings.serialReconnectTime || "UNSET",
+
+ adminAuth: runtime.settings.adminAuth ? "SET" : "UNSET",
+
+ httpAdminRoot: runtime.settings.httpAdminRoot || "UNSET",
+ httpAdminCors: runtime.settings.httpAdminCors ? "SET" : "UNSET",
+ httpNodeAuth: runtime.settings.httpNodeAuth ? "SET" : "UNSET",
+
+ httpNodeRoot: runtime.settings.httpNodeRoot || "UNSET",
+ httpNodeCors: runtime.settings.httpNodeCors ? "SET" : "UNSET",
+
+ httpStatic: runtime.settings.httpStatic ? "SET" : "UNSET",
+ httpStaticRoot: runtime.settings.httpStaticRoot || "UNSET",
+ httpStaticCors: runtime.settings.httpStaticCors ? "SET" : "UNSET",
+
+ uiHost: runtime.settings.uiHost ? "SET" : "UNSET",
+ uiPort: runtime.settings.uiPort ? "SET" : "UNSET",
+ userDir: runtime.settings.userDir ? "SET" : "UNSET",
+ }
+ }
+ }
+
+ // if (scope == "admin") {
+ // const moreSettings = {
+ // adminAuth_type: (runtime.settings.adminAuth && runtime.settings.adminAuth.type) ? runtime.settings.adminAuth.type : "UNSET",
+ // httpAdminCors: runtime.settings.httpAdminCors ? runtime.settings.httpAdminCors : "UNSET",
+ // httpNodeCors: runtime.settings.httpNodeCors ? runtime.settings.httpNodeCors : "UNSET",
+ // httpStaticCors: runtime.settings.httpStaticCors ? "SET" : "UNSET",
+ // settingsFile: runtime.settings.settingsFile ? runtime.settings.settingsFile : "UNSET",
+ // uiHost: runtime.settings.uiHost ? runtime.settings.uiHost : "UNSET",
+ // uiPort: runtime.settings.uiPort ? runtime.settings.uiPort : "UNSET",
+ // userDir: runtime.settings.userDir ? runtime.settings.userDir : "UNSET",
+ // }
+ // const moreNodejs = {
+ // execPath: process.execPath,
+ // pid: process.pid,
+ // }
+ // const moreOs = {
+ // cpus: os.cpus(),
+ // homedir: os.homedir(),
+ // hostname: os.hostname(),
+ // networkInterfaces: os.networkInterfaces(),
+ // }
+ // report.runtime.settings = Object.assign({}, report.runtime.settings, moreSettings);
+ // report.nodejs = Object.assign({}, report.nodejs, moreNodejs);
+ // report.os = Object.assign({}, report.os, moreOs);
+ // }
+
+ callback(report);
+
+ /** gets a sanitised list containing only the module name */
+ function listContextModules() {
+ const keys = Object.keys(runtime.settings.contextStorage);
+ const result = {};
+ keys.forEach(e => {
+ result[e] = {
+ module: String(runtime.settings.contextStorage[e].module)
+ }
+ })
+ return result;
+ }
+}
+
+
+module.exports = {
+ init: function (_runtime) {
+ runtime = _runtime;
+ },
+ /**
+ * Gets the node-red diagnostics report
+ * @param {{scope: string}} opts - settings
+ * @return {Promise} the diagnostics information
+ * @memberof @node-red/diagnostics
+ */
+ get: async function (opts) {
+ return new Promise(function (resolve, reject) {
+ opts = opts || {}
+ try {
+ runtime.log.audit({ event: "diagnostics.get", scope: opts.scope }, opts.req);
+ buildDiagnosticReport(opts.scope, (report) => resolve(report));
+ } catch (error) {
+ error.status = 500;
+ reject(error);
+ }
+ })
+ },
+}
diff --git a/packages/node_modules/@node-red/runtime/lib/api/index.js b/packages/node_modules/@node-red/runtime/lib/api/index.js
index 46d15d1e7..d2fa3c6fd 100644
--- a/packages/node_modules/@node-red/runtime/lib/api/index.js
+++ b/packages/node_modules/@node-red/runtime/lib/api/index.js
@@ -29,6 +29,7 @@ var api = module.exports = {
api.projects.init(runtime);
api.context.init(runtime);
api.plugins.init(runtime);
+ api.diagnostics.init(runtime);
},
comms: require("./comms"),
@@ -39,6 +40,7 @@ var api = module.exports = {
projects: require("./projects"),
context: require("./context"),
plugins: require("./plugins"),
+ diagnostics: require("./diagnostics"),
isStarted: async function(opts) {
return runtime.isStarted();
diff --git a/packages/node_modules/@node-red/runtime/lib/api/settings.js b/packages/node_modules/@node-red/runtime/lib/api/settings.js
index 0224c0ca6..f56b8ab61 100644
--- a/packages/node_modules/@node-red/runtime/lib/api/settings.js
+++ b/packages/node_modules/@node-red/runtime/lib/api/settings.js
@@ -142,6 +142,13 @@ var api = module.exports = {
}
safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType();
+
+ safeSettings.diagnostics = {
+ //unless diagnostics.ui and diagnostics.enabled are explicitly false, they will default to true.
+ enabled: (runtime.settings.diagnostics && runtime.settings.diagnostics.enabled === false) ? false : true,
+ ui: (runtime.settings.diagnostics && runtime.settings.diagnostics.ui === false) ? false : true
+ }
+
runtime.settings.exportNodeSettings(safeSettings);
runtime.plugins.exportPluginSettings(safeSettings);
}
diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js
index b869ef3ce..8e1d2b487 100644
--- a/packages/node_modules/@node-red/runtime/lib/index.js
+++ b/packages/node_modules/@node-red/runtime/lib/index.js
@@ -399,7 +399,12 @@ module.exports = {
* @memberof @node-red/runtime
*/
version: externalAPI.version,
-
+
+ /**
+ * @memberof @node-red/diagnostics
+ */
+ diagnostics:externalAPI.diagnostics,
+
storage: storage,
events: events,
hooks: hooks,
diff --git a/packages/node_modules/node-red/lib/red.js b/packages/node_modules/node-red/lib/red.js
index 2c6f00a6e..ddc140cc4 100644
--- a/packages/node_modules/node-red/lib/red.js
+++ b/packages/node_modules/node-red/lib/red.js
@@ -229,5 +229,12 @@ module.exports = {
* @see @node-red/editor-api_auth
* @memberof node-red
*/
- auth: api.auth
+ auth: api.auth,
+
+ /**
+ * The editor authentication api.
+ * @see @node-red/editor-api_auth
+ * @memberof node-red
+ */
+ get diagnostics() { return api.diagnostics }
};
diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js
index 65d20e907..2e2b7035e 100644
--- a/packages/node_modules/node-red/settings.js
+++ b/packages/node_modules/node-red/settings.js
@@ -242,6 +242,7 @@ module.exports = {
/*******************************************************************************
* Runtime Settings
* - lang
+ * - diagnostics
* - logging
* - contextStorage
* - exportGlobalContextKeys
@@ -254,6 +255,19 @@ module.exports = {
*/
// lang: "de",
+ /** Configure diagnostics options
+ * - enabled: When `enabled` is `true` (or unset), diagnostics data will
+ * be available at http://localhost:1880/diagnostics
+ * - ui: When `ui` is `true` (or unset), the action `show-system-info` will
+ * be available to logged in users of node-red editor
+ */
+ diagnostics: {
+ /** enable or disable diagnostics endpoint. Must be set to `false` to disable */
+ enabled: true,
+ /** enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */
+ ui: true,
+ },
+
/** Configure the logging output */
logging: {
/** Only console logging is currently supported */
diff --git a/test/unit/@node-red/editor-api/lib/admin/diagnostics_spec.js b/test/unit/@node-red/editor-api/lib/admin/diagnostics_spec.js
new file mode 100644
index 000000000..16d1e5b94
--- /dev/null
+++ b/test/unit/@node-red/editor-api/lib/admin/diagnostics_spec.js
@@ -0,0 +1,119 @@
+const should = require("should");
+const request = require('supertest');
+const express = require('express');
+const bodyParser = require("body-parser");
+const sinon = require('sinon');
+
+let app;
+
+const NR_TEST_UTILS = require("nr-test-utils");
+const diagnostics = NR_TEST_UTILS.require("@node-red/editor-api/lib/admin/diagnostics");
+
+describe("api/editor/diagnostics", function() {
+ before(function() {
+ app = express();
+ app.use(bodyParser.json());
+ app.get("/diagnostics",diagnostics.getReport);
+ });
+
+ it('returns the diagnostics report when explicitly enabled', function(done) {
+ const settings = { diagnostics: { ui: true, enabled: true } }
+ const runtimeAPI = {
+ diagnostics: {
+ get: async function (opts) {
+ return new Promise(function (resolve, reject) {
+ opts = opts || {}
+ try {
+ resolve({ opts: opts, a:1, b:2});
+ } catch (error) {
+ error.status = 500;
+ reject(error);
+ }
+ })
+ }
+ }
+ }
+
+ diagnostics.init(settings, runtimeAPI);
+
+ request(app)
+ .get("/diagnostics")
+ .expect(200)
+ .end(function(err,res) {
+ if (err || typeof res.error === "object") {
+ return done(err || res.error);
+ }
+ res.should.have.property("statusCode",200);
+ res.body.should.have.property("a",1);
+ res.body.should.have.property("b",2);
+ done();
+ });
+ });
+ it('returns the diagnostics report when not explicitly enabled (implicitly enabled)', function(done) {
+ const settings = { diagnostics: { enabled: undefined } }
+ const runtimeAPI = {
+ diagnostics: {
+ get: async function (opts) {
+ return new Promise(function (resolve, reject) {
+ opts = opts || {}
+ try {
+ resolve({ opts: opts, a:3, b:4});
+ } catch (error) {
+ error.status = 500;
+ reject(error);
+ }
+ })
+ }
+ }
+ }
+
+ diagnostics.init(settings, runtimeAPI);
+
+ request(app)
+ .get("/diagnostics")
+ .expect(200)
+ .end(function(err,res) {
+ if (err || typeof res.error === "object") {
+ return done(err || res.error);
+ }
+ res.should.have.property("statusCode",200);
+ res.body.should.have.property("a",3);
+ res.body.should.have.property("b",4);
+ done();
+ });
+ });
+ it('should error when setting is disabled', function(done) {
+ const settings = { diagnostics: { ui: true, enabled: false } }
+ const runtimeAPI = {
+ diagnostics: {
+ get: async function (opts) {
+ return new Promise(function (resolve, reject) {
+ opts = opts || {}
+ try {
+ resolve({ opts: opts});
+ } catch (error) {
+ error.status = 500;
+ reject(error);
+ }
+ })
+ }
+ }
+ }
+
+ diagnostics.init(settings, runtimeAPI);
+
+ request(app)
+ .get("/diagnostics")
+ .expect(403)
+ .end(function(err,res) {
+ if (!err && typeof res.error !== "object") {
+ return done(new Error("accessing diagnostics endpoint while disabled should raise error"));
+ }
+ res.should.have.property("statusCode",403);
+ res.body.should.have.property("message","diagnostics are disabled");
+ res.body.should.have.property("code","diagnostics.disabled");
+ done();
+ });
+ });
+
+});
diff --git a/test/unit/@node-red/runtime/lib/api/diagnostics_spec.js b/test/unit/@node-red/runtime/lib/api/diagnostics_spec.js
new file mode 100644
index 000000000..07e499344
--- /dev/null
+++ b/test/unit/@node-red/runtime/lib/api/diagnostics_spec.js
@@ -0,0 +1,126 @@
+
+var should = require("should");
+var sinon = require("sinon");
+var NR_TEST_UTILS = require("nr-test-utils");
+var diagnostics = NR_TEST_UTILS.require("@node-red/runtime/lib/api/diagnostics")
+
+var mockLog = () => ({
+ log: sinon.stub(),
+ debug: sinon.stub(),
+ trace: sinon.stub(),
+ warn: sinon.stub(),
+ info: sinon.stub(),
+ metric: sinon.stub(),
+ audit: sinon.stub(),
+ _: function() { return "abc"}
+})
+
+describe("runtime-api/diagnostics", function() {
+
+ describe("get", function() {
+ before(function() {
+ diagnostics.init({
+ isStarted: () => true,
+ nodes: {
+ getNodeList: () => [{module:"node-red", version:"9.9.9"},{module:"node-red-node-inject", version:"8.8.8"}]
+ },
+ settings: {
+ version: "7.7.7",
+ available: () => true,
+ //apiMaxLength: xxx, deliberately left blank. Should arrive in report as "UNSET"
+ debugMaxLength: 1111,
+ disableEditor: false,
+ flowFile: "flows.json",
+ mqttReconnectTime: 321,
+ serialReconnectTime: 432,
+ adminAuth: {},//should be sanitised to "SET"
+ httpAdminRoot: "/admin/root/",
+ httpAdminCors: {},//should be sanitised to "SET"
+ httpNodeAuth: {},//should be sanitised to "SET"
+ httpNodeRoot: "/node/root/",
+ httpNodeCors: {},//should be sanitised to "SET"
+ httpStatic: "/var/static/",//should be sanitised to "SET"
+ httpStaticRoot: "/static/root/",
+ httpStaticCors: {},//should be sanitised to "SET"
+ uiHost: "something.secret.com",//should be sanitised to "SET"
+ uiPort: 1337,//should be sanitised to "SET"
+ userDir: "/var/super/secret/",//should be sanitised to "SET",
+ contextStorage: {
+ default : { module: "memory" },
+ file: { module: "localfilesystem" },
+ secured: { module: "secure_store", user: "fred", pass: "super-duper-secret" },
+ },
+ editorTheme: {}
+ },
+ log: mockLog()
+ });
+ })
+ it("returns basic user settings", function() {
+ return diagnostics.get({scope:"fake_scope"}).then(result => {
+ should(result).be.type("object");
+
+ //result.xxxxx
+ Object.keys(result)
+ const reportPropCount = Object.keys(result).length;
+ reportPropCount.should.eql(7);//ensure no more than 7 keys are present in the report (avoid leakage of extra info)
+ result.should.have.property("report","diagnostics");
+ result.should.have.property("scope","fake_scope");
+ result.should.have.property("time").type("object");
+ result.should.have.property("intl").type("object");
+ result.should.have.property("nodejs").type("object");
+ result.should.have.property("os").type("object");
+ result.should.have.property("runtime").type("object");
+
+ //result.runtime.xxxxx
+ const runtimeCount = Object.keys(result.runtime).length;
+ runtimeCount.should.eql(4);//ensure no more than 4 keys are present in runtime
+ result.runtime.should.have.property('isStarted',true)
+ result.runtime.should.have.property('modules').type("object");
+ result.runtime.should.have.property('settings').type("object");
+ result.runtime.should.have.property('version','7.7.7');
+
+ //result.runtime.modules.xxxxx
+ const moduleCount = Object.keys(result.runtime.modules).length;
+ moduleCount.should.eql(2);//ensure no more than the 2 modules specified are present
+ result.runtime.modules.should.have.property('node-red','9.9.9');
+ result.runtime.modules.should.have.property('node-red-node-inject','8.8.8');
+
+ //result.runtime.settings.xxxxx
+ const settingsCount = Object.keys(result.runtime.settings).length;
+ settingsCount.should.eql(21);//ensure no more than the 21 settings listed below are present in the settings object
+ result.runtime.settings.should.have.property('available',true);
+ result.runtime.settings.should.have.property('apiMaxLength', "UNSET");//deliberately disabled to ensure UNSET is returned
+ result.runtime.settings.should.have.property('debugMaxLength', 1111);
+ result.runtime.settings.should.have.property('disableEditor', false);
+ result.runtime.settings.should.have.property('editorTheme', {});
+ result.runtime.settings.should.have.property('flowFile', "flows.json");
+ result.runtime.settings.should.have.property('mqttReconnectTime', 321);
+ result.runtime.settings.should.have.property('serialReconnectTime', 432);
+ result.runtime.settings.should.have.property("adminAuth", "SET"); //should be sanitised to "SET"
+ result.runtime.settings.should.have.property("httpAdminCors", "SET"); //should be sanitised to "SET"
+ result.runtime.settings.should.have.property('httpAdminRoot', "/admin/root/");
+ result.runtime.settings.should.have.property("httpNodeAuth", "SET"); //should be sanitised to "SET"
+ result.runtime.settings.should.have.property("httpNodeCors", "SET"); //should be sanitised to "SET"
+ result.runtime.settings.should.have.property('httpNodeRoot', "/node/root/");
+ result.runtime.settings.should.have.property("httpStatic", "SET"); //should be sanitised to "SET"
+ result.runtime.settings.should.have.property('httpStaticRoot', "/static/root/");
+ result.runtime.settings.should.have.property("httpStaticCors", "SET"); //should be sanitised to "SET"
+ result.runtime.settings.should.have.property("uiHost", "SET"); //should be sanitised to "SET"
+ result.runtime.settings.should.have.property("uiPort", "SET"); //should be sanitised to "SET"
+ result.runtime.settings.should.have.property("userDir", "SET"); //should be sanitised to "SET"
+ result.runtime.settings.should.have.property('contextStorage').type("object");
+
+ //result.runtime.settings.contextStorage.xxxxx
+ const contextCount = Object.keys(result.runtime.settings.contextStorage).length;
+ contextCount.should.eql(3);//ensure no more than the 3 settings listed below are present in the contextStorage object
+ result.runtime.settings.contextStorage.should.have.property('default', {module:"memory"});
+ result.runtime.settings.contextStorage.should.have.property('file', {module:"localfilesystem"});
+ result.runtime.settings.contextStorage.should.have.property('secured', {module:"secure_store"}); //only module should be present, other fields are dropped for security
+
+ })
+ })
+
+ });
+
+
+});