Skip to content

Commit

Permalink
Merge pull request #557 from wheresrhys/fix/req-with-query-not-matched
Browse files Browse the repository at this point in the history
Fix/req with query not matched
  • Loading branch information
wheresrhys committed May 13, 2020
2 parents 3d433be + 969fa0d commit 7a827f1
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 73 deletions.
12 changes: 9 additions & 3 deletions docs/_api-mocking/mock_matcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,18 @@ parameters:
types:
- Object
content: |-
Match only requests that have these query parameters set (in any order). In order to match keys which are used multiple times use an array of strings.
Match only requests that have these query parameters set (in any order). Query parameters are matched by using Node.js [querystring](https://nodejs.org/api/querystring.html) module. In summary the bahaviour is as follows
- strings, numbers and booleans are coerced to strings
- arrays of values are coerced to repetitions of the key
- all other values, including `undefined`, are coerced to an empty string
The request will be matched whichever order keys appear in the query string
examples:
- |-
{"q": "cute+kittenz", "format": "gif"}
{"q": "cute+kittenz"} // matches '?q=cute kittenz' or ?q=cute+kittenz'
- |-
{"tags": ["cute", "kittenz"]}
{"tags": ["cute", "kittenz"]} // matches `?q=cute&q=kittenz`
- |-
{"q": undefined, inform: true} // matches `?q=&inform=true`
- name: params
versionAdded: 6.0.0
types:
Expand Down
20 changes: 12 additions & 8 deletions src/lib/matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,25 +72,29 @@ const getMethodMatcher = ({ method: expectedMethod }) => {
};
};

