Skip to content

Commit

Permalink
Merge pull request #509 from wheresrhys/partial-body-matching
Browse files Browse the repository at this point in the history
implement partial body matching
  • Loading branch information
wheresrhys committed Mar 3, 2020
2 parents 0a21071 + 6a8f3ea commit db7ecbd
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 16 deletions.
4 changes: 4 additions & 0 deletions docs/_api-mocking/mock_matcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ parameters:
examples:
- |-
{ "key1": "value1", "key2": "value2" }
- name: matchPartialBody
types:
- Boolean
content: Match calls that only partially match a specified body json. See [global configuration](#usageconfiguration) for details.
- name: query
types:
- Object
Expand Down
4 changes: 4 additions & 0 deletions docs/_usage/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ parameters:
- `undefined`: An error will be thrown
- `true`: Overwrites the existing route
- `false`: Appends the new route to the list of routes
- name: matchPartialBody
types:
- Boolean
content: Match calls that only partially match a specified body json. Uses the [is-subset](https://www.npmjs.com/package/is-subset) library under the hood, which implements behaviour the same as jest's [.objectContainig()](https://jestjs.io/docs/en/expect#expectobjectcontainingobject) method.
- name: warnOnFallback
default: true
types:
Expand Down
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"core-js": "^3.0.0",
"debug": "^4.1.1",
"glob-to-regexp": "^0.4.0",
"is-subset": "^0.1.1",
"lodash.isequal": "^4.5.0",
"path-to-regexp": "^2.2.1",
"querystring": "^0.2.0",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/compile-route.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ const compileRoute = function(args) {
debug('Compiling route');
const route = sanitizeRoute(argsToRoute(args));
validateRoute(route);
route.matcher = generateMatcher(route);
route.matcher = generateMatcher(route, this);
limit(route);
delayResponse(route);
return route;
Expand Down
6 changes: 3 additions & 3 deletions src/lib/fetch-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ FetchMock.fetchHandler.isMock = true;
FetchMock.executeRouter = function(url, options, request) {
const debug = getDebug('executeRouter()');
debug(`Attempting to match request to a route`);
if (this.config.fallbackToNetwork === 'always') {
if (this.getOption('fallbackToNetwork') === 'always') {
debug(
' Configured with fallbackToNetwork=always - passing through to fetch'
);
Expand All @@ -152,7 +152,7 @@ FetchMock.executeRouter = function(url, options, request) {
return match;
}

if (this.config.warnOnFallback) {
if (this.getOption('warnOnFallback')) {
console.warn(`Unmatched ${(options && options.method) || 'GET'} to ${url}`); // eslint-disable-line
}

Expand All @@ -163,7 +163,7 @@ FetchMock.executeRouter = function(url, options, request) {
return { response: this.fallbackResponse };
}

if (!this.config.fallbackToNetwork) {
if (!this.getOption('fallbackToNetwork')) {
throw new Error(
`fetch-mock: No fallback response defined for ${(options &&
options.method) ||
Expand Down
20 changes: 16 additions & 4 deletions src/lib/generate-matcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { debug, setDebugNamespace } = require('./debug');
const glob = require('glob-to-regexp');
const pathToRegexp = require('path-to-regexp');
const querystring = require('querystring');
const isSubset = require('is-subset');
const {
headers: headerUtils,
getPath,
Expand Down Expand Up @@ -115,7 +116,10 @@ const getParamsMatcher = ({ params: expectedParams, url: matcherUrl }) => {
};
};

const getBodyMatcher = ({ body: expectedBody }) => {
const getBodyMatcher = (route, fetchMock) => {
const matchPartialBody = fetchMock.getOption('matchPartialBody', route);
const { body: expectedBody } = route;

debug('Generating body matcher');
return (url, { body, method = 'get' }) => {
debug('Attempting to match body');
Expand All @@ -135,8 +139,16 @@ const getBodyMatcher = ({ body: expectedBody }) => {
}
debug('Expected body:', expectedBody);
debug('Actual body:', sentBody);
if (matchPartialBody) {
debug('matchPartialBody is true - checking for partial match only');
}

return sentBody && isEqual(sentBody, expectedBody);
return (
sentBody &&
(matchPartialBody
? isSubset(sentBody, expectedBody)
: isEqual(sentBody, expectedBody))
);
};
};

Expand Down Expand Up @@ -202,15 +214,15 @@ const getUrlMatcher = route => {
return getFullUrlMatcher(route, matcherUrl, query);
};

module.exports = route => {
module.exports = (route, fetchMock) => {
setDebugNamespace('generateMatcher()');
debug('Compiling matcher for route');
const matchers = [
route.query && getQueryStringMatcher(route),
route.method && getMethodMatcher(route),
route.headers && getHeaderMatcher(route),
route.params && getParamsMatcher(route),
route.body && getBodyMatcher(route),
route.body && getBodyMatcher(route, fetchMock),
route.functionMatcher && getFunctionMatcher(route),
route.url && getUrlMatcher(route)
].filter(matcher => !!matcher);
Expand Down
4 changes: 4 additions & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,8 @@ FetchMock.sandbox = function() {
return sandbox;
};

FetchMock.getOption = function(name, route = {}) {
return name in route ? route[name] : this.config[name];
};

module.exports = FetchMock;
2 changes: 1 addition & 1 deletion src/lib/response-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ e.g. {"body": {"status: "registered"}}`);
}

getOption(name) {
return name in this.route ? this.route[name] : this.fetchMock.config[name];
return this.fetchMock.getOption(name, this.route);
}

convertToJson() {
Expand Down
9 changes: 2 additions & 7 deletions src/lib/set-up-and-tear-down.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,12 @@ FetchMock.addRoute = function(uncompiledRoute) {
(!method || !route.method || method === route.method)
);

const overwriteRoutes =
'overwriteRoutes' in route
? route.overwriteRoutes
: this.config.overwriteRoutes;

if (overwriteRoutes === false || !clashes.length) {
if (this.getOption('overwriteRoutes', route) === false || !clashes.length) {
this._uncompiledRoutes.push(uncompiledRoute);
return this.routes.push(route);
}

if (overwriteRoutes === true) {
if (this.getOption('overwriteRoutes', route) === true) {
clashes.forEach(clash => {
const index = this.routes.indexOf(clash);
this._uncompiledRoutes.splice(index, 1, uncompiledRoute);
Expand Down
11 changes: 11 additions & 0 deletions test/specs/matcher-object.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,16 @@ module.exports = fetchMock => {
const res = await fm.fetchHandler('http://it.at.there/');
expect(res.status).to.equal(300);
});

it('support setting matchPartialBody on matcher parameter', async () => {
fm.mock({ body: { ham: 'sandwich' }, matchPartialBody: true }, 200).catch(
404
);
const res = await fm.fetchHandler('http://it.at.there', {
method: 'POST',
body: JSON.stringify({ ham: 'sandwich', egg: 'mayonaise' })
});
expect(res.status).to.equal(200);
});
});
};
49 changes: 49 additions & 0 deletions test/specs/options.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,55 @@ module.exports = (fetchMock, theGlobal, fetch) => {
});
});

describe('matchPartialBody', () => {
it("don't match partial bodies by default", async () => {
fm.mock({ body: { ham: 'sandwich' } }, 200).catch(404);
const res = await fm.fetchHandler('http://it.at.there', {
method: 'POST',
body: JSON.stringify({ ham: 'sandwich', egg: 'mayonaise' })
});
expect(res.status).to.equal(404);
});

it('match partial bodies when configured true', async () => {
fm.config.matchPartialBody = true;
fm.mock({ body: { ham: 'sandwich' } }, 200).catch(404);
const res = await fm.fetchHandler('http://it.at.there', {
method: 'POST',
body: JSON.stringify({ ham: 'sandwich', egg: 'mayonaise' })
});
expect(res.status).to.equal(200);
fm.config.matchPartialBody = false;
});

it('local setting can override to false', async () => {
fm.config.matchPartialBody = true;
fm.mock(
{ body: { ham: 'sandwich' }, matchPartialBody: false },
200
).catch(404);
const res = await fm.fetchHandler('http://it.at.there', {
method: 'POST',
body: JSON.stringify({ ham: 'sandwich', egg: 'mayonaise' })
});
expect(res.status).to.equal(404);
fm.config.matchPartialBody = false;
});

it('local setting can override to true', async () => {
fm.config.matchPartialBody = false;
fm.mock(
{ body: { ham: 'sandwich' }, matchPartialBody: true },
200
).catch(404);
const res = await fm.fetchHandler('http://it.at.there', {
method: 'POST',
body: JSON.stringify({ ham: 'sandwich', egg: 'mayonaise' })
});
expect(res.status).to.equal(200);
});
});

describe.skip('warnOnFallback', () => {
it('warn on fallback response by default', async () => {});
it("don't warn on fallback response when configured false", async () => {});
Expand Down
64 changes: 64 additions & 0 deletions test/specs/routing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,70 @@ module.exports = fetchMock => {
await fm.fetchHandler('http://it.at.there/');
expect(fm.calls(true).length).to.equal(1);
});

describe('partial body matching', () => {
it('match when missing properties', async () => {
fm.mock(
{ body: { ham: 'sandwich' }, matchPartialBody: true },
200
).catch(404);
const res = await fm.fetchHandler('http://it.at.there', {
method: 'POST',
body: JSON.stringify({ ham: 'sandwich', egg: 'mayonaise' })
});
expect(res.status).to.equal(200);
});

it('match when missing nested properties', async () => {
fm.mock(
{ body: { meal: { ham: 'sandwich' } }, matchPartialBody: true },
200
).catch(404);
const res = await fm.fetchHandler('http://it.at.there', {
method: 'POST',
body: JSON.stringify({
meal: { ham: 'sandwich', egg: 'mayonaise' }
})
});
expect(res.status).to.equal(200);
});

it('not match when properties at wrong indentation', async () => {
fm.mock(
{ body: { ham: 'sandwich' }, matchPartialBody: true },
200
).catch(404);
const res = await fm.fetchHandler('http://it.at.there', {
method: 'POST',
body: JSON.stringify({ meal: { ham: 'sandwich' } })
});
expect(res.status).to.equal(404);
});

it('match when starting subset of array', async () => {
fm.mock(
{ body: { ham: [1, 2] }, matchPartialBody: true },
200
).catch(404);
const res = await fm.fetchHandler('http://it.at.there', {
method: 'POST',
body: JSON.stringify({ ham: [1, 2, 3] })
});
expect(res.status).to.equal(200);
});

it('not match when not starting subset of array', async () => {
fm.mock(
{ body: { ham: [1, 3] }, matchPartialBody: true },
200
).catch(404);
const res = await fm.fetchHandler('http://it.at.there', {
method: 'POST',
body: JSON.stringify({ ham: [1, 2, 3] })
});
expect(res.status).to.equal(404);
});
});
});
});

Expand Down

0 comments on commit db7ecbd

Please sign in to comment.