Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing wildcard matcher #25

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"
}