const getQueryStringMatcher = ({ query: expectedQuery }) => {
const getQueryStringMatcher = ({ query: passedQuery }) => {
debug('Generating query parameters matcher');
if (!expectedQuery) {
if (!passedQuery) {
debug(' No query parameters expectations defined - skipping');
return;
}
debug(' Expected query parameters:', expectedQuery);
const expectedQuery = querystring.parse(querystring.stringify(passedQuery));
debug(' Expected query parameters:', passedQuery);
const keys = Object.keys(expectedQuery);
return (url) => {
debug('Attempting to match query parameters');
const query = querystring.parse(getQuery(url));
debug(' Expected query parameters:', expectedQuery);
debug(' Actual query parameters:', query);
return keys.every((key) => {
const value = Array.isArray(query[key]) ? query[key] : [query[key]];
const expectedValue = Array.isArray(expectedQuery[key])
? expectedQuery[key]
: [expectedQuery[key]];
return isEqual(value.sort(), expectedValue.sort());
if (Array.isArray(query[key])) {
if (!Array.isArray(expectedQuery[key])) {
return false;
} else {
return isEqual(query[key].sort(), expectedQuery[key].sort());
}
}
return query[key] === expectedQuery[key];
});
};
};
Expand Down
288 changes: 227 additions & 61 deletions test/specs/routing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,28 +374,29 @@ module.exports = (fetchMock) => {
});

describe('query strings', () => {
it('can match a query string', async () => {
it('match a query string', async () => {
fm.mock('http://it.at.there/', 200, {
query: { a: 'b' },
query: { a: 'b', c: 'd' },
}).catch();

await fm.fetchHandler('http://it.at.there');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://it.at.there?a=b');
await fm.fetchHandler('http://it.at.there?a=b&c=d');
expect(fm.calls(true).length).to.equal(1);
});

it('match a query string against an URL object', async () => {
it('match a query string against a URL object', async () => {
fm.mock('http://it.at.there/path', 200, {
query: { a: 'b' },
query: { a: 'b', c: 'd' },
}).catch();
const url = new URL.URL('http://it.at.there/path');
url.searchParams.append('a', 'b');
url.searchParams.append('c', 'd');
await fm.fetchHandler(url);
expect(fm.calls(true).length).to.equal(1);
});

it('match a query string against relative path', async () => {
it('match a query string against a relative path', async () => {
fm.mock('/path', 200, {
query: { a: 'b' },
}).catch();
Expand All @@ -404,20 +405,7 @@ module.exports = (fetchMock) => {
expect(fm.calls(true).length).to.equal(1);
});

it('match a query string against multiple similar relative path', async () => {
expect(() =>
fm
.mock('/it-at-there', 200, { query: { a: 'b', c: 'e' } })
.mock('/it-at-there', 300, {
overwriteRoutes: false,
query: { a: 'b', c: 'd' },
})
).not.to.throw();
const res = await fm.fetchHandler('/it-at-there?a=b&c=d');
expect(res.status).to.equal(300);
});

it('can match multiple query strings', async () => {
it('match multiple query strings', async () => {
fm.mock('http://it.at.there/', 200, {
query: { a: 'b', c: 'd' },
}).catch();
Expand All @@ -432,60 +420,238 @@ module.exports = (fetchMock) => {
expect(fm.calls(true).length).to.equal(2);
});

it('can match repeated query strings', async () => {
fm.mock(
{ url: 'http://it.at.there/', query: { a: ['b', 'c'] } },
200
).catch();
it('match an empty query string', async () => {
fm.mock('http://it.at.there/', 200, {
query: { a: '' },
}).catch();

await fm.fetchHandler('http://it.at.there');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://it.at.there?a=b');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://it.at.there?a=b&a=c');
await fm.fetchHandler('http://it.at.there?a=');
expect(fm.calls(true).length).to.equal(1);
await fm.fetchHandler('http://it.at.there?a=c&a=b');
expect(fm.calls(true).length).to.equal(2);
await fm.fetchHandler('http://it.at.there?a=b&a=c&a=d');
expect(fm.calls(true).length).to.equal(2);
});

it('can match a query string array of length 1', async () => {
fm.mock(
{ url: 'http://it.at.there/', query: { a: ['b'] } },
200
).catch();
it('distinguish between query strings that only partially differ', async () => {
expect(() =>
fm
.mock('/it-at-there', 200, { query: { a: 'b', c: 'e' } })
.mock('/it-at-there', 300, {
overwriteRoutes: false,
query: { a: 'b', c: 'd' },
})
).not.to.throw();
const res = await fm.fetchHandler('/it-at-there?a=b&c=d');
expect(res.status).to.equal(300);
});

await fm.fetchHandler('http://it.at.there');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://it.at.there?a=b');
expect(fm.calls(true).length).to.equal(1);
await fm.fetchHandler('http://it.at.there?a=b&a=c');
expect(fm.calls(true).length).to.equal(1);
describe('value coercion', () => {
it('coerce integers to strings and match', async () => {
fm.mock(
{
query: {
a: 1,
},
},
200
).catch();
await fm.fetchHandler('/path');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('/path?a=1');
expect(fm.calls(true).length).to.equal(1);
});

it('coerce floats to strings and match', async () => {
fm.mock(
{
query: {
a: 1.2,
},
},
200
).catch();
await fm.fetchHandler('/path');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('/path?a=1.2');
expect(fm.calls(true).length).to.equal(1);
});

it('coerce booleans to strings and match', async () => {
fm.mock(
{
query: {
a: true,
},
},
200
)
.mock(
{
query: {
b: false,
},
overwriteRoutes: false,
},
200
)
.catch();
await fm.fetchHandler('/path');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('/path?a=true');
expect(fm.calls(true).length).to.equal(1);
await fm.fetchHandler('/path?b=false');
expect(fm.calls(true).length).to.equal(2);
});

it('coerce undefined to an empty string and match', async () => {
fm.mock(
{
query: {
a: undefined,
},
},
200
).catch();
await fm.fetchHandler('/path');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('/path?a=');
expect(fm.calls(true).length).to.equal(1);
});

it('coerce null to an empty string and match', async () => {
fm.mock(
{
query: {
a: null,
},
},
200
).catch();
await fm.fetchHandler('/path');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('/path?a=');
expect(fm.calls(true).length).to.equal(1);
});

it('coerce an object to an empty string and match', async () => {
fm.mock(
{
query: {
a: { b: 'c' },
},
},
200
).catch();
await fm.fetchHandler('/path');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('/path?a=');
expect(fm.calls(true).length).to.equal(1);
});

it('can match a query string with different value types', async () => {
const query = {
t: true,
f: false,
u: undefined,
num: 1,
arr: ['a', undefined],
};
fm.mock('http://it.at.there/', 200, {
query,
}).catch();

await fm.fetchHandler('http://it.at.there');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler(
'http://it.at.there?t=true&f=false&u=&num=1&arr=a&arr='
);
expect(fm.calls(true).length).to.equal(1);
});
});

it('can be used alongside existing query strings', async () => {
fm.mock('http://it.at.there/?c=d', 200, {
query: { a: 'b' },
}).catch();
describe('repeated query strings', () => {
it('match repeated query strings', async () => {
fm.mock(
{ url: 'http://it.at.there/', query: { a: ['b', 'c'] } },
200
).catch();

await fm.fetchHandler('http://it.at.there');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://it.at.there?a=b');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://it.at.there?a=b&a=c');
expect(fm.calls(true).length).to.equal(1);
await fm.fetchHandler('http://it.at.there?a=b&a=c&a=d');
expect(fm.calls(true).length).to.equal(1);
});

await fm.fetchHandler('http://it.at.there?c=d');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://it.at.there?c=d&a=b');
expect(fm.calls(true).length).to.equal(1);
await fm.fetchHandler('http://it.at.there?a=b&c=d');
expect(fm.calls(true).length).to.equal(1);
it('match repeated query strings in any order', async () => {
fm.mock(
{ url: 'http://it.at.there/', query: { a: ['b', 'c'] } },
200
).catch();

await fm.fetchHandler('http://it.at.there');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://it.at.there?a=b&a=c');
expect(fm.calls(true).length).to.equal(1);
await fm.fetchHandler('http://it.at.there?a=c&a=b');
expect(fm.calls(true).length).to.equal(2);
});

it('match a query string array of length 1', async () => {
fm.mock(
{ url: 'http://it.at.there/', query: { a: ['b'] } },
200
).catch();

await fm.fetchHandler('http://it.at.there');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://it.at.there?a=b');
expect(fm.calls(true).length).to.equal(1);
await fm.fetchHandler('http://it.at.there?a=b&a=c');
expect(fm.calls(true).length).to.equal(1);
});

it('match a repeated query string with an empty value', async () => {
fm.mock(
{ url: 'http://it.at.there/', query: { a: ['b', undefined] } },
200
).catch();

await fm.fetchHandler('http://it.at.there');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://it.at.there?a=b');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://it.at.there?a=b&a=');
expect(fm.calls(true).length).to.equal(1);
});
});

it('can be used alongside function matchers', async () => {
fm.mock((url) => /person/.test(url), 200, {
query: { a: 'b' },
}).catch();
describe('interoperability', () => {
it('can be used alongside query strings expressed in the url', async () => {
fm.mock('http://it.at.there/?c=d', 200, {
query: { a: 'b' },
}).catch();

await fm.fetchHandler('http://it.at.there?c=d');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://it.at.there?c=d&a=b');
expect(fm.calls(true).length).to.equal(1);
await fm.fetchHandler('http://it.at.there?a=b&c=d');
expect(fm.calls(true).length).to.equal(1);
});

await fm.fetchHandler('http://domain.com/person');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://domain.com/person?a=b');
expect(fm.calls(true).length).to.equal(1);
it('can be used alongside function matchers', async () => {
fm.mock((url) => /person/.test(url), 200, {
query: { a: 'b' },
}).catch();

await fm.fetchHandler('http://domain.com/person');
expect(fm.calls(true).length).to.equal(0);
await fm.fetchHandler('http://domain.com/person?a=b');
expect(fm.calls(true).length).to.equal(1);
});
});
});

Expand Down

0 comments on commit 7a827f1

Please sign in to comment.