Skip to content

Commit

Permalink
Merge pull request #25 from daanoz/feature/fix-nested-wildcard-matches
Browse files Browse the repository at this point in the history
Fixing wildcard matcher
  • Loading branch information
muratcorlu committed Sep 27, 2018
2 parents 455a465 + 8511362 commit c8d0ac4
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 72 deletions.
173 changes: 112 additions & 61 deletions api-mocker.js
Expand Up @@ -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)
);
}

Expand All @@ -50,6 +50,84 @@ function logger(params) {
}
}


/**
* Locate all possible options to match a file request, select the first one (most relevant)
* @param {string} requestPath Requst path to API mock file.
* @param {string[]} requestMethodFiles A list of files that match the request
*/
function findMatchingPath(requestPath, requestMethodFiles) {
var pathParts = requestPath.split('/');
var pathOptions = recurseLookup([pathParts.shift()], pathParts, []);

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
* @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(function (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(path.join(parentPath, testPath))) {
pathOptions.push({
path: path.join(parentPath, testPath),
params: existingParams.concat([])
});
}
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
};
}).forEach(function (wildcardFolder) {
var pathOption = {
path: path.join(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
Expand Down Expand Up @@ -110,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;
}
Expand All @@ -131,67 +207,42 @@ 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);

res.setHeader('Content-Type', 'application/' + fileType);
logger({ req: req, filePath: filePath, fileType: fileType, config: config })
var buf = fs.readFileSync(filePath);

return res.end(buf);
res.setHeader('Content-Type', 'application/' + fileType);

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

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 methodFiles = [jsMockFile, staticMockFile, wildcardJsMockFile, wildcardStaticMockFile];

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 matchedMethodFile = methodFiles.find(function (methodFile) {
if (fs.existsSync(path.join(targetFullPath, methodFile))) {
return true;
}
return false;
});

if (matchedMethodFile) {
return returnForPath(path.resolve(path.join(targetFullPath, matchedMethodFile)));
} else {
var newTarget = findMatchingPath(targetPath, methodFiles);
if (newTarget) {
return returnForPath(newTarget, requestParams);
var requestParams = {};
newTarget.params.forEach(function (param) {
requestParams[param.key] = param.value;
});
return returnForPath(newTarget.path, requestParams);
} else {
return returnNotFound();
}
Expand Down
64 changes: 53 additions & 11 deletions test/api-mocker.spec.js
Expand Up @@ -63,17 +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('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'));
Expand Down Expand Up @@ -149,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());
});
});
});


Expand Down
5 changes: 5 additions & 0 deletions test/mocks/users/1/any-js-request/ANY.js
@@ -0,0 +1,5 @@
module.exports = function (req, res) {
res.json({
anyMethod: req.method
});
}
3 changes: 3 additions & 0 deletions test/mocks/users/1/any-json-request/ANY.json
@@ -0,0 +1,3 @@
{
"method": "ANY"
}
3 changes: 3 additions & 0 deletions test/mocks/users/__user_id__/nested/GET.json
@@ -0,0 +1,3 @@
{
"result": "WILDCARD_NESTED"
}

0 comments on commit c8d0ac4

Please sign in to comment.