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
}
});