diff --git a/README.md b/README.md index 7364a866..29d0874e 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ ## Server Provides server capabilities for the [UI5 Tooling](https://github.com/SAP/ui5-tooling). -### Middlewares -The development server has already a set of middlewares which supports the developer with the following features: +### Middleware +The development server has already a set of middleware which supports the developer with the following features: * Translation files with `.properties` extension are properly encoded with **ISO-8859-1**. * Changes on files with `.less` extension triggers a theme build and delivers the compiled CSS files. diff --git a/index.js b/index.js index 74496812..f3571066 100644 --- a/index.js +++ b/index.js @@ -5,13 +5,33 @@ module.exports = { server: require("./lib/server"), sslUtil: require("./lib/sslUtil"), + middlewareRepository: require("./lib/middleware/middlewareRepository"), + + // Legacy middleware export. Still private. middleware: { csp: require("./lib/middleware/csp"), - discovery: require("./lib/middleware/discovery"), - nonReadRequests: require("./lib/middleware/discovery"), - serveIndex: require("./lib/middleware/serveIndex"), - serveResources: require("./lib/middleware/serveResources"), - serveThemes: require("./lib/middleware/serveThemes"), - versionInfo: require("./lib/middleware/versionInfo"), + discovery: mapLegacyMiddlewareArguments(require("./lib/middleware/discovery")), + nonReadRequests: mapLegacyMiddlewareArguments(require("./lib/middleware/discovery")), + serveIndex: mapLegacyMiddlewareArguments(require("./lib/middleware/serveIndex")), + serveResources: mapLegacyMiddlewareArguments(require("./lib/middleware/serveResources")), + serveThemes: mapLegacyMiddlewareArguments(require("./lib/middleware/serveThemes")), + versionInfo: mapLegacyMiddlewareArguments(require("./lib/middleware/versionInfo")), } }; + +function mapLegacyMiddlewareArguments(module) { + // Old arguments was a single object with optional properties + // - resourceCollections + // - tree + return function({resourceCollections, tree} = {}) { + const resources = {}; + resources.all = resourceCollections.combo; + resources.rootProject = resourceCollections.source; + resources.dependencies = resourceCollections.dependencies; + + return module({ + resources, + tree + }); + }; +} diff --git a/lib/middleware/MiddlewareManager.js b/lib/middleware/MiddlewareManager.js new file mode 100644 index 00000000..e3c6b04d --- /dev/null +++ b/lib/middleware/MiddlewareManager.js @@ -0,0 +1,178 @@ +const middlewareRepository = require("./middlewareRepository"); +/** + * + * + * @memberof module:@ui5/server.middleware + */ +class MiddlewareManager { + constructor({tree, resources, options = { + sendSAPTargetCSP: false + }}) { + if (!tree || !resources || !resources.all || !resources.rootProject || !resources.dependencies) { + throw new Error("[MiddlewareManager]: One or more mandatory parameters not provided"); + } + this.tree = tree; + this.resources = resources; + this.options = options; + + this.middleware = {}; + this.middlewareExecutionOrder = []; + } + + async applyMiddleware(app) { + await this.addStandardMiddleware(); + await this.addCustomMiddleware(); + + return this.middlewareExecutionOrder.map((name) => { + const m = this.middleware[name]; + app.use(m.mountPath, m.middleware); + }); + } + + async addMiddleware(middlewareName, { + wrapperCallback, mountPath = "/", + beforeMiddleware, afterMiddleware + } = {}) { + let middlewareCallback = middlewareRepository.getMiddleware(middlewareName); + if (wrapperCallback) { + middlewareCallback = wrapperCallback(middlewareCallback); + } + if (this.middleware[middlewareName] || this.middlewareExecutionOrder.includes(middlewareName)) { + throw new Error(`Failed to add duplicate middleware ${middlewareName}`); + } + + if (beforeMiddleware || afterMiddleware) { + const refMiddlewareName = beforeMiddleware || afterMiddleware; + let refMiddlewareIdx = this.middlewareExecutionOrder.indexOf(refMiddlewareName); + if (refMiddlewareIdx === -1) { + throw new Error(`Could not find middleware ${refMiddlewareName}, referenced by custom ` + + `middleware ${middlewareName}`); + } + if (afterMiddleware) { + // Insert after index of referenced middleware + refMiddlewareIdx++; + } + this.middlewareExecutionOrder.splice(refMiddlewareIdx, 0, middlewareName); + } else { + this.middlewareExecutionOrder.push(middlewareName); + } + + this.middleware[middlewareName] = { + middleware: await Promise.resolve(middlewareCallback({resources: this.resources})), + mountPath + }; + } + + async addStandardMiddleware() { + await this.addMiddleware("csp", { + wrapperCallback: (cspModule) => { + const oCspConfig = { + allowDynamicPolicySelection: true, + allowDynamicPolicyDefinition: true, + definedPolicies: { + "sap-target-level-1": + "default-src 'self'; " + + "script-src 'self' 'unsafe-eval'; " + + "style-src 'self' 'unsafe-inline'; " + + "font-src 'self' data:; " + + "img-src 'self' * data: blob:; " + + "frame-src 'self' https: data: blob:; " + + "child-src 'self' https: data: blob:; " + + "connect-src 'self' https: wss:;", + "sap-target-level-2": + "default-src 'self'; " + + "script-src 'self'; " + + "style-src 'self' 'unsafe-inline'; " + + "font-src 'self' data:; " + + "img-src 'self' * data: blob:; " + + "frame-src 'self' https: data: blob:; " + + "child-src 'self' https: data: blob:; " + + "connect-src 'self' https: wss:;" + } + }; + if (this.options.sendSAPTargetCSP) { + Object.assign(oCspConfig, { + defaultPolicy: "sap-target-level-1", + defaultPolicyIsReportOnly: true, + defaultPolicy2: "sap-target-level-2", + defaultPolicy2IsReportOnly: true, + }); + } + return () => { + return cspModule("sap-ui-xx-csp-policy", oCspConfig); + }; + } + }); + await this.addMiddleware("compression"); + await this.addMiddleware("cors"); + await this.addMiddleware("discovery", { + mountPath: "/discovery" + }); + await this.addMiddleware("serveResources"); + await this.addMiddleware("serveThemes"); + await this.addMiddleware("versionInfo", { + mountPath: "/resources/sap-ui-version.json", + wrapperCallback: (versionInfoModule) => { + return ({resources}) => { + return versionInfoModule({ + resources, + tree: this.tree + }); + }; + } + }); + await this.addMiddleware("connectUi5Proxy", { + mountPath: "/proxy" + }); + // Handle anything but read operations *before* the serveIndex middleware + // as it will reject them with a 405 (Method not allowed) instead of 404 like our old tooling + await this.addMiddleware("nonReadRequests"); + await this.addMiddleware("serveIndex"); + } + + async addCustomMiddleware() { + const project = this.tree; + const projectCustomMiddleware = project.server && project.server.customMiddleware; + if (!projectCustomMiddleware || projectCustomMiddleware.length === 0) { + return; // No custom middleware defined + } + + for (let i = 0; i < projectCustomMiddleware.length; i++) { + const middlewareDef = projectCustomMiddleware[i]; + if (!middlewareDef.name) { + throw new Error(`Missing name for custom middleware definition of project ${project.metadata.name} ` + + `at index ${i}`); + } + if (middlewareDef.beforeMiddleware && middlewareDef.afterMiddleware) { + throw new Error( + `Custom middleware definition ${middlewareDef.name} of project ${project.metadata.name} ` + + `defines both "beforeMiddleware" and "afterMiddleware" parameters. Only one must be defined.`); + } + if (!middlewareDef.beforeMiddleware && !middlewareDef.afterMiddleware) { + throw new Error( + `Custom middleware definition ${middlewareDef.name} of project ${project.metadata.name} ` + + `defines neither a "beforeMiddleware" nor an "afterMiddleware" parameter. One must be defined.`); + } + + if (this.middleware[middlewareDef.name]) { + // Middleware is already known + throw new Error(`Failed to add custom middleware ${middlewareDef.name}. ` + + `A middleware with the same name is already known.`); + } + await this.addMiddleware(middlewareDef.name, { + wrapperCallback: (middleware) => { + return ({resources}) => { + const options = { + configuration: middlewareDef.configuration + }; + return middleware({resources, options}); + }; + }, + mountPath: middlewareDef.mountPath, + beforeMiddleware: middlewareDef.beforeMiddleware, + afterMiddleware: middlewareDef.afterMiddleware + }); + } + } +} +module.exports = MiddlewareManager; diff --git a/lib/middleware/connectUi5Proxy.js b/lib/middleware/connectUi5Proxy.js new file mode 100644 index 00000000..0aab2f52 --- /dev/null +++ b/lib/middleware/connectUi5Proxy.js @@ -0,0 +1,9 @@ +const ui5connect = require("connect-openui5"); + +function createMiddleware() { + return ui5connect.proxy({ + secure: false + }); +} + +module.exports = createMiddleware; diff --git a/lib/middleware/discovery.js b/lib/middleware/discovery.js index 0de6d3b0..c44422c8 100644 --- a/lib/middleware/discovery.js +++ b/lib/middleware/discovery.js @@ -13,12 +13,14 @@ const urlPattern = /\/(app_pages|all_libs|all_tests)(?:[?#].*)?$/; * * * @module @ui5/server/middleware/discovery - * @param {Object} resourceCollections Contains the resource reader or collection to access project related files - * @param {module:@ui5/fs.AbstractReader} resourceCollections.source Resource reader or collection for the source project - * @param {module:@ui5/fs.AbstractReader} resourceCollections.combo Resource collection which contains the workspace and the project dependencies + * @param {Object} parameters Parameters + * @param {module:@ui5/fs.AbstractReader} parameters.resources.all Reader or Collection to read resources of the + * root project and its dependencies + * @param {module:@ui5/fs.AbstractReader} parameters.resources.rootProject Reader or Collection to read resources of + * the project the server is started in * @returns {Function} Returns a server middleware closure. */ -function createMiddleware({resourceCollections}) { +function createMiddleware({resources}) { return function discoveryMiddleware(req, res, next) { const parts = urlPattern.exec(req.url); const type = parts && parts[1]; @@ -46,7 +48,7 @@ function createMiddleware({resourceCollections}) { } if (type === "app_pages") { - resourceCollections.source.byGlob("/**/*.{html,htm}").then(function(resources) { + resources.rootProject.byGlob("/**/*.{html,htm}").then(function(resources) { resources.forEach(function(resource) { const relPath = resource.getPath().substr(1); // cut off leading "/" response.push({ @@ -56,7 +58,7 @@ function createMiddleware({resourceCollections}) { sendResponse(); }); } else if (type === "all_libs") { - resourceCollections.combo.byGlob([ + resources.all.byGlob([ "/resources/**/*.library" ]).then(function(resources) { resources.forEach(function(resource) { @@ -72,8 +74,8 @@ function createMiddleware({resourceCollections}) { }); } else if (type === "all_tests") { Promise.all([ - resourceCollections.combo.byGlob("/resources/**/*.library"), - resourceCollections.combo.byGlob("/test-resources/**/*.{html,htm}") + resources.all.byGlob("/resources/**/*.library"), + resources.all.byGlob("/test-resources/**/*.{html,htm}") ]).then(function(results) { const libraryResources = results[0]; const testPageResources = results[1]; diff --git a/lib/middleware/middlewareRepository.js b/lib/middleware/middlewareRepository.js new file mode 100644 index 00000000..5b8ca1cc --- /dev/null +++ b/lib/middleware/middlewareRepository.js @@ -0,0 +1,33 @@ +const middlewares = { + compression: "compression", + cors: "cors", + csp: "./csp", + serveResources: "./serveResources", + serveIndex: "./serveIndex", + discovery: "./discovery", + versionInfo: "./versionInfo", + connectUi5Proxy: "./connectUi5Proxy", + serveThemes: "./serveThemes", + nonReadRequests: "./nonReadRequests" +}; + +function getMiddleware(middlewareName) { + const middlewarePath = middlewares[middlewareName]; + + if (!middlewarePath) { + throw new Error(`middlewareRepository: Unknown Middleware ${middlewareName}`); + } + return require(middlewarePath); +} + +function addMiddleware(name, middlewarePath) { + if (middlewares[name]) { + throw new Error(`middlewareRepository: Middleware ${name} already registered`); + } + middlewares[name] = middlewarePath; +} + +module.exports = { + getMiddleware: getMiddleware, + addMiddleware: addMiddleware +}; diff --git a/lib/middleware/serveIndex.js b/lib/middleware/serveIndex.js index d8292c0e..606e8888 100644 --- a/lib/middleware/serveIndex.js +++ b/lib/middleware/serveIndex.js @@ -133,16 +133,16 @@ function createContent(path, resourceInfos) { * Creates and returns the middleware to serve a resource index. * * @module @ui5/server/middleware/serveIndex - * @param {Object} resourceCollections Contains the resource reader or collection to access project related files - * @param {module:@ui5/fs.AbstractReader} resourceCollections.combo Resource collection which contains the workspace and the project dependencies + * @param {Object} resources Contains the resource reader or collection to access project related files + * @param {module:@ui5/fs.AbstractReader} resources.all Resource collection which contains the workspace and the project dependencies * @returns {Function} Returns a server middleware closure. */ -function createMiddleware({resourceCollections}) { +function createMiddleware({resources}) { return function serveIndex(req, res, next) { const pathname = parseurl(req).pathname; log.verbose("\n Listing index of " + pathname); const glob = pathname + (pathname.endsWith("/") ? "*" : "/*"); - resourceCollections.combo.byGlob(glob, {nodir: false}).then((resources) => { + resources.all.byGlob(glob, {nodir: false}).then((resources) => { if (!resources || resources.length == 0) { // Not found next(); return; diff --git a/lib/middleware/serveResources.js b/lib/middleware/serveResources.js index a26cd650..e1ade546 100644 --- a/lib/middleware/serveResources.js +++ b/lib/middleware/serveResources.js @@ -19,14 +19,14 @@ function isFresh(req, res) { * Creates and returns the middleware to serve application resources. * * @module @ui5/server/middleware/serveResources - * @param {Object} resourceCollections Contains the resource reader or collection to access project related files - * @param {module:@ui5/fs.AbstractReader} resourceCollections.combo Resource collection which contains the workspace and the project dependencies + * @param {Object} resources Contains the resource reader or collection to access project related files + * @param {module:@ui5/fs.AbstractReader} resources.all Resource collection which contains the workspace and the project dependencies * @returns {Function} Returns a server middleware closure. */ -function createMiddleware({resourceCollections}) { +function createMiddleware({resources}) { return function serveResources(req, res, next) { const pathname = parseurl(req).pathname; - resourceCollections.combo.byPath(pathname).then(function(resource) { + resources.all.byPath(pathname).then(function(resource) { if (!resource) { // Not found next(); return; diff --git a/lib/middleware/serveThemes.js b/lib/middleware/serveThemes.js index 1f087a7b..1ece4c7d 100644 --- a/lib/middleware/serveThemes.js +++ b/lib/middleware/serveThemes.js @@ -18,13 +18,13 @@ const themeRequest = /^(.*\/)library(?:(\.css)|(-RTL\.css)|(-parameters\.json))$ * The theme is built in realtime. If a less file was modified, the theme build is triggered to rebuild the theme. * * @module @ui5/server/middleware/serveThemes - * @param {Object} resourceCollections Contains the resource reader or collection to access project related files - * @param {module:@ui5/fs.AbstractReader} resourceCollections.combo Resource collection which contains the workspace and the project dependencies + * @param {Object} resources Contains the resource reader or collection to access project related files + * @param {module:@ui5/fs.AbstractReader} resources.all Resource collection which contains the workspace and the project dependencies * @returns {Function} Returns a server middleware closure. */ -function createMiddleware({resourceCollections}) { +function createMiddleware({resources}) { const builder = new themeBuilder.ThemeBuilder({ - fs: fsInterface(resourceCollections.combo) + fs: fsInterface(resources.all) }); return function theme(req, res, next) { @@ -46,7 +46,7 @@ function createMiddleware({resourceCollections}) { } const sourceLessPath = themeReq[1] + "library.source.less"; - resourceCollections.combo.byPath(sourceLessPath).then((sourceLessResource) => { + resources.all.byPath(sourceLessPath).then((sourceLessResource) => { if (!sourceLessResource) { // Not found next(); return; diff --git a/lib/middleware/versionInfo.js b/lib/middleware/versionInfo.js index 23e97d9c..fa2a6d73 100644 --- a/lib/middleware/versionInfo.js +++ b/lib/middleware/versionInfo.js @@ -4,13 +4,13 @@ const createVersionInfoProcessor = require("@ui5/builder").processors.versionInf * Creates and returns the middleware to create the version info as json object. * * @module @ui5/server/middleware/versionInfo - * @param {Object} resourceCollections Contains the resource reader or collection to access project related files - * @param {module:@ui5/fs.AbstractReader} resourceCollections.dependencies Resource collection which contains the project dependencies + * @param {Object} resources Contains the resource reader or collection to access project related files + * @param {module:@ui5/fs.AbstractReader} resources.dependencies Resource collection which contains the project dependencies * @returns {Function} Returns a server middleware closure. */ -function createMiddleware({resourceCollections, tree: project}) { +function createMiddleware({resources, tree: project}) { return function versionInfo(req, res, next) { - resourceCollections.dependencies.byGlob("/**/.library") + resources.dependencies.byGlob("/**/.library") .then((resources) => { resources.sort((a, b) => { return a._project.metadata.name.localeCompare(b._project.metadata.name); diff --git a/lib/server.js b/lib/server.js index ddd0598d..a374a24b 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,16 +1,8 @@ const express = require("express"); -const compression = require("compression"); -const cors = require("cors"); const portscanner = require("portscanner"); -const serveResources = require("./middleware/serveResources"); -const serveIndex = require("./middleware/serveIndex"); -const discovery = require("./middleware/discovery"); -const versionInfo = require("./middleware/versionInfo"); -const serveThemes = require("./middleware/serveThemes"); -const csp = require("./middleware/csp"); -const ui5connect = require("connect-openui5"); -const nonReadRequests = require("./middleware/nonReadRequests"); +const MiddlewareManager = require("./middleware/MiddlewareManager"); + const ui5Fs = require("@ui5/fs"); const resourceFactory = ui5Fs.resourceFactory; const ReaderCollectionPrioritized = ui5Fs.ReaderCollectionPrioritized; @@ -112,94 +104,47 @@ module.exports = { * h2-flag and a close function, * which can be used to stop the server. */ - serve(tree, {port, changePortIfInUse = false, h2 = false, key, cert, acceptRemoteConnections = false, - sendSAPTargetCSP = false}) { - return Promise.resolve().then(() => { - const projectResourceCollections = resourceFactory.createCollectionsForTree(tree); - - const workspace = resourceFactory.createWorkspace({ - reader: projectResourceCollections.source, - name: tree.metadata.name - }); + async serve(tree, { + port: requestedPort, changePortIfInUse = false, h2 = false, key, cert, + acceptRemoteConnections = false, sendSAPTargetCSP = false}) { + const projectResourceCollections = resourceFactory.createCollectionsForTree(tree); - const combo = new ReaderCollectionPrioritized({ - name: "server - prioritize workspace over dependencies", - readers: [workspace, projectResourceCollections.dependencies] - }); - const resourceCollections = { - source: projectResourceCollections.source, - dependencies: projectResourceCollections.dependencies, - combo - }; - - const app = express(); - - const oCspConfig = { - allowDynamicPolicySelection: true, - allowDynamicPolicyDefinition: true, - definedPolicies: { - "sap-target-level-1": - "default-src 'self'; " + - "script-src 'self' 'unsafe-eval'; " + - "style-src 'self' 'unsafe-inline'; " + - "font-src 'self' data:; " + - "img-src 'self' * data: blob:; " + - "frame-src 'self' https: data: blob:; " + - "child-src 'self' https: data: blob:; " + - "connect-src 'self' https: wss:;", - "sap-target-level-2": - "default-src 'self'; " + - "script-src 'self'; " + - "style-src 'self' 'unsafe-inline'; " + - "font-src 'self' data:; " + - "img-src 'self' * data: blob:; " + - "frame-src 'self' https: data: blob:; " + - "child-src 'self' https: data: blob:; " + - "connect-src 'self' https: wss:;" - } - }; - if ( sendSAPTargetCSP ) { - Object.assign(oCspConfig, { - defaultPolicy: "sap-target-level-1", - defaultPolicyIsReportOnly: true, - defaultPolicy2: "sap-target-level-2", - defaultPolicy2IsReportOnly: true, - }); - } - app.use(csp("sap-ui-xx-csp-policy", oCspConfig)); + // TODO change to ReaderCollection once duplicates are sorted out + const combo = new ReaderCollectionPrioritized({ + name: "server - prioritize workspace over dependencies", + readers: [projectResourceCollections.source, projectResourceCollections.dependencies] + }); - app.use(compression()); - app.use(cors()); + const resources = { + rootProject: projectResourceCollections.source, + dependencies: projectResourceCollections.dependencies, + all: combo + }; + + const middlewareManager = new MiddlewareManager({ + tree, + resources, + options: { + sendSAPTargetCSP + } + }); - app.use("/discovery", discovery({resourceCollections})); - app.use(serveResources({resourceCollections})); - app.use(serveThemes({resourceCollections})); - app.use("/resources/sap-ui-version.json", versionInfo({resourceCollections, tree})); + let app = express(); + await middlewareManager.applyMiddleware(app); - app.use("/proxy", ui5connect.proxy({ - secure: false - })); + if (h2) { + app = _addSsl({app, key, cert}); + } - // Handle anything but read operations *before* the serveIndex middleware - // as it will reject them with a 405 (Method not allowed) instead of 404 like our old tooling - app.use(nonReadRequests({resourceCollections})); - app.use(serveIndex({resourceCollections})); + const {port, server} = await _listen(app, requestedPort, changePortIfInUse, acceptRemoteConnections); - if (h2) { - return _addSsl({app, h2, key, cert}); + return { + h2, + port, + close: function(callback) { + server.close(callback); } - return app; - }).then((app) => { - return _listen(app, port, changePortIfInUse, acceptRemoteConnections).then(function({port, server}) { - return { - h2, - port, - close: function(callback) { - server.close(callback); - } - }; - }); - }); + }; } }; diff --git a/test/lib/indexLegacyExport.js b/test/lib/indexLegacyExport.js new file mode 100644 index 00000000..2313f8bd --- /dev/null +++ b/test/lib/indexLegacyExport.js @@ -0,0 +1,31 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + +test.serial("Correct legacy mapping", async (t) => { + const serveIndexStub = sinon.stub(); + mock("../../lib/middleware/serveIndex", serveIndexStub); + + mock.reRequire("../../index"); + const index = require("../../index"); + const resourceCollections = { + combo: "combo", + source: "source", + dependencies: "dependencies" + }; + index.middleware.serveIndex({ + resourceCollections, + tree: "tree" + }); + + t.deepEqual(serveIndexStub.getCall(0).args[0], { + resources: { + all: "combo", + rootProject: "source", + dependencies: "dependencies" + }, + tree: "tree" + }); + mock.stop("../../lib/middleware/serveIndex"); + mock.reRequire("../../index"); +}); diff --git a/test/lib/server/middleware/MiddlewareManager.js b/test/lib/server/middleware/MiddlewareManager.js new file mode 100644 index 00000000..38df62d9 --- /dev/null +++ b/test/lib/server/middleware/MiddlewareManager.js @@ -0,0 +1,494 @@ +const test = require("ava"); +const sinon = require("sinon"); +const MiddlewareManager = require("../../../../lib/middleware/MiddlewareManager"); +const middlewareRepository = require("../../../../lib/middleware/middlewareRepository"); + +test("Missing parameters", async (t) => { + const err = t.throws(() => { + new MiddlewareManager({ + tree: {}, + resources: {} + }); + }); + t.deepEqual(err.message, "[MiddlewareManager]: One or more mandatory parameters not provided", + "Threw error with correct message"); +}); + +test("Correct parameters", async (t) => { + t.notThrows(() => { + new MiddlewareManager({ + tree: {}, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + }, "No error thrown"); +}); + +test("applyMiddleware", async (t) => { + const middlewareManager = new MiddlewareManager({ + tree: {}, + resources: { + all: "I", + rootProject: "love", + dependencies: "ponies" + } + }); + + const addStandardMiddlewareStub = sinon.stub(middlewareManager, "addStandardMiddleware").resolves(); + const addCustomMiddlewareStub = sinon.stub(middlewareManager, "addCustomMiddleware").resolves(); + middlewareManager.middlewareExecutionOrder.push(["ponyware"]); + middlewareManager.middleware["ponyware"] = { + mountPath: "/myMountPath", + middleware: "myMiddleware" + }; + + const appUseStub = sinon.stub(); + const app = { + use: appUseStub + }; + + await middlewareManager.applyMiddleware(app); + t.deepEqual(addStandardMiddlewareStub.callCount, 1, "addStandardMiddleware got called once"); + t.deepEqual(addCustomMiddlewareStub.callCount, 1, "addCustomMiddleware got called once"); + t.deepEqual(appUseStub.callCount, 1, "app.use got called once"); + t.deepEqual(appUseStub.getCall(0).args[0], "/myMountPath", "app.use got called with correct mount path parameter"); + t.deepEqual(appUseStub.getCall(0).args[1], "myMiddleware", "app.use got called with correct middleware parameter"); +}); + +test("addMiddleware: Add already added middleware", async (t) => { + const middlewareManager = new MiddlewareManager({ + tree: {}, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + + await middlewareManager.addMiddleware("serveIndex"); + const err = await t.throwsAsync(() => { + return middlewareManager.addMiddleware("serveIndex"); + }); + t.deepEqual(err.message, "Failed to add duplicate middleware serveIndex", "Rejected with correct error message"); +}); + +test("addMiddleware: Add middleware", async (t) => { + const middlewareManager = new MiddlewareManager({ + tree: {}, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + + await middlewareManager.addMiddleware("compression"); // Add some middleware + + await middlewareManager.addMiddleware("serveIndex"); // Add middleware to test for + t.truthy(middlewareManager.middleware["serveIndex"], "Middleware got added to internal map"); + t.truthy(middlewareManager.middleware["serveIndex"].middleware, "Middleware module is given"); + t.deepEqual(middlewareManager.middleware["serveIndex"].mountPath, "/", "Correct default mount path set"); + + t.deepEqual(middlewareManager.middlewareExecutionOrder.length, 2, + "Two middleware got added to middleware execution order"); + t.deepEqual(middlewareManager.middlewareExecutionOrder[1], "serveIndex", + "Last added middleware was added to the end of middleware execution order array"); +}); + +test("addMiddleware: Add middleware with beforeMiddleware and mountPath parameter", async (t) => { + const middlewareManager = new MiddlewareManager({ + tree: {}, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + + await middlewareManager.addMiddleware("compression"); // Add some middleware + + await middlewareManager.addMiddleware("serveIndex", { // Add middleware to test for + beforeMiddleware: "compression", + mountPath: "/pony" + }); + t.truthy(middlewareManager.middleware["serveIndex"], "Middleware got added to internal map"); + t.truthy(middlewareManager.middleware["serveIndex"].middleware, "Middleware module is given"); + t.deepEqual(middlewareManager.middleware["serveIndex"].mountPath, "/pony", "Correct mount path set"); + + t.deepEqual(middlewareManager.middlewareExecutionOrder.length, 2, + "Two middleware got added to middleware execution order"); + t.deepEqual(middlewareManager.middlewareExecutionOrder[0], "serveIndex", + "Middleware was inserted at correct position of middleware execution order array"); +}); + +test("addMiddleware: Add middleware with afterMiddleware parameter", async (t) => { + const middlewareManager = new MiddlewareManager({ + tree: {}, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + + await middlewareManager.addMiddleware("compression"); // Add some middleware + await middlewareManager.addMiddleware("cors"); // Add some middleware + + await middlewareManager.addMiddleware("serveIndex", { // Add middleware to test for + afterMiddleware: "compression" + }); + t.truthy(middlewareManager.middleware["serveIndex"], "Middleware got added to internal map"); + t.truthy(middlewareManager.middleware["serveIndex"].middleware, "Middleware module is given"); + t.deepEqual(middlewareManager.middleware["serveIndex"].mountPath, "/", "Correct default mount path set"); + + t.deepEqual(middlewareManager.middlewareExecutionOrder.length, 3, + "Three middleware got added to middleware execution order"); + t.deepEqual(middlewareManager.middlewareExecutionOrder[1], "serveIndex", + "Middleware was inserted at correct position of middleware execution order array"); +}); + +test("addMiddleware: Add middleware with invalid afterMiddleware parameter", async (t) => { + const middlewareManager = new MiddlewareManager({ + tree: {}, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + + await middlewareManager.addMiddleware("compression"); // Add some middleware + + const err = await t.throwsAsync(() => { + return middlewareManager.addMiddleware("serveIndex", { // Add middleware to test for + afterMiddleware: "🦆" + }); + }); + t.deepEqual(err.message, "Could not find middleware 🦆, referenced by custom middleware serveIndex"); + + t.falsy(middlewareManager.middleware["serveIndex"], "Middleware did not get added to internal map"); + t.deepEqual(middlewareManager.middlewareExecutionOrder.length, 1, + "No new middleware got added to middleware execution order array"); +}); + +test("addMiddleware: Add middleware with rapperCallback parameter", async (t) => { + const middlewareManager = new MiddlewareManager({ + tree: {}, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + const serveIndexModule = middlewareRepository.getMiddleware("serveIndex"); + + const moduleStub = sinon.stub().returns("🍅"); + const wrapperCallbackStub = sinon.stub().returns(moduleStub); + await middlewareManager.addMiddleware("serveIndex", { // Add middleware to test for + wrapperCallback: wrapperCallbackStub + }); + t.deepEqual(wrapperCallbackStub.callCount, 1, "Wrapper callback got called once"); + t.is(wrapperCallbackStub.getCall(0).args[0], serveIndexModule, "Wrapper callback got called with correct module"); + t.deepEqual(moduleStub.callCount, 1, "Wrapper callback got called once"); + t.deepEqual(moduleStub.getCall(0).args[0].resources, { + all: "I", + rootProject: "like", + dependencies: "ponies" + }, "Wrapper callback got called with correct arguments"); + + t.truthy(middlewareManager.middleware["serveIndex"], "Middleware got added to internal map"); + t.deepEqual(middlewareManager.middleware["serveIndex"].middleware, "🍅", + "Middleware module is given"); + t.deepEqual(middlewareManager.middleware["serveIndex"].mountPath, "/", "Correct default mount path set"); + + t.deepEqual(middlewareManager.middlewareExecutionOrder.length, 1, + "One middleware got added to middleware execution order"); + t.deepEqual(middlewareManager.middlewareExecutionOrder[0], "serveIndex", + "Middleware was inserted at correct position of middleware execution order array"); +}); + +test("addMiddleware: Add middleware with async wrapperCallback", async (t) => { + const middlewareManager = new MiddlewareManager({ + tree: {}, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + const moduleStub = sinon.stub().resolves("🍅"); + const wrapperCallbackStub = sinon.stub().returns(moduleStub); + await middlewareManager.addMiddleware("serveIndex", { // Add middleware to test for + wrapperCallback: wrapperCallbackStub + }); + + t.truthy(middlewareManager.middleware["serveIndex"], "Middleware got added to internal map"); + t.deepEqual(middlewareManager.middleware["serveIndex"].middleware, "🍅", + "Middleware module is given"); +}); + +test("addStandardMiddleware: Adds standard middleware in correct order", async (t) => { + const middlewareManager = new MiddlewareManager({ + tree: {}, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + const addMiddlewareStub = sinon.stub(middlewareManager, "addMiddleware").resolves(); + await middlewareManager.addStandardMiddleware(); + + t.deepEqual(addMiddlewareStub.callCount, 10, "Expected count of middleware got added"); + const addedMiddlewareNames = []; + for (let i = 0; i < addMiddlewareStub.callCount; i++) { + addedMiddlewareNames.push(addMiddlewareStub.getCall(i).args[0]); + } + t.deepEqual(addedMiddlewareNames, [ + "csp", + "compression", + "cors", + "discovery", + "serveResources", + "serveThemes", + "versionInfo", + "connectUi5Proxy", + "nonReadRequests", + "serveIndex" + ], "Correct order of standard middlewares"); +}); + +test("addCustomMiddleware: No custom middleware defined", async (t) => { + const project = { + server: { + customMiddleware: [] + } + }; + const middlewareManager = new MiddlewareManager({ + tree: project, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + const addMiddlewareStub = sinon.stub(middlewareManager, "addMiddleware").resolves(); + await middlewareManager.addCustomMiddleware(); + + t.deepEqual(addMiddlewareStub.callCount, 0, "addMiddleware was not called"); +}); + +test("addCustomMiddleware: Custom middleware got added", async (t) => { + const project = { + metadata: { + name: "my project" + }, + server: { + customMiddleware: [{ + name: "my custom middleware A", + beforeMiddleware: "cors", + mountPath: "/pony" + }, { + name: "my custom middleware B", + afterMiddleware: "my custom middleware A" + }] + } + }; + const middlewareManager = new MiddlewareManager({ + tree: project, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + const addMiddlewareStub = sinon.stub(middlewareManager, "addMiddleware").resolves(); + await middlewareManager.addCustomMiddleware(); + + t.deepEqual(addMiddlewareStub.callCount, 2, "addMiddleware was called twice"); + t.deepEqual(addMiddlewareStub.getCall(0).args[0], "my custom middleware A", + "addMiddleware was called with correct middleware name"); + const middlewareOptionsA = addMiddlewareStub.getCall(0).args[1]; + t.deepEqual(middlewareOptionsA.mountPath, "/pony", + "addMiddleware was called with correct mountPath option"); + t.deepEqual(middlewareOptionsA.beforeMiddleware, "cors", + "addMiddleware was called with correct beforeMiddleware option"); + t.deepEqual(middlewareOptionsA.afterMiddleware, undefined, + "addMiddleware was called with correct afterMiddleware option"); + + t.deepEqual(addMiddlewareStub.getCall(1).args[0], "my custom middleware B", + "addMiddleware was called with correct middleware name"); + const middlewareOptionsB = addMiddlewareStub.getCall(1).args[1]; + t.deepEqual(middlewareOptionsB.mountPath, undefined, + "addMiddleware was called with correct mountPath option"); + t.deepEqual(middlewareOptionsB.beforeMiddleware, undefined, + "addMiddleware was called with correct beforeMiddleware option"); + t.deepEqual(middlewareOptionsB.afterMiddleware, "my custom middleware A", + "addMiddleware was called with correct afterMiddleware option"); +}); + +test("addCustomMiddleware: Custom middleware with duplicate name", async (t) => { + const project = { + metadata: { + name: "my project" + }, + server: { + customMiddleware: [{ + name: "my custom middleware A", + afterMiddleware: "my custom middleware A" + }] + } + }; + const middlewareManager = new MiddlewareManager({ + tree: project, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + middlewareManager.middleware["my custom middleware A"] = true; + const addMiddlewareStub = sinon.stub(middlewareManager, "addMiddleware").resolves(); + const err = await t.throwsAsync(() => { + return middlewareManager.addCustomMiddleware(); + }); + + t.deepEqual(err.message, "Failed to add custom middleware my custom middleware A. " + + "A middleware with the same name is already known.", + "Rejected with correct error message"); + t.deepEqual(addMiddlewareStub.callCount, 0, "Add middleware did not get called"); +}); + +test("addCustomMiddleware: Missing name configuration", async (t) => { + const project = { + metadata: { + name: "my project" + }, + server: { + customMiddleware: [{ + afterMiddleware: "my custom middleware A" + }] + } + }; + const middlewareManager = new MiddlewareManager({ + tree: project, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + const err = await t.throwsAsync(() => { + return middlewareManager.addCustomMiddleware(); + }); + + t.deepEqual(err.message, "Missing name for custom middleware definition of project my project at index 0", + "Rejected with correct error message"); +}); + +test("addCustomMiddleware: Both before- and afterMiddleware configuration", async (t) => { + const project = { + metadata: { + name: "🐧" + }, + server: { + customMiddleware: [{ + name: "🦆", + beforeMiddleware: "🐝", + afterMiddleware: "🐒" + }] + } + }; + const middlewareManager = new MiddlewareManager({ + tree: project, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + const err = await t.throwsAsync(() => { + return middlewareManager.addCustomMiddleware(); + }); + + t.deepEqual(err.message, `Custom middleware definition 🦆 of project 🐧 ` + + `defines both "beforeMiddleware" and "afterMiddleware" parameters. Only one must be defined.`, + "Rejected with correct error message"); +}); + +test("addCustomMiddleware: Missing before- or afterMiddleware configuration", async (t) => { + const project = { + metadata: { + name: "🐧" + }, + server: { + customMiddleware: [{ + name: "🦆" + }] + } + }; + const middlewareManager = new MiddlewareManager({ + tree: project, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + const err = await t.throwsAsync(() => { + return middlewareManager.addCustomMiddleware(); + }); + + t.deepEqual(err.message, `Custom middleware definition 🦆 of project 🐧 ` + + `defines neither a "beforeMiddleware" nor an "afterMiddleware" parameter. One must be defined.`, + "Rejected with correct error message"); +}); + +test("addCustomMiddleware: wrapperCallback", async (t) => { + const project = { + metadata: { + name: "my project" + }, + server: { + customMiddleware: [{ + name: "my custom middleware A", + beforeMiddleware: "cors", + configuration: { + "🦊": "🐰" + } + }] + } + }; + const middlewareManager = new MiddlewareManager({ + tree: project, + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + const addMiddlewareStub = sinon.stub(middlewareManager, "addMiddleware").resolves(); + await middlewareManager.addCustomMiddleware(); + + t.deepEqual(addMiddlewareStub.callCount, 1, "addMiddleware was called once"); + + const wrapperCallback = addMiddlewareStub.getCall(0).args[1].wrapperCallback; + const middlewareModuleStub = sinon.stub().returns("ok"); + const middlewareWrapper = wrapperCallback(middlewareModuleStub); + const res = middlewareWrapper({ + resources: "resources" + }); + t.deepEqual(res, "ok", "Wrapper callback returned expected value"); + t.deepEqual(middlewareModuleStub.callCount, 1, "Middleware module got called once"); + t.deepEqual(middlewareModuleStub.getCall(0).args[0], { + resources: "resources", + options: { + configuration: { + "🦊": "🐰" + } + } + }, "Middleware module got called with correct arguments"); +}); diff --git a/test/lib/server/middleware/middlewareRepository.js b/test/lib/server/middleware/middlewareRepository.js new file mode 100644 index 00000000..e44cb5de --- /dev/null +++ b/test/lib/server/middleware/middlewareRepository.js @@ -0,0 +1,32 @@ +const test = require("ava"); +const middlewareRepository = require("../../../../lib/middleware/middlewareRepository"); + +test("getMiddleware", async (t) => { + const cspModule = require("../../../../lib/middleware/csp"); + const res = middlewareRepository.getMiddleware("csp"); + t.is(res, cspModule, "Returned correct middleware module"); +}); + +test("getMiddleware: Unkown middleware", async (t) => { + const err = t.throws(() => { + middlewareRepository.getMiddleware("🐬"); + }); + t.deepEqual(err.message, "middlewareRepository: Unknown Middleware 🐬", + "Threw error with correct message"); +}); + +test("addMiddleware", async (t) => { + const cspModule = require("../../../../lib/middleware/csp"); + middlewareRepository.addMiddleware("🐠", "./csp"); + const res = middlewareRepository.getMiddleware("🐠"); + + t.is(res, cspModule, "Returned added middleware module"); +}); + +test("addMiddleware: Duplicate middleware", async (t) => { + const err = t.throws(() => { + middlewareRepository.addMiddleware("cors"); + }); + t.deepEqual(err.message, "middlewareRepository: Middleware cors already registered", + "Threw error with correct message"); +}); diff --git a/test/lib/server/middleware/serveIndex.js b/test/lib/server/middleware/serveIndex.js index 12727066..f2bba1d0 100644 --- a/test/lib/server/middleware/serveIndex.js +++ b/test/lib/server/middleware/serveIndex.js @@ -26,8 +26,8 @@ test.serial("Check if index for files is created", (t) => { writeResource(readerWriter, "/myFile3.properties", 1024 * 1024 * 1024), // GB ]).then(() => { const middleware = serveIndexMiddleware({ - resourceCollections: { - combo: readerWriter + resources: { + all: readerWriter } });