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

Allow using route params #205

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/handle_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 41 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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];
Expand All @@ -157,13 +159,46 @@ 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);
}
}
}

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;
37 changes: 36 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
162 changes: 162 additions & 0 deletions test/route_params.spec.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});