diff --git a/README.md b/README.md index 58f7e36..766f581 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,48 @@ const url = new RegExp(`${usersUri}/*`); mock.onGet(url).reply(200, users); ``` +Using route params (colon notation) + +```js +const routeParams = { + ':userId': '[0-9]{1,8}', + ':filter': 'active|inactive|all', +} +const mock = new MockAdapter(axios, {}, routeParams); + +mock.onGet('/users/:userId/posts/:filter').reply(function(config) { + const { userId, filter } = config.routeParams; + + // userId === '123' + // filter === 'active' + + return [200, {}]; +}); + +axios.get('/users/123/posts/active'); +``` + +Using route params (curly braces notation) + +```js +const routeParams = { + '{uuid}': '[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}', + '{page}': '\\d?', +} +const mock = new MockAdapter(axios, {}, routeParams); + +mock.onGet('/users/{uuid}/posts/{page}').reply(function(config) { + const { uuid, page } = config.routeParams; + + // uuid === 'b67c0749-656c-4beb-9cd9-17e274a648d9' + // page === '3' + + return [200, {}]; +}); + +axios.get('/users/b67c0749-656c-4beb-9cd9-17e274a648d9/posts/3'); +``` + Specify no path to match by verb alone diff --git a/src/handle_request.js b/src/handle_request.js index 937a9d0..841f4f0 100644 --- a/src/handle_request.js +++ b/src/handle_request.js @@ -31,9 +31,10 @@ function handleRequest(mockAdapter, resolve, reject, config) { ); if (handler) { - if (handler.length === 7) { + if (handler.length === 8) { utils.purgeIfReplyOnce(mockAdapter, handler); } + config.routeParams = utils.getRouteParams(mockAdapter.knownRouteParams, handler[6], config); if (handler.length === 2) { // passThrough handler diff --git a/src/index.js b/src/index.js index d55517b..d6cfbc2 100644 --- a/src/index.js +++ b/src/index.js @@ -41,13 +41,14 @@ function resetHistory() { this.history = getVerbObject(); } -function MockAdapter(axiosInstance, options) { +function MockAdapter(axiosInstance, options, knownRouteParams) { reset.call(this); if (axiosInstance) { this.axiosInstance = axiosInstance; this.originalAdapter = axiosInstance.defaults.adapter; this.delayResponse = options && options.delayResponse > 0 ? options.delayResponse : null; + this.knownRouteParams = getValidRouteParams(knownRouteParams); axiosInstance.defaults.adapter = this.adapter.call(this); } } @@ -68,16 +69,17 @@ VERBS.concat('any').forEach(function(method) { var methodName = 'on' + method.charAt(0).toUpperCase() + method.slice(1); MockAdapter.prototype[methodName] = function(matcher, body, requestHeaders) { var _this = this; - var matcher = matcher === undefined ? /.*/ : matcher; + var originalMatcher = matcher; + matcher = getMatcher(matcher, _this.knownRouteParams); function reply(code, response, headers) { - var handler = [matcher, body, requestHeaders, code, response, headers]; + var handler = [matcher, body, requestHeaders, code, response, headers, originalMatcher]; addHandler(method, _this.handlers, handler); return _this; } function replyOnce(code, response, headers) { - var handler = [matcher, body, requestHeaders, code, response, headers, true]; + var handler = [matcher, body, requestHeaders, code, response, headers, originalMatcher, true]; addHandler(method, _this.handlers, handler); return _this; } @@ -134,7 +136,7 @@ function findInHandlers(method, handlers, handler) { var index = -1; for (var i = 0; i < handlers[method].length; i += 1) { var item = handlers[method][i]; - var isReplyOnce = item.length === 7; + var isReplyOnce = item.length === 8; var comparePaths = item[0] instanceof RegExp && handler[0] instanceof RegExp ? String(item[0]) === String(handler[0]) : item[0] === handler[0]; @@ -157,7 +159,7 @@ function addHandler(method, handlers, handler) { }); } else { var indexOfExistingHandler = findInHandlers(method, handlers, handler); - if (indexOfExistingHandler > -1 && handler.length < 7) { + if (indexOfExistingHandler > -1 && handler.length < 8) { handlers[method].splice(indexOfExistingHandler, 1, handler); } else { handlers[method].push(handler); @@ -165,5 +167,38 @@ function addHandler(method, handlers, handler) { } } +function getValidRouteParams(knownRouteParams) { + if (typeof knownRouteParams !== 'object') { + return null; + } + + var valid = {}; + var hasValidParams = false; + + Object.keys(knownRouteParams).forEach(function(param) { + if (/^:(.+)|{(.+)}$/.test(param)) { + valid[param] = knownRouteParams[param]; + hasValidParams = true; + } + }) + + return hasValidParams ? valid : null; +} + +function getMatcher(matcher, knownRouteParams) { + if (matcher === undefined) { + return /.*/; + } + + if (typeof matcher === 'string' && knownRouteParams !== null) { + Object.keys(knownRouteParams).forEach(function(param) { + matcher = matcher.replace(param, '(' + knownRouteParams[param] + ')') + }) + return new RegExp('^' + matcher + '$') + } + + return matcher; +} + module.exports = MockAdapter; module.exports.default = MockAdapter; diff --git a/src/utils.js b/src/utils.js index 5420cc1..bbc56d8 100644 --- a/src/utils.js +++ b/src/utils.js @@ -124,10 +124,45 @@ function isSimpleObject(value) { return value !== null && value !== undefined && value.toString() === '[object Object]'; } +function getRouteParams(knownRouteParams, route, config) { + var routeParams = {} + + if (knownRouteParams == null || typeof route !== 'string') { + return routeParams; + } + + var paramsUsedInRoute = route.split('/').filter(function (param) { + return knownRouteParams[param] !== undefined; + }); + if (paramsUsedInRoute.length == 0) { + return routeParams; + } + + paramsUsedInRoute.forEach(function(param) { + route = route.replace(param, '(' + knownRouteParams[param] + ')'); + }); + + var actualUrl = config.baseURL ? config.url.slice(config.baseURL.length) : config.url; + var routeMatches = actualUrl.match(new RegExp('^' + route + '$')); + + paramsUsedInRoute.forEach(function(param, index) { + var paramNameMatches = param.match(/^:(.+)|{(.+)}$/) || []; + var paramName = paramNameMatches[1] || paramNameMatches[2]; + if (paramName === undefined) { + return; + } + + routeParams[paramName] = routeMatches[index+1]; + }) + + return routeParams; +} + module.exports = { find: find, findHandler: findHandler, isSimpleObject: isSimpleObject, purgeIfReplyOnce: purgeIfReplyOnce, - settle: settle + settle: settle, + getRouteParams: getRouteParams, }; diff --git a/test/route_params.spec.js b/test/route_params.spec.js new file mode 100644 index 0000000..2e229ac --- /dev/null +++ b/test/route_params.spec.js @@ -0,0 +1,162 @@ +var axios = require('axios'); +var expect = require('chai').expect; + +var MockAdapter = require('../src'); + +describe('MockAdapter route params', function() { + var instance; + var mock; + + it('matches route with params', function() { + var routeParams = { + ':userUuid': '[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}', + ':filter': '((active)|inactive|all)', + } + + instance = axios.create(); + mock = new MockAdapter(instance, {}, routeParams); + + expect(mock.knownRouteParams).to.deep.equal(routeParams); + + mock.onGet('/users/:userUuid/posts/:filter').reply(200, 'body'); + + return instance.get('/users/b67c0749-656c-4beb-9cd9-17e274a648d9/posts/active').then(function(response) { + expect(response.status).to.equal(200); + }); + }); + + it('rejects route when params regex does not match', function() { + var routeParams = { + ':userUuid': '[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}', + ':filter': 'active|inactive|all', + } + + instance = axios.create(); + mock = new MockAdapter(instance, {}, routeParams); + + expect(mock.knownRouteParams).to.deep.equal(routeParams); + + mock.onGet('/users/:userUuid/posts/:filter').reply(200, 'body'); + + return instance.get('/users/all/posts/recent').catch(function(error) { + expect(error.response.status).to.equal(404); + expect(error.response.config.routeParams).to.equal(undefined); + }); + }); + + it('matches route with params and makes params available on config', function() { + var routeParams = { + ':userUuid': '[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}', + ':filter': '.+', + } + + instance = axios.create(); + mock = new MockAdapter(instance, {}, routeParams); + + expect(mock.knownRouteParams).to.deep.equal(routeParams); + + mock.onGet('/users/:userUuid/posts/:filter').reply(function(config) { + expect(config.routeParams).to.deep.equal({ + 'userUuid': 'b67c0749-656c-4beb-9cd9-17e274a648d9', + 'filter': 'inactive' + }); + return [200, 'body']; + }); + + return instance.get('/users/b67c0749-656c-4beb-9cd9-17e274a648d9/posts/inactive').then(function(response) { + expect(response.status).to.equal(200); + }); + }); + + it('matches route with params when using baseURL', function() { + var routeParams = { + ':userId': '\\d+', + ':filter': 'active|inactive|all', + } + + instance = axios.create(); + instance.defaults.baseURL = 'http://www.example.org/api/v1'; + mock = new MockAdapter(instance, {}, routeParams); + + expect(mock.knownRouteParams).to.deep.equal(routeParams); + + mock.onGet('/users/:userId/posts/:filter').reply(function(config) { + expect(config.routeParams).to.deep.equal({ + 'userId': '123', + 'filter': 'inactive' + }); + return [200, 'body']; + }); + + return instance.get('/users/123/posts/inactive').then(function(response) { + expect(response.status).to.equal(200); + }); + }); + + it('matches route with params when using curly braces', function() { + var routeParams = { + '{userId}': '\\d+', + '{filter}': 'active|inactive|all', + } + + instance = axios.create(); + mock = new MockAdapter(instance, {}, routeParams); + + expect(mock.knownRouteParams).to.deep.equal(routeParams); + + mock.onGet('/users/{userId}/posts/{filter}/orderby:date:desc').reply(function(config) { + expect(config.routeParams).to.deep.equal({ + 'userId': '123', + 'filter': 'inactive' + }); + return [200, 'body']; + }); + + return instance.get('/users/123/posts/inactive/orderby:date:desc').then(function(response) { + expect(response.status).to.equal(200); + }); + }); + + it('does not match params when param keys are not using colons or curly braces notation', function() { + var routeParams = { + 'userId': '\\d+', + 'filter': 'active|inactive|all', + } + + instance = axios.create(); + mock = new MockAdapter(instance, {}, routeParams); + + expect(mock.knownRouteParams).to.deep.equal(null); + + mock.onGet('/users/userId/posts/filter').reply(function(config) { + expect(config.routeParams).to.deep.equal({}); + return [200, 'body']; + }); + + return instance.get('/users/123/posts/inactive').catch(function(error) { + expect(error.response.status).to.equal(404); + expect(error.response.config.routeParams).to.equal(undefined); + }); + }); + + it('does not use known route params when matcher is not a string', function() { + var routeParams = { + ':userId': '\\d+', + ':filter': 'active|inactive|all', + } + + instance = axios.create(); + mock = new MockAdapter(instance, {}, routeParams); + + expect(mock.knownRouteParams).to.deep.equal(routeParams); + + mock.onGet(/\/users\/\d+\/posts\/active|inactive|all/).reply(function(config) { + expect(config.routeParams).to.deep.equal({}); + return [200, 'body']; + }); + + return instance.get('/users/123/posts/inactive').then(function(response) { + expect(response.status).to.equal(200); + }); + }); +});