From 41af5187aa3e682196231730f5f4be5b9c99846f Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 22 Aug 2017 22:26:29 +0100 Subject: [PATCH] Reorganise red/api layout to better componentise --- Gruntfile.js | 12 +- red/api/{ => admin}/flow.js | 0 red/api/{ => admin}/flows.js | 0 red/api/admin/index.js | 62 ++++ red/api/{ => admin}/info.js | 4 +- red/api/{ => admin}/nodes.js | 6 +- red/api/auth/index.js | 2 +- red/api/{ => editor}/comms.js | 6 +- red/api/{ => editor}/credentials.js | 0 red/api/editor/index.js | 108 +++++++ red/api/{ => editor}/library.js | 2 +- red/api/{ => editor}/locales.js | 14 +- .../{ => editor}/locales/en-US/editor.json | 0 .../{ => editor}/locales/en-US/infotips.json | 0 .../{ => editor}/locales/en-US/jsonata.json | 0 red/api/{ => editor}/locales/ja/editor.json | 0 red/api/{ => editor}/locales/ja/infotips.json | 0 red/api/{ => editor}/locales/ja/jsonata.json | 0 .../{ => editor}/locales/zh-CN/editor.json | 0 red/api/{ => editor}/theme.js | 0 red/api/{ => editor}/ui.js | 4 +- red/api/index.js | 144 ++------- red/api/util.js | 47 +++ test/nodes/helper.js | 2 +- test/red/api/{ => admin}/flow_spec.js | 2 +- test/red/api/{ => admin}/flows_spec.js | 2 +- test/red/api/admin/index_spec.js | 299 ++++++++++++++++++ test/red/api/{ => admin}/info_spec.js | 4 +- test/red/api/{ => admin}/nodes_spec.js | 10 +- test/red/api/{ => editor}/comms_spec.js | 10 +- test/red/api/{ => editor}/credentials_spec.js | 4 +- test/red/api/editor/index_spec.js | 109 +++++++ test/red/api/{ => editor}/library_spec.js | 6 +- test/red/api/editor/locales_spec.js | 122 +++++++ test/red/api/{ => editor}/theme_spec.js | 5 +- test/red/api/{ => editor}/ui_spec.js | 8 +- test/red/api/index_spec.js | 215 ++++--------- test/red/api/locales_spec.js | 19 -- test/red/api/util_spec.js | 107 +++++++ 39 files changed, 1004 insertions(+), 331 deletions(-) rename red/api/{ => admin}/flow.js (100%) rename red/api/{ => admin}/flows.js (100%) create mode 100644 red/api/admin/index.js rename red/api/{ => admin}/info.js (96%) rename red/api/{ => admin}/nodes.js (98%) rename red/api/{ => editor}/comms.js (98%) rename red/api/{ => editor}/credentials.js (100%) create mode 100644 red/api/editor/index.js rename red/api/{ => editor}/library.js (99%) rename red/api/{ => editor}/locales.js (80%) rename red/api/{ => editor}/locales/en-US/editor.json (100%) rename red/api/{ => editor}/locales/en-US/infotips.json (100%) rename red/api/{ => editor}/locales/en-US/jsonata.json (100%) rename red/api/{ => editor}/locales/ja/editor.json (100%) rename red/api/{ => editor}/locales/ja/infotips.json (100%) rename red/api/{ => editor}/locales/ja/jsonata.json (100%) rename red/api/{ => editor}/locales/zh-CN/editor.json (100%) rename red/api/{ => editor}/theme.js (100%) rename red/api/{ => editor}/ui.js (92%) create mode 100644 red/api/util.js rename test/red/api/{ => admin}/flow_spec.js (99%) rename test/red/api/{ => admin}/flows_spec.js (99%) create mode 100644 test/red/api/admin/index_spec.js rename test/red/api/{ => admin}/info_spec.js (97%) rename test/red/api/{ => admin}/nodes_spec.js (99%) rename test/red/api/{ => editor}/comms_spec.js (98%) rename test/red/api/{ => editor}/credentials_spec.js (96%) create mode 100644 test/red/api/editor/index_spec.js rename test/red/api/{ => editor}/library_spec.js (98%) create mode 100644 test/red/api/editor/locales_spec.js rename test/red/api/{ => editor}/theme_spec.js (97%) rename test/red/api/{ => editor}/ui_spec.js (95%) delete mode 100644 test/red/api/locales_spec.js create mode 100644 test/red/api/util_spec.js diff --git a/Gruntfile.js b/Gruntfile.js index 1a0dc6ed9..4056474db 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -47,10 +47,12 @@ module.exports = function(grunt) { timeout: 3000, ignoreLeaks: false, ui: 'bdd', - reportFormats: ['lcov'], + reportFormats: ['lcov','html'], print: 'both' }, - coverage: { src: ['test/**/*_spec.js'] } + all: { src: ['test/**/*_spec.js'] }, + core: { src: ["test/_spec.js","test/red/**/*_spec.js"]}, + nodes: { src: ["test/nodes/**/*_spec.js"]} }, jshint: { options: { @@ -466,7 +468,7 @@ module.exports = function(grunt) { grunt.registerTask('test-core', 'Runs code style check and unit tests on core runtime code', - ['jshint:core','simplemocha:core']); + ['build','mocha_istanbul:core']); grunt.registerTask('test-editor', 'Runs code style check on editor code', @@ -474,7 +476,7 @@ module.exports = function(grunt) { grunt.registerTask('test-nodes', 'Runs unit tests on core nodes', - ['simplemocha:nodes']); + ['build','mocha_istanbul:nodes']); grunt.registerTask('build', 'Builds editor content', @@ -490,5 +492,5 @@ module.exports = function(grunt) { grunt.registerTask('coverage', 'Run Istanbul code test coverage task', - ['build','mocha_istanbul']); + ['build','mocha_istanbul:all']); }; diff --git a/red/api/flow.js b/red/api/admin/flow.js similarity index 100% rename from red/api/flow.js rename to red/api/admin/flow.js diff --git a/red/api/flows.js b/red/api/admin/flows.js similarity index 100% rename from red/api/flows.js rename to red/api/admin/flows.js diff --git a/red/api/admin/index.js b/red/api/admin/index.js new file mode 100644 index 000000000..81b733aee --- /dev/null +++ b/red/api/admin/index.js @@ -0,0 +1,62 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var express = require("express"); + +var nodes = require("./nodes"); +var flows = require("./flows"); +var flow = require("./flow"); +var info = require("./info"); +var auth = require("../auth"); + +var apiUtil = require("../util"); + +module.exports = { + init: function(runtime) { + flows.init(runtime); + flow.init(runtime); + info.init(runtime); + nodes.init(runtime); + + var needsPermission = auth.needsPermission; + + var adminApp = express(); + + // Flows + adminApp.get("/flows",needsPermission("flows.read"),flows.get,apiUtil.errorHandler); + adminApp.post("/flows",needsPermission("flows.write"),flows.post,apiUtil.errorHandler); + + // Flow + adminApp.get("/flow/:id",needsPermission("flows.read"),flow.get,apiUtil.errorHandler); + adminApp.post("/flow",needsPermission("flows.write"),flow.post,apiUtil.errorHandler); + adminApp.delete("/flow/:id",needsPermission("flows.write"),flow.delete,apiUtil.errorHandler); + adminApp.put("/flow/:id",needsPermission("flows.write"),flow.put,apiUtil.errorHandler); + + // Nodes + adminApp.get("/nodes",needsPermission("nodes.read"),nodes.getAll,apiUtil.errorHandler); + adminApp.post("/nodes",needsPermission("nodes.write"),nodes.post,apiUtil.errorHandler); + adminApp.get(/\/nodes\/((@[^\/]+\/)?[^\/]+)$/,needsPermission("nodes.read"),nodes.getModule,apiUtil.errorHandler); + adminApp.put(/\/nodes\/((@[^\/]+\/)?[^\/]+)$/,needsPermission("nodes.write"),nodes.putModule,apiUtil.errorHandler); + adminApp.delete(/\/nodes\/((@[^\/]+\/)?[^\/]+)$/,needsPermission("nodes.write"),nodes.delete,apiUtil.errorHandler); + adminApp.get(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.read"),nodes.getSet,apiUtil.errorHandler); + adminApp.put(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.write"),nodes.putSet,apiUtil.errorHandler); + + // Settings + adminApp.get("/settings",needsPermission("settings.read"),info.settings,apiUtil.errorHandler); + + return adminApp; + } +} diff --git a/red/api/info.js b/red/api/admin/info.js similarity index 96% rename from red/api/info.js rename to red/api/admin/info.js index b34f41662..83b16210f 100644 --- a/red/api/info.js +++ b/red/api/admin/info.js @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -var theme = require("./theme"); +var theme = require("../editor/theme"); var util = require('util'); var runtime; var settings; module.exports = { init: function(_runtime) { + console.log("info.init"); runtime = _runtime; settings = runtime.settings; }, @@ -50,7 +51,6 @@ module.exports = { } settings.exportNodeSettings(safeSettings); - res.json(safeSettings); } } diff --git a/red/api/nodes.js b/red/api/admin/nodes.js similarity index 98% rename from red/api/nodes.js rename to red/api/admin/nodes.js index c9f0d6041..8034bc6fb 100644 --- a/red/api/nodes.js +++ b/red/api/admin/nodes.js @@ -15,7 +15,7 @@ **/ var when = require("when"); -var locales = require("./locales"); +var apiUtils = require("../util"); var redNodes; var log; var i18n; @@ -35,7 +35,7 @@ module.exports = { log.audit({event: "nodes.list.get"},req); res.json(redNodes.getNodeList()); } else { - var lang = locales.determineLangFromHeaders(req.acceptsLanguages()); + var lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); log.audit({event: "nodes.configs.get"},req); res.send(redNodes.getNodeConfigs(lang)); } @@ -141,7 +141,7 @@ module.exports = { res.status(404).end(); } } else { - var lang = locales.determineLangFromHeaders(req.acceptsLanguages()); + var lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); result = redNodes.getNodeConfig(id,lang); if (result) { log.audit({event: "nodes.config.get",id:id},req); diff --git a/red/api/auth/index.js b/red/api/auth/index.js index cc90dd8ce..e0d13e6d0 100644 --- a/red/api/auth/index.js +++ b/red/api/auth/index.js @@ -22,7 +22,7 @@ var Tokens = require("./tokens"); var Users = require("./users"); var permissions = require("./permissions"); -var theme = require("../theme"); +var theme = require("../editor/theme"); var settings = null; var log = null diff --git a/red/api/comms.js b/red/api/editor/comms.js similarity index 98% rename from red/api/comms.js rename to red/api/editor/comms.js index 030b46e24..08c285788 100644 --- a/red/api/comms.js +++ b/red/api/editor/comms.js @@ -48,9 +48,9 @@ function init(_server,runtime) { } function start() { - var Tokens = require("./auth/tokens"); - var Users = require("./auth/users"); - var Permissions = require("./auth/permissions"); + var Tokens = require("../auth/tokens"); + var Users = require("../auth/users"); + var Permissions = require("../auth/permissions"); if (!settings.disableEditor) { Users.default().then(function(anonymousUser) { var webSocketKeepAliveTime = settings.webSocketKeepAliveTime || 15000; diff --git a/red/api/credentials.js b/red/api/editor/credentials.js similarity index 100% rename from red/api/credentials.js rename to red/api/editor/credentials.js diff --git a/red/api/editor/index.js b/red/api/editor/index.js new file mode 100644 index 000000000..c236bbb26 --- /dev/null +++ b/red/api/editor/index.js @@ -0,0 +1,108 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var express = require("express"); +var path = require('path'); + +var comms = require("./comms"); +var library = require("./library"); + +var auth = require("../auth"); +var needsPermission = auth.needsPermission; +var runtime; +var log; +var apiUtil = require("../util"); + +var ensureRuntimeStarted = function(req,res,next) { + if (!runtime.isStarted()) { + log.error("Node-RED runtime not started"); + res.status(503).send("Not started"); + } else { + next(); + } +} + +module.exports = { + init: function(server, _runtime) { + runtime = _runtime; + log = runtime.log; + var settings = runtime.settings; + if (!settings.disableEditor) { + + comms.init(server,runtime); + + var ui = require("./ui"); + ui.init(runtime); + var editorApp = express(); + if (settings.requireHttps === true) { + editorApp.enable('trust proxy'); + editorApp.use(function (req, res, next) { + if (req.secure) { + next(); + } else { + res.redirect('https://' + req.headers.host + req.originalUrl); + } + }); + } + editorApp.get("/",ensureRuntimeStarted,ui.ensureSlash,ui.editor); + editorApp.get("/icons/:module/:icon",ui.icon); + editorApp.get("/icons/:scope/:module/:icon",ui.icon); + + var theme = require("./theme"); + theme.init(runtime); + editorApp.use("/theme",theme.app()); + editorApp.use("/",ui.editorResources); + + // //Projects + // var projects = require("./projects"); + // projects.init(runtime); + // editorApp.get("/projects",projects.app()); + + // Locales + var locales = require("./locales"); + locales.init(runtime); + editorApp.get('/locales/nodes',locales.getAllNodes,apiUtil.errorHandler); + editorApp.get(/locales\/(.+)\/?$/,locales.get,apiUtil.errorHandler); + + // Library + var library = require("./library"); + library.init(editorApp,runtime); + editorApp.post(new RegExp("/library/flows\/(.*)"),needsPermission("library.write"),library.post,apiUtil.errorHandler); + editorApp.get("/library/flows",needsPermission("library.read"),library.getAll,apiUtil.errorHandler); + editorApp.get(new RegExp("/library/flows\/(.*)"),needsPermission("library.read"),library.get,apiUtil.errorHandler); + + // Credentials + var credentials = require("./credentials"); + credentials.init(runtime); + editorApp.get('/credentials/:type/:id', needsPermission("credentials.read"),credentials.get,apiUtil.errorHandler); + + return editorApp; + } + }, + start: function() { + var catalogPath = path.resolve(path.join(__dirname,"locales")); + return runtime.i18n.registerMessageCatalogs([ + {namespace: "editor", dir: catalogPath, file:"editor.json"}, + {namespace: "jsonata", dir: catalogPath, file:"jsonata.json"}, + {namespace: "infotips", dir: catalogPath, file:"infotips.json"} + ]).then(function(){ + comms.start(); + }); + }, + stop: comms.stop, + publish: comms.publish, + registerLibrary: library.register +} diff --git a/red/api/library.js b/red/api/editor/library.js similarity index 99% rename from red/api/library.js rename to red/api/editor/library.js index ff1eac60a..c9139fc1b 100644 --- a/red/api/library.js +++ b/red/api/editor/library.js @@ -21,7 +21,7 @@ var redApp = null; var storage; var log; var redNodes; -var needsPermission = require("./auth").needsPermission; +var needsPermission = require("../auth").needsPermission; function createLibrary(type) { if (redApp) { diff --git a/red/api/locales.js b/red/api/editor/locales.js similarity index 80% rename from red/api/locales.js rename to red/api/editor/locales.js index 9d9703fe2..32bfa5c80 100644 --- a/red/api/locales.js +++ b/red/api/editor/locales.js @@ -15,17 +15,10 @@ **/ var fs = require('fs'); var path = require('path'); +//var apiUtil = require('../util'); var i18n; var redNodes; -function determineLangFromHeaders(acceptedLanguages){ - var lang = i18n.defaultLang; - acceptedLanguages = acceptedLanguages || []; - if (acceptedLanguages.length >= 1) { - lang = acceptedLanguages[0]; - } - return lang; -} module.exports = { init: function(runtime) { i18n = runtime.i18n; @@ -35,7 +28,7 @@ module.exports = { var namespace = req.params[0]; var lngs = req.query.lng; namespace = namespace.replace(/\.json$/,""); - var lang = req.query.lng; //determineLangFromHeaders(req.acceptsLanguages() || []); + var lang = req.query.lng; //apiUtil.determineLangFromHeaders(req.acceptsLanguages() || []); var prevLang = i18n.i.lng(); // Trigger a load from disk of the language if it is not the default i18n.i.setLng(lang, function(){ @@ -55,6 +48,5 @@ module.exports = { } }); res.json(result); - }, - determineLangFromHeaders: determineLangFromHeaders + } } diff --git a/red/api/locales/en-US/editor.json b/red/api/editor/locales/en-US/editor.json similarity index 100% rename from red/api/locales/en-US/editor.json rename to red/api/editor/locales/en-US/editor.json diff --git a/red/api/locales/en-US/infotips.json b/red/api/editor/locales/en-US/infotips.json similarity index 100% rename from red/api/locales/en-US/infotips.json rename to red/api/editor/locales/en-US/infotips.json diff --git a/red/api/locales/en-US/jsonata.json b/red/api/editor/locales/en-US/jsonata.json similarity index 100% rename from red/api/locales/en-US/jsonata.json rename to red/api/editor/locales/en-US/jsonata.json diff --git a/red/api/locales/ja/editor.json b/red/api/editor/locales/ja/editor.json similarity index 100% rename from red/api/locales/ja/editor.json rename to red/api/editor/locales/ja/editor.json diff --git a/red/api/locales/ja/infotips.json b/red/api/editor/locales/ja/infotips.json similarity index 100% rename from red/api/locales/ja/infotips.json rename to red/api/editor/locales/ja/infotips.json diff --git a/red/api/locales/ja/jsonata.json b/red/api/editor/locales/ja/jsonata.json similarity index 100% rename from red/api/locales/ja/jsonata.json rename to red/api/editor/locales/ja/jsonata.json diff --git a/red/api/locales/zh-CN/editor.json b/red/api/editor/locales/zh-CN/editor.json similarity index 100% rename from red/api/locales/zh-CN/editor.json rename to red/api/editor/locales/zh-CN/editor.json diff --git a/red/api/theme.js b/red/api/editor/theme.js similarity index 100% rename from red/api/theme.js rename to red/api/editor/theme.js diff --git a/red/api/ui.js b/red/api/editor/ui.js similarity index 92% rename from red/api/ui.js rename to red/api/editor/ui.js index 10fe74d76..aec8c2fef 100644 --- a/red/api/ui.js +++ b/red/api/editor/ui.js @@ -22,7 +22,7 @@ var theme = require("./theme"); var redNodes; -var templateDir = path.resolve(__dirname+"/../../editor/templates"); +var templateDir = path.resolve(__dirname+"/../../../editor/templates"); var editorTemplate; module.exports = { @@ -52,5 +52,5 @@ module.exports = { editor: function(req,res) { res.send(Mustache.render(editorTemplate,theme.context())); }, - editorResources: express.static(__dirname + '/../../public') + editorResources: express.static(__dirname + '/../../../public') }; diff --git a/red/api/index.js b/red/api/index.js index e4b86eb43..7336b2331 100644 --- a/red/api/index.js +++ b/red/api/index.js @@ -17,49 +17,19 @@ var express = require("express"); var bodyParser = require("body-parser"); var util = require('util'); -var path = require('path'); var passport = require('passport'); var when = require('when'); var cors = require('cors'); -var ui = require("./ui"); -var nodes = require("./nodes"); -var flows = require("./flows"); -var flow = require("./flow"); -var library = require("./library"); -var info = require("./info"); -var theme = require("./theme"); -var locales = require("./locales"); -var credentials = require("./credentials"); -var comms = require("./comms"); - var auth = require("./auth"); -var needsPermission = auth.needsPermission; +var apiUtil = require("./util"); var i18n; var log; var adminApp; var server; var runtime; - -var errorHandler = function(err,req,res,next) { - if (err.message === "request entity too large") { - log.error(err); - } else { - console.log(err.stack); - } - log.audit({event: "api.error",error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:"unexpected_error", message:err.toString()}); -}; - -var ensureRuntimeStarted = function(req,res,next) { - if (!runtime.isStarted()) { - log.error("Node-RED runtime not started"); - res.status(503).send("Not started"); - } else { - next(); - } -} +var editor; function init(_server,_runtime) { server = _server; @@ -68,45 +38,22 @@ function init(_server,_runtime) { i18n = runtime.i18n; log = runtime.log; if (settings.httpAdminRoot !== false) { - comms.init(server,runtime); + apiUtil.init(runtime); adminApp = express(); auth.init(runtime); - credentials.init(runtime); - flows.init(runtime); - flow.init(runtime); - info.init(runtime); - library.init(adminApp,runtime); - locales.init(runtime); - nodes.init(runtime); - // Editor - if (!settings.disableEditor) { - ui.init(runtime); - var editorApp = express(); - if (settings.requireHttps === true) { - editorApp.enable('trust proxy'); - editorApp.use(function (req, res, next) { - if (req.secure) { - next(); - } else { - res.redirect('https://' + req.headers.host + req.originalUrl); - } - }); - } - editorApp.get("/",ensureRuntimeStarted,ui.ensureSlash,ui.editor); - editorApp.get("/icons/:module/:icon",ui.icon); - editorApp.get("/icons/:scope/:module/:icon",ui.icon); - theme.init(runtime); - editorApp.use("/theme",theme.app()); - editorApp.use("/",ui.editorResources); - adminApp.use(editorApp); - } var maxApiRequestSize = settings.apiMaxLength || '5mb'; adminApp.use(bodyParser.json({limit:maxApiRequestSize})); adminApp.use(bodyParser.urlencoded({limit:maxApiRequestSize,extended:true})); - adminApp.get("/auth/login",auth.login,errorHandler); + // Editor + if (!settings.disableEditor) { + editor = require("./editor"); + var editorApp = editor.init(server, runtime); + adminApp.use(editorApp); + } + adminApp.get("/auth/login",auth.login,apiUtil.errorHandler); if (settings.adminAuth) { if (settings.adminAuth.type === "strategy") { auth.genericStrategy(adminApp,settings.adminAuth.strategy); @@ -119,62 +66,31 @@ function init(_server,_runtime) { auth.errorHandler ); } - adminApp.post("/auth/revoke",needsPermission(""),auth.revoke,errorHandler); + adminApp.post("/auth/revoke",auth.needsPermission(""),auth.revoke,apiUtil.errorHandler); } + if (settings.httpAdminCors) { var corsHandler = cors(settings.httpAdminCors); adminApp.use(corsHandler); } - // Flows - adminApp.get("/flows",needsPermission("flows.read"),flows.get,errorHandler); - adminApp.post("/flows",needsPermission("flows.write"),flows.post,errorHandler); - - adminApp.get("/flow/:id",needsPermission("flows.read"),flow.get,errorHandler); - adminApp.post("/flow",needsPermission("flows.write"),flow.post,errorHandler); - adminApp.delete("/flow/:id",needsPermission("flows.write"),flow.delete,errorHandler); - adminApp.put("/flow/:id",needsPermission("flows.write"),flow.put,errorHandler); - - // Nodes - adminApp.get("/nodes",needsPermission("nodes.read"),nodes.getAll,errorHandler); - adminApp.post("/nodes",needsPermission("nodes.write"),nodes.post,errorHandler); - - adminApp.get(/\/nodes\/((@[^\/]+\/)?[^\/]+)$/,needsPermission("nodes.read"),nodes.getModule,errorHandler); - adminApp.put(/\/nodes\/((@[^\/]+\/)?[^\/]+)$/,needsPermission("nodes.write"),nodes.putModule,errorHandler); - adminApp.delete(/\/nodes\/((@[^\/]+\/)?[^\/]+)$/,needsPermission("nodes.write"),nodes.delete,errorHandler); - - adminApp.get(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.read"),nodes.getSet,errorHandler); - adminApp.put(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.write"),nodes.putSet,errorHandler); - - adminApp.get('/credentials/:type/:id', needsPermission("credentials.read"),credentials.get,errorHandler); - - adminApp.get('/locales/nodes',locales.getAllNodes,errorHandler); - adminApp.get(/locales\/(.+)\/?$/,locales.get,errorHandler); - - // Library - adminApp.post(new RegExp("/library/flows\/(.*)"),needsPermission("library.write"),library.post,errorHandler); - adminApp.get("/library/flows",needsPermission("library.read"),library.getAll,errorHandler); - adminApp.get(new RegExp("/library/flows\/(.*)"),needsPermission("library.read"),library.get,errorHandler); - - // Settings - adminApp.get("/settings",needsPermission("settings.read"),info.settings,errorHandler); - - // Error Handler - //adminApp.use(errorHandler); + var adminApiApp = require("./admin").init(runtime); + adminApp.use(adminApiApp); + } else { + adminApp = null; } } function start() { - var catalogPath = path.resolve(path.join(__dirname,"locales")); - return i18n.registerMessageCatalogs([ - {namespace: "editor", dir: catalogPath, file:"editor.json"}, - {namespace: "jsonata", dir: catalogPath, file:"jsonata.json"}, - {namespace: "infotips", dir: catalogPath, file:"infotips.json"} - ]).then(function(){ - comms.start(); - }); + if (editor) { + return editor.start(); + } else { + return when.resolve(); + } } function stop() { - comms.stop(); + if (editor) { + editor.stop(); + } return when.resolve(); } module.exports = { @@ -182,13 +98,21 @@ module.exports = { start: start, stop: stop, library: { - register: library.register + register: function(type) { + if (editor) { + editor.registerLibrary(type); + } + } }, auth: { needsPermission: auth.needsPermission }, comms: { - publish: comms.publish + publish: function(topic,data,retain) { + if (editor) { + editor.publish(topic,data,retain); + } + } }, get adminApp() { return adminApp; }, get server() { return server; } diff --git a/red/api/util.js b/red/api/util.js new file mode 100644 index 000000000..ac2144d8f --- /dev/null +++ b/red/api/util.js @@ -0,0 +1,47 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * 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 i18n; +var log; + +module.exports = { + init: function(_runtime) { + log = _runtime.log; + i18n = _runtime.i18n; + }, + errorHandler: function(err,req,res,next) { + if (err.message === "request entity too large") { + log.error(err); + } else { + log.error(err.stack); + } + log.audit({event: "api.error",error:err.code||"unexpected_error",message:err.toString()},req); + res.status(400).json({error:"unexpected_error", message:err.toString()}); + }, + + determineLangFromHeaders: function(acceptedLanguages){ + console.log("GOT",acceptedLanguages) + var lang = i18n.defaultLang; + acceptedLanguages = acceptedLanguages || []; + if (acceptedLanguages.length >= 1) { + console.log("WE HAVE SOMETHING"); + lang = acceptedLanguages[0]; + } + console.log("RETURNING",lang); + return lang; + } +} diff --git a/test/nodes/helper.js b/test/nodes/helper.js index 3d5df4d1f..decbaccb4 100644 --- a/test/nodes/helper.js +++ b/test/nodes/helper.js @@ -33,7 +33,7 @@ var RED = require("../../red/red.js"); var redNodes = require("../../red/runtime/nodes"); var flows = require("../../red/runtime/nodes/flows"); var credentials = require("../../red/runtime/nodes/credentials"); -var comms = require("../../red/api/comms.js"); +var comms = require("../../red/api/editor/comms.js"); var log = require("../../red/runtime/log.js"); var context = require("../../red/runtime/nodes/context.js"); var events = require("../../red/runtime/events.js"); diff --git a/test/red/api/flow_spec.js b/test/red/api/admin/flow_spec.js similarity index 99% rename from test/red/api/flow_spec.js rename to test/red/api/admin/flow_spec.js index a677a1182..9e7f69fa2 100644 --- a/test/red/api/flow_spec.js +++ b/test/red/api/admin/flow_spec.js @@ -21,7 +21,7 @@ var bodyParser = require('body-parser'); var sinon = require('sinon'); var when = require('when'); -var flow = require("../../../red/api/flow"); +var flow = require("../../../../red/api/admin/flow"); describe("flow api", function() { diff --git a/test/red/api/flows_spec.js b/test/red/api/admin/flows_spec.js similarity index 99% rename from test/red/api/flows_spec.js rename to test/red/api/admin/flows_spec.js index 364a30169..d073bca88 100644 --- a/test/red/api/flows_spec.js +++ b/test/red/api/admin/flows_spec.js @@ -21,7 +21,7 @@ var bodyParser = require('body-parser'); var sinon = require('sinon'); var when = require('when'); -var flows = require("../../../red/api/flows"); +var flows = require("../../../../red/api/admin/flows"); describe("flows api", function() { diff --git a/test/red/api/admin/index_spec.js b/test/red/api/admin/index_spec.js new file mode 100644 index 000000000..3bdc54175 --- /dev/null +++ b/test/red/api/admin/index_spec.js @@ -0,0 +1,299 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * 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 express = require("express"); +var adminApi = require("../../../../red/api/admin"); +var auth = require("../../../../red/api/auth"); + +var nodes = require("../../../../red/api/admin/nodes"); +var flows = require("../../../../red/api/admin/flows"); +var flow = require("../../../../red/api/admin/flow"); +var info = require("../../../../red/api/admin/info"); + +/** +* Ensure all API routes are correctly mounted, with the expected permissions checks +*/ +describe("api/admin/index", function() { + describe("Ensure all API routes are correctly mounted, with the expected permissions checks", function() { + var app; + var mockList = [ + flows,flow,info,nodes + ] + var permissionChecks = {}; + var lastRequest; + var stubApp = function(req,res,next) { + lastRequest = req; + res.status(200).end(); + }; + before(function() { + mockList.forEach(function(m) { + sinon.stub(m,"init",function(){}); + }); + sinon.stub(auth,"needsPermission", function(permission) { + return function(req,res,next) { + permissionChecks[permission] = (permissionChecks[permission]||0)+1; + next(); + } + }); + + sinon.stub(flows,"get",stubApp); + sinon.stub(flows,"post",stubApp); + + sinon.stub(flow,"get",stubApp); + sinon.stub(flow,"post",stubApp); + sinon.stub(flow,"delete",stubApp); + sinon.stub(flow,"put",stubApp); + + sinon.stub(nodes,"getAll",stubApp); + sinon.stub(nodes,"post",stubApp); + sinon.stub(nodes,"getModule",stubApp); + sinon.stub(nodes,"putModule",stubApp); + sinon.stub(nodes,"delete",stubApp); + sinon.stub(nodes,"getSet",stubApp); + sinon.stub(nodes,"putSet",stubApp); + + sinon.stub(info,"settings",stubApp); + + }); + after(function() { + mockList.forEach(function(m) { + m.init.restore(); + }); + auth.needsPermission.restore(); + + flows.get.restore(); + flows.post.restore(); + flow.get.restore(); + flow.post.restore(); + flow.delete.restore(); + flow.put.restore(); + nodes.getAll.restore(); + nodes.post.restore(); + nodes.getModule.restore(); + nodes.putModule.restore(); + nodes.delete.restore(); + nodes.getSet.restore(); + nodes.putSet.restore(); + + info.settings.restore(); + }); + + before(function() { + app = adminApi.init({}); + }); + beforeEach(function() { + permissionChecks = {}; + }) + it('GET /flows', function(done) { + request(app).get("/flows").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('flows.read',1); + done(); + }) + }); + it('POST /flows', function(done) { + request(app).post("/flows").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('flows.write',1); + done(); + }) + }); + + it('GET /flow/1234', function(done) { + request(app).get("/flow/1234").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('flows.read',1); + lastRequest.params.should.have.property('id','1234') + done(); + }) + }); + it('POST /flow', function(done) { + request(app).post("/flow").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('flows.write',1); + done(); + }) + }); + it('DELETE /flow/1234', function(done) { + request(app).del("/flow/1234").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('flows.write',1); + lastRequest.params.should.have.property('id','1234') + done(); + }) + }); + it('PUT /flow/1234', function(done) { + request(app).put("/flow/1234").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('flows.write',1); + lastRequest.params.should.have.property('id','1234') + done(); + }) + }); + + it('GET /nodes', function(done) { + request(app).get("/nodes").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('nodes.read',1); + done(); + }) + }); + it('POST /nodes', function(done) { + request(app).post("/nodes").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('nodes.write',1); + done(); + }) + }); + it('GET /nodes/module', function(done) { + request(app).get("/nodes/module").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('nodes.read',1); + lastRequest.params.should.have.property(0,'module') + done(); + }) + }); + it('GET /nodes/@scope/module', function(done) { + request(app).get("/nodes/@scope/module").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('nodes.read',1); + lastRequest.params.should.have.property(0,'@scope/module') + done(); + }) + }); + + it('PUT /nodes/module', function(done) { + request(app).put("/nodes/module").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('nodes.write',1); + lastRequest.params.should.have.property(0,'module') + done(); + }) + }); + it('PUT /nodes/@scope/module', function(done) { + request(app).put("/nodes/@scope/module").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('nodes.write',1); + lastRequest.params.should.have.property(0,'@scope/module') + done(); + }) + }); + + it('DELETE /nodes/module', function(done) { + request(app).del("/nodes/module").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('nodes.write',1); + lastRequest.params.should.have.property(0,'module') + done(); + }) + }); + it('DELETE /nodes/@scope/module', function(done) { + request(app).del("/nodes/@scope/module").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('nodes.write',1); + lastRequest.params.should.have.property(0,'@scope/module') + done(); + }) + }); + + it('GET /nodes/module/set', function(done) { + request(app).get("/nodes/module/set").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('nodes.read',1); + lastRequest.params.should.have.property(0,'module') + lastRequest.params.should.have.property(2,'set') + done(); + }) + }); + it('GET /nodes/@scope/module/set', function(done) { + request(app).get("/nodes/@scope/module/set").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('nodes.read',1); + lastRequest.params.should.have.property(0,'@scope/module') + lastRequest.params.should.have.property(2,'set') + done(); + }) + }); + + it('PUT /nodes/module/set', function(done) { + request(app).put("/nodes/module/set").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('nodes.write',1); + lastRequest.params.should.have.property(0,'module') + lastRequest.params.should.have.property(2,'set') + done(); + }) + }); + it('PUT /nodes/@scope/module/set', function(done) { + request(app).put("/nodes/@scope/module/set").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('nodes.write',1); + lastRequest.params.should.have.property(0,'@scope/module') + lastRequest.params.should.have.property(2,'set') + done(); + }) + }); + + it('GET /settings', function(done) { + request(app).get("/settings").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + permissionChecks.should.have.property('settings.read',1); + done(); + }) + }); + }); +}); diff --git a/test/red/api/info_spec.js b/test/red/api/admin/info_spec.js similarity index 97% rename from test/red/api/info_spec.js rename to test/red/api/admin/info_spec.js index c222aec5c..c9a01683a 100644 --- a/test/red/api/info_spec.js +++ b/test/red/api/admin/info_spec.js @@ -21,8 +21,8 @@ var sinon = require('sinon'); var when = require('when'); var app = express(); -var info = require("../../../red/api/info"); -var theme = require("../../../red/api/theme"); +var info = require("../../../../red/api/admin/info"); +var theme = require("../../../../red/api/editor/theme"); describe("info api", function() { describe("settings handler", function() { diff --git a/test/red/api/nodes_spec.js b/test/red/api/admin/nodes_spec.js similarity index 99% rename from test/red/api/nodes_spec.js rename to test/red/api/admin/nodes_spec.js index 1b91afab4..67a60090c 100644 --- a/test/red/api/nodes_spec.js +++ b/test/red/api/admin/nodes_spec.js @@ -21,8 +21,8 @@ var bodyParser = require('body-parser'); var sinon = require('sinon'); var when = require('when'); -var nodes = require("../../../red/api/nodes"); -var locales = require("../../../red/api/locales"); +var nodes = require("../../../../red/api/admin/nodes"); +var apiUtil = require("../../../../red/api/util"); describe("nodes api", function() { @@ -51,11 +51,13 @@ describe("nodes api", function() { app.put(/\/nodes\/((@[^\/]+\/)?[^\/]+)$/,nodes.putModule); app.put(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,nodes.putSet); app.delete("/nodes/:id",nodes.delete); - sinon.stub(locales,"determineLangFromHeaders", function() { + sinon.stub(apiUtil,"determineLangFromHeaders", function() { return "en-US"; }); }); - + after(function() { + apiUtil.determineLangFromHeaders.restore(); + }) describe('get nodes', function() { it('returns node list', function(done) { diff --git a/test/red/api/comms_spec.js b/test/red/api/editor/comms_spec.js similarity index 98% rename from test/red/api/comms_spec.js rename to test/red/api/editor/comms_spec.js index 458b92fbf..c4bfe04d4 100644 --- a/test/red/api/comms_spec.js +++ b/test/red/api/editor/comms_spec.js @@ -23,14 +23,14 @@ var express = require('express'); var app = express(); var WebSocket = require('ws'); -var comms = require("../../../red/api/comms"); -var Users = require("../../../red/api/auth/users"); -var Tokens = require("../../../red/api/auth/tokens"); +var comms = require("../../../../red/api/editor/comms"); +var Users = require("../../../../red/api/auth/users"); +var Tokens = require("../../../../red/api/auth/tokens"); var address = '127.0.0.1'; var listenPort = 0; // use ephemeral port -describe("api/comms", function() { +describe("api/editor/comms", function() { describe("with default keepalive", function() { var server; var url; @@ -327,7 +327,7 @@ describe("api/comms", function() { ws.on('message', function(data) { var msg = JSON.parse(data); msg.should.have.property('topic','hb'); - msg.should.have.property('data').be.a.Number; + msg.should.have.property('data').be.a.Number(); count++; if (count == 3) { ws.close(); diff --git a/test/red/api/credentials_spec.js b/test/red/api/editor/credentials_spec.js similarity index 96% rename from test/red/api/credentials_spec.js rename to test/red/api/editor/credentials_spec.js index 3bc413d0a..c020cd285 100644 --- a/test/red/api/credentials_spec.js +++ b/test/red/api/editor/credentials_spec.js @@ -20,9 +20,9 @@ var express = require('express'); var sinon = require('sinon'); var when = require('when'); -var credentials = require("../../../red/api/credentials"); +var credentials = require("../../../../red/api/editor/credentials"); -describe('credentials api', function() { +describe('api/editor/credentials', function() { var app; before(function() { diff --git a/test/red/api/editor/index_spec.js b/test/red/api/editor/index_spec.js new file mode 100644 index 000000000..9bd27c81c --- /dev/null +++ b/test/red/api/editor/index_spec.js @@ -0,0 +1,109 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * 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 express = require("express"); +var editorApi = require("../../../../red/api/editor"); +var comms = require("../../../../red/api/editor/comms"); + + +describe("api/editor/index", function() { + var app; + describe("disabled the editor", function() { + beforeEach(function() { + sinon.stub(comms,'init', function(){}); + }); + afterEach(function() { + comms.init.restore(); + }); + it("disables the editor", function() { + var editorApp = editorApi.init({},{ + settings:{disableEditor:true} + }); + should.not.exist(editorApp); + comms.init.called.should.be.false(); + }); + }); + describe("enables the editor", function() { + var mockList = [ + 'library','theme','locales','credentials','comms' + ] + var isStarted = true; + var errors = []; + before(function() { + mockList.forEach(function(m) { + sinon.stub(require("../../../../red/api/editor/"+m),"init",function(){}); + }); + sinon.stub(require("../../../../red/api/editor/theme"),"app",function(){ return express()}); + }); + after(function() { + mockList.forEach(function(m) { + require("../../../../red/api/editor/"+m).init.restore(); + }) + require("../../../../red/api/editor/theme").app.restore(); + }); + + before(function() { + app = editorApi.init({},{ + log:{audit:function(){},error:function(msg){errors.push(msg)}}, + settings:{httpNodeRoot:true, httpAdminRoot: true,disableEditor:false}, + events:{on:function(){},removeListener:function(){}}, + isStarted: function() { return isStarted; } + }); + }); + it('serves the editor', function(done) { + request(app) + .get("/") + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + // Index page should probably mention Node-RED somewhere + res.text.indexOf("Node-RED").should.not.eql(-1); + done(); + }); + }); + it('serves icons', function(done) { + request(app) + .get("/icons/inject.png") + .expect("Content-Type", /image\/png/) + .expect(200,done) + }); + it('handles page not there', function(done) { + request(app) + .get("/foo") + .expect(404,done) + }); + it('warns if runtime not started', function(done) { + isStarted = false; + request(app) + .get("/") + .expect(503) + .end(function(err,res) { + if (err) { + return done(err); + } + res.text.should.eql("Not started"); + errors.should.have.lengthOf(1); + errors[0].should.eql("Node-RED runtime not started"); + done(); + }); + }); + }); +}); diff --git a/test/red/api/library_spec.js b/test/red/api/editor/library_spec.js similarity index 98% rename from test/red/api/library_spec.js rename to test/red/api/editor/library_spec.js index c30d3fa52..7520154e1 100644 --- a/test/red/api/library_spec.js +++ b/test/red/api/editor/library_spec.js @@ -22,10 +22,10 @@ var bodyParser = require('body-parser'); var when = require('when'); var app; -var library = require("../../../red/api/library"); -var auth = require("../../../red/api/auth"); +var library = require("../../../../red/api/editor/library"); +var auth = require("../../../../red/api/auth"); -describe("library api", function() { +describe("api/editor/library", function() { function initLibrary(_flows,_libraryEntries,_examples) { var flows = _flows; diff --git a/test/red/api/editor/locales_spec.js b/test/red/api/editor/locales_spec.js new file mode 100644 index 000000000..f6d1ff762 --- /dev/null +++ b/test/red/api/editor/locales_spec.js @@ -0,0 +1,122 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var should = require("should"); +var request = require('supertest'); +var express = require('express'); +var sinon = require('sinon'); + +var locales = require("../../../../red/api/editor/locales"); + +describe("api/editor/locales", function() { + beforeEach(function() { + }) + afterEach(function() { + }) + describe('get named resource catalog',function() { + var app; + before(function() { + // bit of a mess of internal workings + locales.init({ + i18n: { + i: { + lng: function() { return 'en-US'}, + setLng: function(lang,callback) { + if (callback) { + callback(); + } + } + }, + catalog: function(namespace, lang) { + return {namespace:namespace, lang:lang}; + } + } + }); + app = express(); + app.get(/locales\/(.+)\/?$/,locales.get); + }); + it('returns with default language', function(done) { + request(app) + .get("/locales/message-catalog") + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property('namespace','message-catalog'); + done(); + }); + }); + it('returns with selected language', function(done) { + request(app) + .get("/locales/message-catalog?lng=fr-FR") + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property('namespace','message-catalog'); + res.body.should.have.property('lang','fr-FR'); + done(); + }); + }); + }); + + describe('get all node resource catalogs',function() { + var app; + before(function() { + // bit of a mess of internal workings + locales.init({ + i18n: { + catalog: function(namespace, lang) { + return { + "node-red": "should not return", + "test-module-a-id": "test-module-a-catalog", + "test-module-b-id": "test-module-b-catalog", + "test-module-c-id": "test-module-c-catalog" + }[namespace] + } + }, + nodes: { + getNodeList: function() { + return [ + {module:"node-red",id:"node-red-id"}, + {module:"test-module-a",id:"test-module-a-id"}, + {module:"test-module-b",id:"test-module-b-id"} + ]; + } + } + }); + app = express(); + app.get("/locales/nodes",locales.getAllNodes); + }); + it('returns with the node catalogs', function(done) { + request(app) + .get("/locales/nodes") + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.eql({ + 'test-module-a-id': 'test-module-a-catalog', + 'test-module-b-id': 'test-module-b-catalog' + }); + done(); + }); + }); + }); +}); diff --git a/test/red/api/theme_spec.js b/test/red/api/editor/theme_spec.js similarity index 97% rename from test/red/api/theme_spec.js rename to test/red/api/editor/theme_spec.js index dff6aeec4..513a8e912 100644 --- a/test/red/api/theme_spec.js +++ b/test/red/api/editor/theme_spec.js @@ -15,7 +15,6 @@ **/ var should = require("should"); -var request = require('supertest'); var express = require('express'); var sinon = require('sinon'); var when = require('when'); @@ -23,9 +22,9 @@ var fs = require("fs"); var app = express(); -var theme = require("../../../red/api/theme"); +var theme = require("../../../../red/api/editor/theme"); -describe("theme handler", function() { +describe("api/editor/theme", function() { beforeEach(function() { sinon.stub(fs,"statSync",function() { return true; }); }); diff --git a/test/red/api/ui_spec.js b/test/red/api/editor/ui_spec.js similarity index 95% rename from test/red/api/ui_spec.js rename to test/red/api/editor/ui_spec.js index f562d2551..9ebfa8185 100644 --- a/test/red/api/ui_spec.js +++ b/test/red/api/editor/ui_spec.js @@ -22,10 +22,10 @@ var path = require("path"); var EventEmitter = require('events').EventEmitter; var events = new EventEmitter(); -var ui = require("../../../red/api/ui"); +var ui = require("../../../../red/api/editor/ui"); -describe("ui api", function() { +describe("api/editor/ui", function() { var app; before(function() { @@ -33,7 +33,7 @@ describe("ui api", function() { events:events, nodes: { getNodeIconPath: function(module,icon) { - return path.resolve(__dirname+'/../../../public/icons/arrow-in.png'); + return path.resolve(__dirname+'/../../../../public/icons/arrow-in.png'); } } }); @@ -91,7 +91,7 @@ describe("ui api", function() { } } it('returns the requested icon', function(done) { - var defaultIcon = fs.readFileSync(path.resolve(__dirname+'/../../../public/icons/arrow-in.png')); + var defaultIcon = fs.readFileSync(path.resolve(__dirname+'/../../../../public/icons/arrow-in.png')); request(app) .get("/icons/module/icon.png") .expect("Content-Type", /image\/png/) diff --git a/test/red/api/index_spec.js b/test/red/api/index_spec.js index b1c80babb..09dd9a11c 100644 --- a/test/red/api/index_spec.js +++ b/test/red/api/index_spec.js @@ -23,164 +23,83 @@ var fs = require("fs"); var path = require("path"); var api = require("../../../red/api"); -describe("api index", function() { - var app; +var apiUtil = require("../../../red/api/util"); +var apiAuth = require("../../../red/api/auth"); +var apiEditor = require("../../../red/api/editor"); +var apiAdmin = require("../../../red/api/admin"); - describe("disables editor", function() { + +describe("api/index", function() { + var beforeEach = function() { + sinon.stub(apiUtil,"init",function(){}); + sinon.stub(apiAuth,"init",function(){}); + sinon.stub(apiEditor,"init",function(){ + var app = express(); + app.get("/editor",function(req,res) { res.status(200).end(); }); + return app; + }); + sinon.stub(apiAdmin,"init",function(){ + var app = express(); + app.get("/admin",function(req,res) { res.status(200).end(); }); + return app; + }); + sinon.stub(apiAuth,"login",function(req,res){ + res.status(200).end(); + }); + }; + var afterEach = function() { + apiUtil.init.restore(); + apiAuth.init.restore(); + apiAuth.login.restore(); + apiEditor.init.restore(); + apiAdmin.init.restore(); + }; + + beforeEach(beforeEach); + afterEach(afterEach); + + it("does not setup admin api if httpAdminRoot is false", function(done) { + api.init({},{ + settings: { httpAdminRoot: false } + }); + should.not.exist(api.adminApp); + done(); + }); + describe('initalises admin api without adminAuth', function(done) { before(function() { + beforeEach(); api.init({},{ - settings:{httpNodeRoot:true, httpAdminRoot: true,disableEditor:true, exportNodeSettings: function() {}}, - events: {on:function(){},removeListener: function(){}}, - log: {info:function(){},_:function(){}}, - nodes: {paletteEditorEnabled: function(){return true}} + settings: { } }); - app = api.adminApp; - }); - - it('does not serve the editor', function(done) { - request(app) - .get("/") - .expect(404,done) - }); - it('does not serve icons', function(done) { - request(app) - .get("/icons/default.png") - .expect(404,done) - }); - it('serves settings', function(done) { - request(app) - .get("/settings") - .expect(200,done) }); + after(afterEach); + it('exposes the editor',function() { + request(api.adminApp).get("/editor").expect(200).end(done); + }) + it('exposes the admin api',function() { + request(api.adminApp).get("/admin").expect(200).end(done); + }) + it('exposes the auth api',function(done) { + request(api.adminApp).get("/auth/login").expect(200).end(done); + }) }); - describe("can serve auth", function() { - var mockList = [ - 'ui','nodes','flows','library','info','locales','credentials' - ] - before(function() { - mockList.forEach(function(m) { - sinon.stub(require("../../../red/api/"+m),"init",function(){}); - }); - }); - after(function() { - mockList.forEach(function(m) { - require("../../../red/api/"+m).init.restore(); - }) - }); + describe('initalises admin api without editor', function(done) { before(function() { + beforeEach(); api.init({},{ - settings:{httpNodeRoot:true, httpAdminRoot: true, adminAuth:{type: "credentials",users:[],default:{permissions:"read"}}}, - storage:{getSessions:function(){return when.resolve({})}}, - events:{on:function(){},removeListener:function(){}} - }); - app = api.adminApp; - }); - - it('it now serves auth', function(done) { - request(app) - .get("/auth/login") - .expect(200) - .end(function(err,res) { - if (err) { return done(err); } - res.body.type.should.equal("credentials"); - done(); - }); - }); - }); - - describe("editor warns if runtime not started", function() { - var mockList = [ - 'nodes','flows','library','info','theme','locales','credentials' - ] - before(function() { - mockList.forEach(function(m) { - sinon.stub(require("../../../red/api/"+m),"init",function(){}); + settings: { disableEditor: true } }); }); - after(function() { - mockList.forEach(function(m) { - require("../../../red/api/"+m).init.restore(); - }) - }); - - it('serves the editor', function(done) { - var errorLog = sinon.spy(); - api.init({},{ - log:{audit:function(){},error:errorLog}, - settings:{httpNodeRoot:true, httpAdminRoot: true,disableEditor:false}, - events:{on:function(){},removeListener:function(){}}, - isStarted: function() { return false; } // <----- - }); - app = api.adminApp; - request(app) - .get("/") - .expect(503) - .end(function(err,res) { - if (err) { - return done(err); - } - res.text.should.eql("Not started"); - errorLog.calledOnce.should.be.true(); - done(); - }); - }); - - }); - - describe("enables editor", function() { - - var mockList = [ - 'nodes','flows','library','info','theme','locales','credentials' - ] - before(function() { - mockList.forEach(function(m) { - sinon.stub(require("../../../red/api/"+m),"init",function(){}); - }); - }); - after(function() { - mockList.forEach(function(m) { - require("../../../red/api/"+m).init.restore(); - }) - }); - - before(function() { - api.init({},{ - log:{audit:function(){}}, - settings:{httpNodeRoot:true, httpAdminRoot: true,disableEditor:false}, - events:{on:function(){},removeListener:function(){}}, - isStarted: function() { return true; } - }); - app = api.adminApp; - }); - it('serves the editor', function(done) { - request(app) - .get("/") - .expect(200) - .end(function(err,res) { - if (err) { - return done(err); - } - // Index page should probably mention Node-RED somewhere - res.text.indexOf("Node-RED").should.not.eql(-1); - done(); - }); - }); - it('serves icons', function(done) { - request(app) - .get("/icons/inject.png") - .expect("Content-Type", /image\/png/) - .expect(200,done) - }); - it('serves settings', function(done) { - request(app) - .get("/settings") - .expect(200,done) - }); - it('handles page not there', function(done) { - request(app) - .get("/foo") - .expect(404,done) - }); + after(afterEach); + it('does not expose the editor',function() { + request(api.adminApp).get("/editor").expect(404).end(done); + }) + it('exposes the admin api',function() { + request(api.adminApp).get("/admin").expect(200).end(done); + }) + it('exposes the auth api',function(done) { + request(api.adminApp).get("/auth/login").expect(200).end(done) + }) }); }); diff --git a/test/red/api/locales_spec.js b/test/red/api/locales_spec.js deleted file mode 100644 index c181ab65b..000000000 --- a/test/red/api/locales_spec.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright JS Foundation and other contributors, http://js.foundation - * - * 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. - **/ - -describe("locales api", function() { - it.skip("works",function() {}); -}); diff --git a/test/red/api/util_spec.js b/test/red/api/util_spec.js new file mode 100644 index 000000000..fb5151c14 --- /dev/null +++ b/test/red/api/util_spec.js @@ -0,0 +1,107 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var should = require("should"); +var request = require('supertest'); +var express = require('express'); + +var apiUtil = require("../../../red/api/util"); + +describe("api/util", function() { + describe("errorHandler", function() { + var loggedError = null; + var loggedEvent = null; + var app; + before(function() { + app = express(); + apiUtil.init({ + log:{ + error: function(msg) { + loggedError = msg; + }, + audit: function(event) { + loggedEvent = event; + } + }, + i18n:{} + }) + app.get("/tooLarge", function(req,res) { + var err = new Error(); + err.message = "request entity too large"; + throw err; + },apiUtil.errorHandler) + app.get("/stack", function(req,res) { + var err = new Error(); + err.message = "stacktrace"; + throw err; + },apiUtil.errorHandler) + }); + beforeEach(function() { + loggedError = null; + loggedEvent = null; + }) + it("logs an error for request entity too large", function(done) { + request(app).get("/tooLarge").expect(400).end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property("error","unexpected_error"); + res.body.should.have.property("message","Error: request entity too large"); + + loggedError.should.have.property("message","request entity too large"); + + loggedEvent.should.have.property("event","api.error"); + loggedEvent.should.have.property("error","unexpected_error"); + loggedEvent.should.have.property("message","Error: request entity too large"); + done(); + }); + }) + it("logs an error plus stack for other errors", function(done) { + request(app).get("/stack").expect(400).end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property("error","unexpected_error"); + res.body.should.have.property("message","Error: stacktrace"); + + /Error: stacktrace\s*at.*util_spec.js/m.test(loggedError).should.be.true(); + + loggedEvent.should.have.property("event","api.error"); + loggedEvent.should.have.property("error","unexpected_error"); + loggedEvent.should.have.property("message","Error: stacktrace"); + + + + done(); + }); + }); + }) + + describe('determineLangFromHeaders', function() { + before(function() { + apiUtil.init({ + log:{}, + i18n:{defaultLang:"en-US"} + }); + }) + it('returns the default lang if non provided', function() { + apiUtil.determineLangFromHeaders(null).should.eql("en-US"); + }) + it('returns the first language accepted', function() { + apiUtil.determineLangFromHeaders(['fr-FR','en-GB']).should.eql("fr-FR"); + }) + }) +});