diff --git a/demo/index.js b/demo/index.js index 352f6d8..09a2ce8 100755 --- a/demo/index.js +++ b/demo/index.js @@ -19,6 +19,19 @@ mockServer({ 'Global-Custom-Header': 'Global-Custom-Header', }, customDTOToClassTemplate: __dirname + '/templates/dto_es6flow.ejs', + middleware: { + '/rest/products/#{productCode}/GET'(serverOptions, requestOptions) { + var productCode = requestOptions.req.params[0].split('/')[3]; + + if (productCode === '1234') { + requestOptions.res.statusCode = 201; + requestOptions.res.end('product 1234'); + return null; + } + + return 'success'; + } + }, swaggerImport: { protocol: 'http', authUser: undefined, diff --git a/doc/readme-middleware.md b/doc/readme-middleware.md new file mode 100644 index 0000000..28e19f0 --- /dev/null +++ b/doc/readme-middleware.md @@ -0,0 +1,20 @@ +# Middleware + +For validation and self controlled responses you can select middleware as response. +The middleware function will be called, in case of "middleware" is selected. It's response can be a string which will continue with the given string as selected response. If you return null or undefined you have to implement the response by yourself. + +[example](/demo/index.js#L22) + +## Parameter[0] serverOptions + +See [node-mock-server options](/doc/readme-options.md) + +## Parameter[1] responseOptions + +| attribute | type | description | +| ------------- | ------------- | ----- | +| req | Object | The (request object)[http://expressjs.com/en/api.html#req]. | +| res | Object | The (response object)[http://expressjs.com/en/api.html#res]. | +| method | string | Contains a string corresponding to the HTTP method of the request: GET, POST, PUT, and so on. | +| dir | string | The directory of selected response | +| preferences | Object | The preferences object | diff --git a/doc/readme-options.md b/doc/readme-options.md index 80a2534..509830b 100644 --- a/doc/readme-options.md +++ b/doc/readme-options.md @@ -97,6 +97,12 @@ Default value: `origin, x-requested-with, content-type` A string that define the header "Access-Control-Allow-Headers". +#### options.middleware +Type: `Object` +Optional + +A object including the middleware functions. +Read [middleware.md](/doc/readme-middleware.md) for details. #### options.swaggerImport Type: `Object` diff --git a/lib/UserInterface.js b/lib/UserInterface.js index 63ee966..4ba71c0 100755 --- a/lib/UserInterface.js +++ b/lib/UserInterface.js @@ -396,6 +396,9 @@ UserInterface.prototype = extend(UserInterface.prototype, { } var availableMockResponses = this.readDir(mockPath, ['response.txt', '.DS_Store']); + availableMockResponses.push({ + file: 'middleware', + }); var availableMockResponsesOut = []; var selected; try { diff --git a/lib/controller/MockController.js b/lib/controller/MockController.js index eaa5a15..0d45f98 100644 --- a/lib/controller/MockController.js +++ b/lib/controller/MockController.js @@ -37,22 +37,34 @@ MockController.prototype = extend(MockController.prototype, { appController.app.all('/*', this._handleMockRequest.bind(this)); }, + /** + * @method _acceptMiddleware + * @param {Object} serverOptions + * @param {Object} responseOptions + * @private + */ + _acceptMiddleware: function (serverOptions, responseOptions) { + var endPointId = responseOptions.dir.replace(serverOptions.dirName, '').replace(/\/$/, ''); + var middleware = serverOptions.middleware; + + if (!endPointId || !middleware || typeof middleware[endPointId] !== 'function') { + responseOptions.res.statusCode = 500; + responseOptions.res.end('Error: middleware for ' + endPointId + '" don\'t exist!'); + return false; + } + + return middleware[endPointId](serverOptions, responseOptions); + }, + /** * @method _handleMockRequest - * @param {object} req - * @param {object} res + * @param {Object} req + * @param {Object} res * @private */ _handleMockRequest: function (req, res) { var path = req.originalUrl.replace(this.options.urlPath, this.options.restPath); - var method = req.method; - var dir = this._findFolder(path, this.options) + '/' + method + '/'; - var expectedResponse = this._getExpectedResponse(req, dir); - var preferences = this.getPreferences(this.options); - var timeout = 0; - var responseFilePath; - var responseHeadersFilePath; var responseHeaders; var headers = this.options.headers || {}; var options; @@ -62,55 +74,42 @@ MockController.prototype = extend(MockController.prototype, { return true; } - if (preferences && preferences.responseDelay) { - timeout = parseInt(preferences.responseDelay, 10); - } + options = this.getResponseOptions(req, res); - responseFilePath = dir + 'mock/' + expectedResponse + '.json'; - responseHeadersFilePath = dir + 'mock/' + expectedResponse + '.headers.json'; + if (!options) { + return; + } // Fallback to success.json - if (!this.existFile(responseFilePath)) { - expectedResponse = 'success'; - responseFilePath = dir + 'mock/success.json'; - this.writeFile(dir + 'mock/response.txt', 'success'); + if (!this.existFile(options.responseFilePath)) { + options.expectedResponse = 'success'; + options.responseFilePath = options.dir + 'mock/success.json'; + this.writeFile(options.dir + 'mock/response.txt', 'success'); } // Add response headers - if (this.existFile(responseHeadersFilePath)) { - responseHeaders = JSON.parse(this.readFile(responseHeadersFilePath)) || {}; + if (this.existFile(options.responseHeadersFilePath)) { + responseHeaders = JSON.parse(this.readFile(options.responseHeadersFilePath)) || {}; } - options = { - req: req, - res: res, - path: path, - method: method, - dir: dir, - expectedResponse: expectedResponse, - preferences: preferences, - timeout: timeout, - responseFilePath: responseFilePath, - }; - this._writeDefaultHeader(res, extend(headers, responseHeaders)); setTimeout(function () { if (!this._hasValidDynamicPathParam(options)) { this._sendErrorEmptyPath(options); - } else if (expectedResponse.search('error') >= 0) { + } else if (options.expectedResponse.search('error') >= 0) { this._sendError(options); - } else if (method === 'HEAD') { + } else if (options.method === 'HEAD') { this._sendHead(options); } else { this._sendSuccess(options); } - }.bind(this), timeout); + }.bind(this), options.timeout); }, /** * @method _sendSuccess - * @param {object} options + * @param {Object} options * @returns {void} * @private */ @@ -146,9 +145,62 @@ MockController.prototype = extend(MockController.prototype, { }, + /** + * @method getResponseOptions + * @param {Object} req + * @param {Object} res + * @public + */ + getResponseOptions: function (req, res) { + + var path = req.originalUrl.replace(this.options.urlPath, this.options.restPath); + var method = req.method; + var dir = this._findFolder(path, this.options) + '/' + method + '/'; + var expectedResponse = this._getExpectedResponse(req, dir); + var preferences = this.getPreferences(this.options); + var timeout = 0; + var responseFilePath = dir + 'mock/' + expectedResponse + '.json'; + var responseHeadersFilePath = dir + 'mock/' + expectedResponse + '.headers.json'; + + if (preferences && preferences.responseDelay) { + timeout = parseInt(preferences.responseDelay, 10); + } + + if (expectedResponse === 'middleware') { + var middlewareResponse = this._acceptMiddleware(this.options, { + req: req, + res: res, + method: method, + dir: dir, + preferences: preferences, + }); + + if (typeof middlewareResponse === 'string') { + expectedResponse = middlewareResponse; + responseFilePath = dir + 'mock/' + expectedResponse + '.json'; + responseHeadersFilePath = dir + 'mock/' + expectedResponse + '.headers.json'; + } else { + return undefined; + } + } + + return { + req: req, + res: res, + path: path, + method: method, + dir: dir, + expectedResponse: expectedResponse, + preferences: preferences, + timeout: timeout, + responseFilePath: responseFilePath, + responseHeadersFilePath: responseHeadersFilePath, + }; + }, + /** * @method _sendError - * @param {object} options + * @param {Object} options * @returns {void} * @private */ @@ -162,13 +214,16 @@ MockController.prototype = extend(MockController.prototype, { status = parseInt(reg[3], 10); } - options.res.statusCode = status; + if (options.res.statusCode === 200) { + options.res.statusCode = status; + } + options.res.send(this.readFile(options.responseFilePath)); }, /** * @method _sendError - * @param {object} options + * @param {Object} options * @returns {void} * @private */ @@ -186,7 +241,7 @@ MockController.prototype = extend(MockController.prototype, { /** * @method _sendHead - * @param {object} options + * @param {Object} options * @returns {void} * @private */ @@ -229,7 +284,7 @@ MockController.prototype = extend(MockController.prototype, { /** * @method _hasValidDynamicPathParam - * @param {object} options + * @param {Object} options * @returns {boolean} * @private */ @@ -267,9 +322,9 @@ MockController.prototype = extend(MockController.prototype, { /** * @method _getResponseFiles - * @param {object} options - * @param {object} responseData - * @returns {object} + * @param {Object} options + * @param {Object} responseData + * @returns {Object} * @private */ _getResponseFiles: function (options, responseData) { @@ -294,7 +349,7 @@ MockController.prototype = extend(MockController.prototype, { /** * @method _getExpectedResponse - * @param {object} req + * @param {Object} req * @param {string} dir * @returns {string} * @private @@ -324,7 +379,7 @@ MockController.prototype = extend(MockController.prototype, { /** * @method _getFunc * @param {string|Array} path - * @returns {object} + * @returns {Object} * @private */ _getFunc: function (path) { @@ -376,8 +431,8 @@ MockController.prototype = extend(MockController.prototype, { /** * @method _getDynamicPathParams - * @param {object} options - * @returns {object} + * @param {Object} options + * @returns {Object} * @private */ _getDynamicPathParams: function (options) { @@ -410,9 +465,9 @@ MockController.prototype = extend(MockController.prototype, { /** * @method _getResponseData - * @param {object} req + * @param {Object} req * @param {string} method - * @returns {object} + * @returns {Object} * @private */ _getResponseData: function (req, method) { @@ -452,8 +507,8 @@ MockController.prototype = extend(MockController.prototype, { /** * @method _findFolder * @param {string} path - * @param {object} options - * @returns {object} + * @param {Object} options + * @returns {Object} * @private */ _findFolder: function (path, options) { @@ -497,8 +552,8 @@ MockController.prototype = extend(MockController.prototype, { /** * @method _writeDefaultHeader - * @param {object} customHeaders - * @param {object} res + * @param {Object} customHeaders + * @param {Object} res * @returns {void} * @private */ diff --git a/package.json b/package.json index daf7873..81e7c27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-mock-server", - "version": "0.12.1", + "version": "0.13.0", "description": "File based Node REST API mock server", "email": "simon.mollweide@web.de", "author": "Simon Mollweide ", diff --git a/readme.md b/readme.md index 283f048..1c9493c 100755 --- a/readme.md +++ b/readme.md @@ -20,10 +20,11 @@ - [Query params in mock data](/doc/readme-query-params.md) - [Dynamic path params in mock data](/doc/readme-path-params.md) - [Expected responses](/doc/readme-expected-response.md) +- [Middleware responses](/doc/readme-middleware.md) - [Error cases](/doc/readme-expected-response.md) - [Swagger import](/doc/readme-swagger-import.md) - - DTO import - - DTO response function + - DTO import + - DTO response function - [Response validation](/doc/readme-response-validation.md) - [Response header](/doc/readme-response-header.md) - [DTO to Class converter](/doc/readme-dto-2-class.md) @@ -59,30 +60,43 @@ var mockServer = require('node-mock-server'); mockServer({ restPath: __dirname + '/mock/rest', dirName: __dirname, - title: 'Api mock server', - version: 2, - urlBase: 'http://localhost:3003', - urlPath: '/rest/v2', - port: 3003, - funcPath: __dirname + '/func', - headers: { - 'Global-Custom-Header': 'Global-Custom-Header' - }, - customDTOToClassTemplate: __dirname + '/templates/dto_es6flow.ejs', - swaggerImport: { - protocol: 'http', - authUser: undefined, - authPass: undefined, - host: 'localhost', - port: 3001, - path: '/src/swagger/swagger-demo-docs.json', - dest: dest, - replacePathsStr: '/v2/{baseSiteId}', - createErrorFile: true, - createEmptyFile: true, - overwriteExistingDescriptions: true, - responseFuncPath: __dirname + '/func-imported' - } + title: 'Api mock server', + version: 2, + urlBase: 'http://localhost:3003', + urlPath: '/rest/v2', + port: 3003, + funcPath: __dirname + '/func', + headers: { + 'Global-Custom-Header': 'Global-Custom-Header' + }, + customDTOToClassTemplate: __dirname + '/templates/dto_es6flow.ejs', + middleware: { + '/rest/products/#{productCode}/GET'(serverOptions, requestOptions) { + var productCode = requestOptions.req.params[0].split('/')[3]; + + if (productCode === '1234') { + requestOptions.res.statusCode = 201; + requestOptions.res.end('product 1234'); + return null; + } + + return 'success'; + } + }, + swaggerImport: { + protocol: 'http', + authUser: undefined, + authPass: undefined, + host: 'petstore.swagger.io', + port: 80, + path: '/v2/swagger.json', + dest: dest, + replacePathsStr: '/v2/{baseSiteId}', + createErrorFile: true, + createEmptyFile: true, + overwriteExistingDescriptions: true, + responseFuncPath: __dirname + '/func-imported' + } }); ``` diff --git a/test/tests-mock-server.js b/test/tests-mock-server.js index 9cabd5c..5a9b676 100755 --- a/test/tests-mock-server.js +++ b/test/tests-mock-server.js @@ -117,6 +117,26 @@ module.exports = function(serverOptions, _getFile) { }); }); + it('GET /products/{productCode} - with middleware', function (done) { + _fetch({ + url: baseUrl + '/products/31221?_expected=middleware', + success: function (data) { + assert.equal(data, 'middware response'); + done(); + } + }); + }); + + it('GET /products/{productCode} - with middleware 2', function (done) { + _fetch({ + url: baseUrl + '/products/1234?_expected=middleware', + success: function (data) { + assert.equal(data, 'product 1234'); + done(); + } + }); + }); + it('GET /search/users/{userId}/products/{productCode}/available - with empty dynamic path param', function (done) { _fetch({ url: baseUrl + '/search/users//products/1/available?_expected=success', @@ -261,4 +281,4 @@ module.exports = function(serverOptions, _getFile) { }); -}; \ No newline at end of file +}; diff --git a/test/tests.js b/test/tests.js index fccc055..4561054 100755 --- a/test/tests.js +++ b/test/tests.js @@ -23,6 +23,21 @@ serverOptions = { 'Global-Custom-Header': 'Global-Custom-Header' }, customDTOToClassTemplate: __dirname + '/data/class-templates/dto_es6flow.ejs', + middleware: { + '/../demo/rest/products/#{productCode}/GET'(serverOptions, requestOptions) { + + var productCode = requestOptions.req.params[0].split('/')[3]; + + if (productCode === '1234') { + requestOptions.res.statusCode = 201; + requestOptions.res.end('product 1234'); + return null; + } + + requestOptions.res.end('middware response'); + return null; + } + }, swaggerImport: { protocol: 'http', dirName: __dirname, diff --git a/views/method-modal.ejs b/views/method-modal.ejs index d1b1a06..f809387 100755 --- a/views/method-modal.ejs +++ b/views/method-modal.ejs @@ -135,34 +135,36 @@ - <% for(var m=0; m - <% var mock = method.availableMockResponses[m]; %> - <% var checked = mock.isSelected ? 'checked' : ''; %> - <% - var label = ''; - if (mock.isValidated) { - if (mock.isValid) { - label = 'Valid'; - } else { - label = 'Invalid (' + mock.inValidCounter + ')'; - } - } - %> -
- -  preview - <% if (mock.name.search(/error/) < 0) { %> + <% for(var m=0; m + <% var mock = method.availableMockResponses[m]; %> + <% var checked = mock.isSelected ? 'checked' : ''; %> + <% var isMiddleware = mock.name.search(/^middleware$/) >= 0; %> + <% var isError = mock.name.search(/error/) >= 0; %> + <% + var label = ''; + if (mock.isValidated && !isMiddleware) { + if (mock.isValid) { + label = 'Valid'; + } else { + label = 'Invalid (' + mock.inValidCounter + ')'; + } + } + %> +
+ + <% if (!isError && !isMiddleware) { %> +  preview - <% } %> -
- <% } %> + <% } %> +
+ <% } %>