Skip to content

Commit

Permalink
Merge pull request #728 from wheresrhys/rhys/express-params
Browse files Browse the repository at this point in the history
Matchers expect normalizedRequest
  • Loading branch information
wheresrhys authored Jul 21, 2024
2 parents 98c3c37 + da9dfe8 commit f3b1423
Show file tree
Hide file tree
Showing 20 changed files with 482 additions and 319 deletions.
4 changes: 3 additions & 1 deletion docs/docs/@fetch-mock/core/more-routing-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ If your matcher requires access to the body of the request set this to true; bec

`{Function}`

A function which takes a route definition object as input, and returns a function of the signature `(url, options, request) => Boolean`. See the examples below for more detail. The function is passed the fetchMock instance as a second parameter in case you need to access any config.
A function which takes a route definition object as input, and returns a function of the signature `(NormalizedRequest) => Boolean`. See the examples below for more detail. The function is passed the fetchMock instance as a second parameter in case you need to access any config.

// TODO at time of writing the NormalizedRequest schema is still evolving, so more detailed docs will emerge soon

##### Examples

Expand Down
6 changes: 3 additions & 3 deletions docs/docs/@fetch-mock/core/route/response.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ A `Promise` that resolves to any of the options documented above e.g. `new Promi

`{Function}`

A function that returns any of the options documented above (including `Promise`. The function will be passed the `url` and `options` `fetch` was called with. If `fetch` was called with a `Request` instance, it will be passed `url` and `options` inferred from the `Request` instance, with the original `Request` passed as a third argument.
A function that is passed a [`CallLog`](/fetch-mock/docs/@fetch-mock/core/CallHistory#calllog-schema) and returns any of the options documented above (including `Promise`).

### Examples

- `(url, opts) => opts.headers.Authorization ? 200 : 403`
- `(_, _, request) => request.headers.get('Authorization') ? 200 : 403`
- `({url, options}) => options.headers.Authorization ? 200 : 403`
- `({request}) => request.headers.get('Authorization') ? 200 : 403`
7 changes: 1 addition & 6 deletions packages/core/src/CallHistory.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,7 @@ class CallHistory {
});

calls = calls.filter(({ url, options }) => {
const {
url: normalizedUrl,
options: normalizedOptions,
request,
} = normalizeRequest(url, options, this.config.Request);
return matcher(normalizedUrl, normalizedOptions, request);
return matcher(normalizeRequest(url, options, this.config.Request));
});

return calls;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/FetchMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const FetchMock = {
requestInit,
this.config.Request,
);

/** @type {Promise<any>[]} */
const pendingPromises = [];
const callLog = { ...normalizedRequest, pendingPromises };
Expand Down
39 changes: 24 additions & 15 deletions packages/core/src/Matchers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//@type-check
/** @typedef {import('./Route').RouteConfig} RouteConfig */
/** @typedef {import('./RequestUtils').NormalizedRequestOptions} NormalizedRequestOptions */
/** @typedef {import('./RequestUtils').NormalizedRequest} NormalizedRequest */
import glob from 'globrex';
import * as regexparam from 'regexparam';
import querystring from 'querystring';
Expand Down Expand Up @@ -32,7 +32,7 @@ export const isFunctionMatcher = (matcher) => typeof matcher === 'function';
/** @typedef {string | RegExp | URL} RouteMatcherUrl */
/** @typedef {function(string): boolean} UrlMatcher */
/** @typedef {function(string): UrlMatcher} UrlMatcherGenerator */
/** @typedef {function(string, NormalizedRequestOptions, Request): boolean} RouteMatcherFunction */
/** @typedef {function(NormalizedRequest): boolean} RouteMatcherFunction */
/** @typedef {function(RouteConfig): RouteMatcherFunction} MatcherGenerator */
/** @typedef {RouteMatcherUrl | RouteMatcherFunction} RouteMatcher */

Expand All @@ -47,19 +47,27 @@ export const isFunctionMatcher = (matcher) => typeof matcher === 'function';
* @type {Object.<string, UrlMatcherGenerator>}
*/
const stringMatchers = {
begin: (targetString) => (url) => url.indexOf(targetString) === 0,
end: (targetString) => (url) =>
url.substr(-targetString.length) === targetString,
begin:
(targetString) =>
({ url }) =>
url.indexOf(targetString) === 0,
end:
(targetString) =>
({ url }) =>
url.substr(-targetString.length) === targetString,

glob: (targetString) => {
const urlRX = glob(targetString);
return (url) => urlRX.regex.test(url);
return ({ url }) => urlRX.regex.test(url);
},
express: (targetString) => {
const urlRX = regexparam.parse(targetString);
return (url) => urlRX.pattern.test(getPath(url));
return ({ url }) => urlRX.pattern.test(getPath(url));
},
path: (targetString) => (url) => getPath(url) === targetString,
path:
(targetString) =>
({ url }) =>
getPath(url) === targetString,
};
/**
* @type {MatcherGenerator}
Expand All @@ -69,7 +77,7 @@ const getHeaderMatcher = ({ headers: expectedHeaders }) => {
return;
}
const expectation = normalizeHeaders(expectedHeaders);
return (url, { headers = {} }) => {
return ({ options: { headers = {} } }) => {
// TODO do something to handle multi value headers
const lowerCaseHeaders = normalizeHeaders(headers);
return Object.keys(expectation).every(
Expand All @@ -84,7 +92,7 @@ const getMethodMatcher = ({ method: expectedMethod }) => {
if (!expectedMethod) {
return;
}
return (url, { method }) => {
return ({ options: { method } = {} }) => {
const actualMethod = method ? method.toLowerCase() : 'get';
return expectedMethod === actualMethod;
};
Expand All @@ -98,7 +106,7 @@ const getQueryStringMatcher = ({ query: passedQuery }) => {
}
const expectedQuery = querystring.parse(querystring.stringify(passedQuery));
const keys = Object.keys(expectedQuery);
return (url) => {
return ({ url }) => {
const query = querystring.parse(getQuery(url));
return keys.every((key) => {
if (Array.isArray(query[key])) {
Expand Down Expand Up @@ -129,7 +137,7 @@ const getParamsMatcher = ({ params: expectedParams, url: matcherUrl }) => {
}
const expectedKeys = Object.keys(expectedParams);
const re = regexparam.parse(matcherUrl.replace(/^express:/, ''));
return (url) => {
return ({ url }) => {
const vals = re.pattern.exec(getPath(url)) || [];
vals.shift();
/** @type {Object.<string,string>} */
Expand All @@ -148,7 +156,8 @@ const getParamsMatcher = ({ params: expectedParams, url: matcherUrl }) => {
const getBodyMatcher = (route) => {
const { body: expectedBody } = route;

return (url, { body, method = 'get' }) => {
return ({ url, options: { body, method = 'get' } }) => {
console.log({ url, body, method });
if (method.toLowerCase() === 'get') {
// GET requests don’t send a body so the body matcher should be ignored for them
return true;
Expand Down Expand Up @@ -185,7 +194,7 @@ const getFullUrlMatcher = (route, matcherUrl, query) => {
route.url = expectedUrl;
}

return (url) => {
return ({ url }) => {
if (query && expectedUrl.indexOf('?')) {
return getPath(url) === getPath(expectedUrl);
}
Expand All @@ -208,7 +217,7 @@ const getUrlMatcher = (route) => {
}

if (matcherUrl instanceof RegExp) {
return (url) => matcherUrl.test(url);
return ({ url }) => matcherUrl.test(url);
}
if (matcherUrl instanceof URL) {
if (matcherUrl.href) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/RequestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const isRequest = (urlOrRequest, Request) =>
* @param {typeof Request} Request
* @returns {NormalizedRequest}
*/
export function normalizeRequest(urlOrRequest, options, Request) {
export function normalizeRequest(urlOrRequest, options = {}, Request) {
if (isRequest(urlOrRequest, Request)) {
/** @type {NormalizedRequestOptions} */
const derivedOptions = {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/Route.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ class Route {
}));
this.config.usesBody = activeMatchers.some(({ usesBody }) => usesBody);
/** @type {RouteMatcherFunction} */
this.matcher = (url, options = {}, request) =>
activeMatchers.every(({ matcher }) => matcher(url, options, request));
this.matcher = (normalizedRequest) =>
activeMatchers.every(({ matcher }) => matcher(normalizedRequest));
}
/**
* @returns {void}
Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/Router.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ function shouldSendAsObject(responseInput) {
* @returns
*/
const resolveUntilResponseConfig = async (response, normalizedRequest) => {
const { url, options, request } = normalizedRequest;
// We want to allow things like
// - function returning a Promise for a response
// - delaying (using a timeout Promise) a function's execution to generate
Expand All @@ -111,7 +110,7 @@ const resolveUntilResponseConfig = async (response, normalizedRequest) => {
//eslint-disable-next-line no-constant-condition
while (true) {
if (typeof response === 'function') {
response = response(url, options, request);
response = response(normalizedRequest);
} else if (isPromise(response)) {
response = await response; // eslint-disable-line no-await-in-loop
} else {
Expand Down Expand Up @@ -164,7 +163,7 @@ export default class Router {
}
normalizedRequest.signal.addEventListener('abort', abort);
}

console.log(this.routes);
if (this.needsToReadBody(request)) {
options.body = await options.body;
}
Expand All @@ -173,7 +172,7 @@ export default class Router {
? [...this.routes, this.fallbackRoute]
: this.routes;
const route = routesToTry.find((route) =>
route.matcher(url, options, request),
route.matcher(normalizedRequest),
);

if (route) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/__tests__/CallHistory.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ describe('CallHistory', () => {
});

describe('filtering with a matcher', () => {
//TODO write a test that just makes it clear this is contracted out to Route
// TODO write a test that just makes it clear this is contracted out to Route
// spy on route constructor, and then on matcher for that route
it('should be able to filter with a url matcher', async () => {
fm.catch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ describe('response generation', () => {
});

it('construct a response based on the request', async () => {
fm.route('*', (url, opts) => url + opts.headers.header);
fm.route('*', ({ url, options }) => url + options.headers.header);
const res = await fm.fetchHandler('http://a.com/', {
headers: { header: 'val' },
});
Expand All @@ -206,7 +206,7 @@ describe('response generation', () => {
});

it('construct a response based on a Request instance', async () => {
fm.route('*', (url, opts, request) => request.json().then(({ a }) => a));
fm.route('*', ({ request }) => request.json().then(({ a }) => a));
const res = await fm.fetchHandler(
new fm.config.Request('http://a.com', {
body: JSON.stringify({ a: 'b' }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('response negotiation', () => {
});

it('function', async () => {
fm.route('*', (url) => url);
fm.route('*', ({ url }) => url);
const res = await fm.fetchHandler('http://a.com/');
expect(res.status).toEqual(200);
expect(await res.text()).toEqual('http://a.com/');
Expand All @@ -27,7 +27,7 @@ describe('response negotiation', () => {
expect(res.status).toEqual(300);
});
it('function that returns a Promise for a body', async () => {
fm.route('*', (url) => Promise.resolve(`test: ${url}`));
fm.route('*', ({ url }) => Promise.resolve(`test: ${url}`));
const res = await fm.fetchHandler('http://a.com/');
expect(res.status).toEqual(200);
expect(await res.text()).toEqual('test: http://a.com/');
Expand All @@ -36,7 +36,7 @@ describe('response negotiation', () => {
it('Promise for a function that returns a response', async () => {
fm.route(
'http://a.com/',
Promise.resolve((url) => `test: ${url}`),
Promise.resolve(({ url }) => `test: ${url}`),
);
const res = await fm.fetchHandler('http://a.com/');
expect(res.status).toEqual(200);
Expand Down Expand Up @@ -79,7 +79,7 @@ describe('response negotiation', () => {
});

it('pass values to delayed function', async () => {
fm.route('*', (url) => `delayed: ${url}`, {
fm.route('*', ({ url }) => `delayed: ${url}`, {
delay: 10,
});
const req = fm.fetchHandler('http://a.com/');
Expand Down
Loading

0 comments on commit f3b1423

Please sign in to comment.