From 4c006f315d18ae943f53b12572d7361b2b372dd0 Mon Sep 17 00:00:00 2001 From: Daan Sieben Date: Thu, 27 Sep 2018 10:33:16 +0200 Subject: [PATCH 1/4] Fixing wildcard matcher --- api-mocker.js | 119 ++++++++++++------- test/api-mocker.spec.js | 10 ++ test/mocks/users/__user_id__/nested/GET.json | 3 + 3 files changed, 89 insertions(+), 43 deletions(-) create mode 100644 test/mocks/users/__user_id__/nested/GET.json diff --git a/api-mocker.js b/api-mocker.js index 23e98d0..df0cb42 100644 --- a/api-mocker.js +++ b/api-mocker.js @@ -50,6 +50,76 @@ function logger(params) { } } + +/** + * Locate all possible options to match a file request, select the first one (most relevant) + * @param {string} path Requst path to API mock file. + */ +function findMatchingPath(path) { + let pathParts = path.split('/'); + let pathOptions = recurseLookup([pathParts.shift()], pathParts, []); + if (pathOptions.length < 1) { + return false; + } + return pathOptions[0]; +} +/** + * Recursively loop through path to find all possible path matches including wildcards + * @param {string[]} basePath rootPath to traverse down form + * @param {string[]} lookupPath section of path to traverse + * @param {object[]} existingParams list of params found on the basepath (key, value) + */ +function recurseLookup(basePath, lookupPath, existingParams) { + var paths = []; + var matchingFolders = findMatchingFolderOnLevel(basePath.join('/'), lookupPath[0], existingParams); + if(lookupPath.length < 2) { return matchingFolders; } + matchingFolders.forEach(folder => { + paths = paths.concat(recurseLookup(folder.path.split('/'), lookupPath.slice(1), folder.params)); + }); + return paths; +} +/** + * Find possible folder matches for current path + * @param {string} parentPath path to current level + * @param {string} testPath folder to locate on current level + * @param {object[]} existingParams list of params found on the parentPath (key, value) + */ +function findMatchingFolderOnLevel(parentPath, testPath, existingParams) { + var pathOptions = []; + if (parentPath === false || !fs.existsSync(parentPath)) { + return pathOptions; + } + if (fs.existsSync(parentPath + '/' + testPath)) { + pathOptions.push({ + path: parentPath + '/' + testPath, + params: existingParams.concat([]) + }); + } + var wildcardFolders = fs.readdirSync(parentPath) + .filter(function (file) { + return fs.lstatSync(path.join(parentPath, file)).isDirectory(); + }) + .filter(function (folder_name) { + return folder_name.slice(0, 2) == '__' && folder_name.slice(-2) == '__'; + }) + .map(function (wildcardFolder) { + return { + param: wildcardFolder.slice(2, -2), + folder: wildcardFolder + }; + }); + if (wildcardFolders.length > 0) { + wildcardFolders.forEach(wildcardFolder => { + var pathOption = { + path: parentPath + '/' + wildcardFolder.folder, + params: existingParams.concat({key: wildcardFolder.param, value: testPath}) + }; + pathOptions.push(pathOption); + }); + } + return pathOptions; +} + /** * @param {string|object} urlRoot Base path for API url or full config object * @param {string|object} pathRoot Base path of API mock files. eg: ./mock/api or @@ -148,50 +218,13 @@ module.exports = function (urlRoot, pathRoot) { if (fs.existsSync(targetFullPath)) { return returnForPath(targetFullPath); } else { - var requestParams = {}; - - var newTarget = targetPath.split('/').reduce(function (currentFolder, nextFolder, index) { - if (currentFolder === false) { - return ''; - } - // First iteration - if (currentFolder === '') { - return nextFolder; - } - - var pathToCheck = currentFolder + '/' + nextFolder; - if (fs.existsSync(pathToCheck)) { - return pathToCheck - } else { - if (!fs.existsSync(currentFolder)) { - return false; - } - - var folders = fs.readdirSync(currentFolder) - .filter(function (file) { - return fs.lstatSync(path.join(currentFolder, file)).isDirectory(); - }) - .filter(function (folder_name) { - return folder_name.slice(0, 2) == '__' && folder_name.slice(-2) == '__'; - }) - .map(function (wildcardFolder) { - return { - param: wildcardFolder.slice(2, -2), - folder: wildcardFolder - }; - }); - - if (folders.length > 0) { - requestParams[folders[0].param] = nextFolder; - return currentFolder + '/' + folders[0].folder; - } else { - return false; - } - } - }, ''); - + var newTarget = findMatchingPath(targetPath); if (newTarget) { - return returnForPath(newTarget, requestParams); + var requestParams = {}; + newTarget.params.forEach(param => { + requestParams[param.key] = param.value; + }); + return returnForPath(newTarget.path, requestParams); } else { return returnNotFound(); } diff --git a/test/api-mocker.spec.js b/test/api-mocker.spec.js index 0cb0b9a..94ff0e3 100644 --- a/test/api-mocker.spec.js +++ b/test/api-mocker.spec.js @@ -74,6 +74,16 @@ describe('Simple configuration with baseUrl', function () { }, done); }); + it('wildcard mock works properly with nested resources', function (done) { + request(app) + .get('/api/users/1/nested') + .expect(200) + .expect('Content-Type', /json/) + .expect({ + result: 'WILDCARD_NESTED' + }, done); + }); + it('custom response will not cache', function (done) { fs.mkdirSync('./test/mocks/users/2'); fs.writeFileSync('./test/mocks/users/2/GET.js', fs.readFileSync('./test/mocks/users/__user_id__/GET_example1.js')); diff --git a/test/mocks/users/__user_id__/nested/GET.json b/test/mocks/users/__user_id__/nested/GET.json new file mode 100644 index 0000000..c3139a3 --- /dev/null +++ b/test/mocks/users/__user_id__/nested/GET.json @@ -0,0 +1,3 @@ +{ + "result": "WILDCARD_NESTED" +} \ No newline at end of file From 28ff0f083d1fae3bad2b345bac264c7d429d0aa9 Mon Sep 17 00:00:00 2001 From: Daan Sieben Date: Thu, 27 Sep 2018 11:19:42 +0200 Subject: [PATCH 2/4] Add node 5 support --- api-mocker.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api-mocker.js b/api-mocker.js index df0cb42..b1cc1f9 100644 --- a/api-mocker.js +++ b/api-mocker.js @@ -73,7 +73,7 @@ function recurseLookup(basePath, lookupPath, existingParams) { var paths = []; var matchingFolders = findMatchingFolderOnLevel(basePath.join('/'), lookupPath[0], existingParams); if(lookupPath.length < 2) { return matchingFolders; } - matchingFolders.forEach(folder => { + matchingFolders.forEach(function (folder) { paths = paths.concat(recurseLookup(folder.path.split('/'), lookupPath.slice(1), folder.params)); }); return paths; @@ -109,7 +109,7 @@ function findMatchingFolderOnLevel(parentPath, testPath, existingParams) { }; }); if (wildcardFolders.length > 0) { - wildcardFolders.forEach(wildcardFolder => { + wildcardFolders.forEach(function (wildcardFolder) { var pathOption = { path: parentPath + '/' + wildcardFolder.folder, params: existingParams.concat({key: wildcardFolder.param, value: testPath}) @@ -221,7 +221,7 @@ module.exports = function (urlRoot, pathRoot) { var newTarget = findMatchingPath(targetPath); if (newTarget) { var requestParams = {}; - newTarget.params.forEach(param => { + newTarget.params.forEach(function (param) { requestParams[param.key] = param.value; }); return returnForPath(newTarget.path, requestParams); From 6daed02033c7667666aa4e97973cc6ab9a2f1a39 Mon Sep 17 00:00:00 2001 From: Daan Sieben Date: Thu, 27 Sep 2018 11:29:41 +0200 Subject: [PATCH 3/4] Add node 5 support --- api-mocker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api-mocker.js b/api-mocker.js index b1cc1f9..5645d29 100644 --- a/api-mocker.js +++ b/api-mocker.js @@ -56,8 +56,8 @@ function logger(params) { * @param {string} path Requst path to API mock file. */ function findMatchingPath(path) { - let pathParts = path.split('/'); - let pathOptions = recurseLookup([pathParts.shift()], pathParts, []); + var pathParts = path.split('/'); + var pathOptions = recurseLookup([pathParts.shift()], pathParts, []); if (pathOptions.length < 1) { return false; } From 851136215268f8f338e1d5a1e12f0f9eaf2f8588 Mon Sep 17 00:00:00 2001 From: Daan Sieben Date: Thu, 27 Sep 2018 14:56:30 +0200 Subject: [PATCH 4/4] Fix file path matcher + add ANY.js --- api-mocker.js | 90 ++++++++++++-------- test/api-mocker.spec.js | 74 +++++++++++----- test/mocks/users/1/any-js-request/ANY.js | 5 ++ test/mocks/users/1/any-json-request/ANY.json | 3 + 4 files changed, 115 insertions(+), 57 deletions(-) create mode 100644 test/mocks/users/1/any-js-request/ANY.js create mode 100644 test/mocks/users/1/any-json-request/ANY.json diff --git a/api-mocker.js b/api-mocker.js index 5645d29..8959d07 100644 --- a/api-mocker.js +++ b/api-mocker.js @@ -36,9 +36,9 @@ function escapeRegExp(str) { function defaultLogger(params) { console.log( chalk.bgYellow.black('api-mocker') + ' ' + - chalk.green(params.req.method.toUpperCase()) + ' ' + + chalk.green(params.req.method.toUpperCase()) + ' ' + chalk.blue(params.req.originalUrl) + ' => ' + - chalk.cyan(params.filePath + '.' + params.fileType) + chalk.cyan(params.filePath) ); } @@ -53,15 +53,26 @@ function logger(params) { /** * Locate all possible options to match a file request, select the first one (most relevant) - * @param {string} path Requst path to API mock file. + * @param {string} requestPath Requst path to API mock file. + * @param {string[]} requestMethodFiles A list of files that match the request */ -function findMatchingPath(path) { - var pathParts = path.split('/'); +function findMatchingPath(requestPath, requestMethodFiles) { + var pathParts = requestPath.split('/'); var pathOptions = recurseLookup([pathParts.shift()], pathParts, []); - if (pathOptions.length < 1) { - return false; - } - return pathOptions[0]; + + var result = false; + pathOptions.some(function (pathOption) { + return requestMethodFiles.some(function (requestMethodFile) { + if (fs.existsSync(path.join(pathOption.path, requestMethodFile))) { + result = { + path: path.resolve(path.join(pathOption.path, requestMethodFile)), + params: pathOption.params + }; + return true; + } + }); + }); + return result; } /** * Recursively loop through path to find all possible path matches including wildcards @@ -72,7 +83,7 @@ function findMatchingPath(path) { function recurseLookup(basePath, lookupPath, existingParams) { var paths = []; var matchingFolders = findMatchingFolderOnLevel(basePath.join('/'), lookupPath[0], existingParams); - if(lookupPath.length < 2) { return matchingFolders; } + if (lookupPath.length < 2) { return matchingFolders; } matchingFolders.forEach(function (folder) { paths = paths.concat(recurseLookup(folder.path.split('/'), lookupPath.slice(1), folder.params)); }); @@ -89,13 +100,13 @@ function findMatchingFolderOnLevel(parentPath, testPath, existingParams) { if (parentPath === false || !fs.existsSync(parentPath)) { return pathOptions; } - if (fs.existsSync(parentPath + '/' + testPath)) { + if (fs.existsSync(path.join(parentPath, testPath))) { pathOptions.push({ - path: parentPath + '/' + testPath, + path: path.join(parentPath, testPath), params: existingParams.concat([]) }); } - var wildcardFolders = fs.readdirSync(parentPath) + fs.readdirSync(parentPath) .filter(function (file) { return fs.lstatSync(path.join(parentPath, file)).isDirectory(); }) @@ -107,16 +118,13 @@ function findMatchingFolderOnLevel(parentPath, testPath, existingParams) { param: wildcardFolder.slice(2, -2), folder: wildcardFolder }; - }); - if (wildcardFolders.length > 0) { - wildcardFolders.forEach(function (wildcardFolder) { + }).forEach(function (wildcardFolder) { var pathOption = { - path: parentPath + '/' + wildcardFolder.folder, + path: path.join(parentPath, wildcardFolder.folder), params: existingParams.concat({key: wildcardFolder.param, value: testPath}) }; pathOptions.push(pathOption); }); - } return pathOptions; } @@ -180,13 +188,11 @@ module.exports = function (urlRoot, pathRoot) { return res.end('Endpoint not found on mock files: ' + url); }; - var returnForPath = function (targetFolder, requestParams) { - var filePath = path.resolve(targetFolder, req.method); - - if (fs.existsSync(filePath + '.js')) { + var returnForPath = function (filePath, requestParams) { + if (filePath.endsWith('.js')) { logger({ req: req, filePath: filePath, fileType: 'js', config: config }) - delete require.cache[require.resolve(filePath + '.js')]; - var customMiddleware = require(filePath + '.js'); + delete require.cache[require.resolve(path.resolve(filePath))]; + var customMiddleware = require(path.resolve(filePath)); if (requestParams) { req.params = requestParams; } @@ -201,24 +207,36 @@ module.exports = function (urlRoot, pathRoot) { fileType = req.accepts(['json', 'xml']); }; - if (fs.existsSync(filePath + '.' + fileType)) { - logger({ req: req, filePath: filePath, fileType: fileType, config: config }) - var buf = fs.readFileSync(filePath + '.' + fileType); + logger({ req: req, filePath: filePath, fileType: fileType, config: config }) + var buf = fs.readFileSync(filePath); - res.setHeader('Content-Type', 'application/' + fileType); + res.setHeader('Content-Type', 'application/' + fileType); - return res.end(buf); - - } else { - return returnNotFound(); - } + return res.end(buf); } }; + var methodFileExtension = config.type || 'json'; + if (methodFileExtension == 'auto') { + methodFileExtension = req.accepts(['json', 'xml']); + } + var jsMockFile = req.method + '.js'; + var staticMockFile = req.method + '.' + methodFileExtension; + var wildcardJsMockFile = 'ANY.js'; + var wildcardStaticMockFile = 'ANY.' + methodFileExtension; + + var methodFiles = [jsMockFile, staticMockFile, wildcardJsMockFile, wildcardStaticMockFile]; + + var matchedMethodFile = methodFiles.find(function (methodFile) { + if (fs.existsSync(path.join(targetFullPath, methodFile))) { + return true; + } + return false; + }); - if (fs.existsSync(targetFullPath)) { - return returnForPath(targetFullPath); + if (matchedMethodFile) { + return returnForPath(path.resolve(path.join(targetFullPath, matchedMethodFile))); } else { - var newTarget = findMatchingPath(targetPath); + var newTarget = findMatchingPath(targetPath, methodFiles); if (newTarget) { var requestParams = {}; newTarget.params.forEach(function (param) { diff --git a/test/api-mocker.spec.js b/test/api-mocker.spec.js index 94ff0e3..6d3ec0e 100644 --- a/test/api-mocker.spec.js +++ b/test/api-mocker.spec.js @@ -63,27 +63,6 @@ describe('Simple configuration with baseUrl', function () { }, done); }); - it('wildcard mock works properly', function (done) { - request(app) - .get('/api/users/2812391232') - .expect('Content-Type', /json/) - .expect(200) - .expect({ - id: '2812391232', - method: 'GET' - }, done); - }); - - it('wildcard mock works properly with nested resources', function (done) { - request(app) - .get('/api/users/1/nested') - .expect(200) - .expect('Content-Type', /json/) - .expect({ - result: 'WILDCARD_NESTED' - }, done); - }); - it('custom response will not cache', function (done) { fs.mkdirSync('./test/mocks/users/2'); fs.writeFileSync('./test/mocks/users/2/GET.js', fs.readFileSync('./test/mocks/users/__user_id__/GET_example1.js')); @@ -159,6 +138,59 @@ describe('Wildcard feature', function () { .get('/notdefined/products/1') .expect(404, done); }); + + it('wildcard mock works properly', function (done) { + request(app) + .get('/api/users/2812391232') + .expect(200) + .expect('Content-Type', /json/) + .expect({ + id: '2812391232', + method: 'GET' + }, done); + }); + + it('wildcard mock works properly with nested resources', function (done) { + request(app) + .get('/api/users/1/nested') + .expect(200) + .expect('Content-Type', /json/) + .expect({ + result: 'WILDCARD_NESTED' + }, done); + }); + + it('wildcard json methods should work on any given method', function (done) { + request(app) + .get('/api/users/1/any-json-request') + .expect(200) + .expect({ + method: 'ANY' + }, function() { + request(app) + .post('/api/users/1/any-json-request') + .expect(200) + .expect({ + method: 'ANY' + }, done()); + }); + }); + + it('wildcard js methods should work on any given method', function (done) { + request(app) + .get('/api/users/1/any-js-request') + .expect(200) + .expect({ + anyMethod: 'GET' + }, function() { + request(app) + .post('/api/users/1/any-js-request') + .expect(200) + .expect({ + anyMethod: 'POST' + }, done()); + }); + }); }); diff --git a/test/mocks/users/1/any-js-request/ANY.js b/test/mocks/users/1/any-js-request/ANY.js new file mode 100644 index 0000000..667f3dd --- /dev/null +++ b/test/mocks/users/1/any-js-request/ANY.js @@ -0,0 +1,5 @@ +module.exports = function (req, res) { + res.json({ + anyMethod: req.method + }); +} \ No newline at end of file diff --git a/test/mocks/users/1/any-json-request/ANY.json b/test/mocks/users/1/any-json-request/ANY.json new file mode 100644 index 0000000..c9c918c --- /dev/null +++ b/test/mocks/users/1/any-json-request/ANY.json @@ -0,0 +1,3 @@ +{ + "method": "ANY" +} \ No newline at end of file