UNPKG

7.55 kBJavaScriptView Raw
1const { debug } = require('./debug');
2const glob = require('glob-to-regexp');
3const pathToRegexp = require('path-to-regexp');
4const querystring = require('querystring');
5const isSubset = require('is-subset');
6const {
7 headers: headerUtils,
8 getPath,
9 getQuery,
10 normalizeUrl,
11} = require('./request-utils');
12const isEqual = require('lodash.isequal');
13
14const debuggableUrlFunc = (func) => (url) => {
15 debug('Actual url:', url);
16 return func(url);
17};
18
19const stringMatchers = {
20 begin: (targetString) =>
21 debuggableUrlFunc((url) => url.indexOf(targetString) === 0),
22 end: (targetString) =>
23 debuggableUrlFunc(
24 (url) => url.substr(-targetString.length) === targetString
25 ),
26 glob: (targetString) => {
27 const urlRX = glob(targetString);
28 return debuggableUrlFunc((url) => urlRX.test(url));
29 },
30 express: (targetString) => {
31 const urlRX = pathToRegexp(targetString);
32 return debuggableUrlFunc((url) => urlRX.test(getPath(url)));
33 },
34 path: (targetString) =>
35 debuggableUrlFunc((url) => getPath(url) === targetString),
36};
37
38const getHeaderMatcher = ({ headers: expectedHeaders }) => {
39 debug('Generating header matcher');
40 if (!expectedHeaders) {
41 debug(' No header expectations defined - skipping');
42 return;
43 }
44 const expectation = headerUtils.toLowerCase(expectedHeaders);
45 debug(' Expected headers:', expectation);
46 return (url, { headers = {} }) => {
47 debug('Attempting to match headers');
48 const lowerCaseHeaders = headerUtils.toLowerCase(
49 headerUtils.normalize(headers)
50 );
51 debug(' Expected headers:', expectation);
52 debug(' Actual headers:', lowerCaseHeaders);
53 return Object.keys(expectation).every((headerName) =>
54 headerUtils.equal(lowerCaseHeaders[headerName], expectation[headerName])
55 );
56 };
57};
58
59const getMethodMatcher = ({ method: expectedMethod }) => {
60 debug('Generating method matcher');
61 if (!expectedMethod) {
62 debug(' No method expectations defined - skipping');
63 return;
64 }
65 debug(' Expected method:', expectedMethod);
66 return (url, { method }) => {
67 debug('Attempting to match method');
68 const actualMethod = method ? method.toLowerCase() : 'get';
69 debug(' Expected method:', expectedMethod);
70 debug(' Actual method:', actualMethod);
71 return expectedMethod === actualMethod;
72 };
73};
74
75const getQueryStringMatcher = ({ query: expectedQuery }) => {
76 debug('Generating query parameters matcher');
77 if (!expectedQuery) {
78 debug(' No query parameters expectations defined - skipping');
79 return;
80 }
81 debug(' Expected query parameters:', expectedQuery);
82 const keys = Object.keys(expectedQuery);
83 return (url) => {
84 debug('Attempting to match query parameters');
85 const query = querystring.parse(getQuery(url));
86 debug(' Expected query parameters:', expectedQuery);
87 debug(' Actual query parameters:', query);
88 return keys.every((key) => query[key] === expectedQuery[key]);
89 };
90};
91
92const getParamsMatcher = ({ params: expectedParams, url: matcherUrl }) => {
93 debug('Generating path parameters matcher');
94 if (!expectedParams) {
95 debug(' No path parameters expectations defined - skipping');
96 return;
97 }
98 if (!/express:/.test(matcherUrl)) {
99 throw new Error(
100 'fetch-mock: matching on params is only possible when using an express: matcher'
101 );
102 }
103 debug(' Expected path parameters:', expectedParams);
104 const expectedKeys = Object.keys(expectedParams);
105 const keys = [];
106 const re = pathToRegexp(matcherUrl.replace(/^express:/, ''), keys);
107 return (url) => {
108 debug('Attempting to match path parameters');
109 const vals = re.exec(getPath(url)) || [];
110 vals.shift();
111 const params = keys.reduce(
112 (map, { name }, i) =>
113 vals[i] ? Object.assign(map, { [name]: vals[i] }) : map,
114 {}
115 );
116 debug(' Expected path parameters:', expectedParams);
117 debug(' Actual path parameters:', params);
118 return expectedKeys.every((key) => params[key] === expectedParams[key]);
119 };
120};
121
122const getBodyMatcher = (route, fetchMock) => {
123 const matchPartialBody = fetchMock.getOption('matchPartialBody', route);
124 const { body: expectedBody } = route;
125
126 debug('Generating body matcher');
127 return (url, { body, method = 'get' }) => {
128 debug('Attempting to match body');
129 if (method.toLowerCase() === 'get') {
130 debug(' GET request - skip matching body');
131 // GET requests don’t send a body so the body matcher should be ignored for them
132 return true;
133 }
134
135 let sentBody;
136
137 try {
138 debug(' Parsing request body as JSON');
139 sentBody = JSON.parse(body);
140 } catch (err) {
141 debug(' Failed to parse request body as JSON', err);
142 }
143 debug('Expected body:', expectedBody);
144 debug('Actual body:', sentBody);
145 if (matchPartialBody) {
146 debug('matchPartialBody is true - checking for partial match only');
147 }
148
149 return (
150 sentBody &&
151 (matchPartialBody
152 ? isSubset(sentBody, expectedBody)
153 : isEqual(sentBody, expectedBody))
154 );
155 };
156};
157
158const getFullUrlMatcher = (route, matcherUrl, query) => {
159 // if none of the special syntaxes apply, it's just a simple string match
160 // but we have to be careful to normalize the url we check and the name
161 // of the route to allow for e.g. http://it.at.there being indistinguishable
162 // from http://it.at.there/ once we start generating Request/Url objects
163 debug(' Matching using full url', matcherUrl);
164 const expectedUrl = normalizeUrl(matcherUrl);
165 debug(' Normalised url to:', matcherUrl);
166 if (route.identifier === matcherUrl) {
167 debug(' Updating route identifier to match normalized url:', matcherUrl);
168 route.identifier = expectedUrl;
169 }
170
171 return (matcherUrl) => {
172 debug('Expected url:', expectedUrl);
173 debug('Actual url:', matcherUrl);
174 if (query && expectedUrl.indexOf('?')) {
175 debug('Ignoring query string when matching url');
176 return matcherUrl.indexOf(expectedUrl) === 0;
177 }
178 return normalizeUrl(matcherUrl) === expectedUrl;
179 };
180};
181
182const getFunctionMatcher = ({ functionMatcher }) => {
183 debug('Detected user defined function matcher', functionMatcher);
184 return (...args) => {
185 debug('Calling function matcher with arguments', args);
186 return functionMatcher(...args);
187 };
188};
189
190const getUrlMatcher = (route) => {
191 debug('Generating url matcher');
192 const { url: matcherUrl, query } = route;
193
194 if (matcherUrl === '*') {
195 debug(' Using universal * rule to match any url');
196 return () => true;
197 }
198
199 if (matcherUrl instanceof RegExp) {
200 debug(' Using regular expression to match url:', matcherUrl);
201 return (url) => matcherUrl.test(url);
202 }
203
204 if (matcherUrl.href) {
205 debug(` Using URL object to match url`, matcherUrl);
206 return getFullUrlMatcher(route, matcherUrl.href, query);
207 }
208
209 for (const shorthand in stringMatchers) {
210 if (matcherUrl.indexOf(shorthand + ':') === 0) {
211 debug(` Using ${shorthand}: pattern to match url`, matcherUrl);
212 const urlFragment = matcherUrl.replace(new RegExp(`^${shorthand}:`), '');
213 return stringMatchers[shorthand](urlFragment);
214 }
215 }
216
217 return getFullUrlMatcher(route, matcherUrl, query);
218};
219
220const FetchMock = {};
221
222FetchMock._matchers = [];
223
224FetchMock.addMatcher = function (matcher) {
225 this._matchers.push(matcher);
226};
227
228FetchMock.addMatcher({ name: 'query', matcher: getQueryStringMatcher });
229FetchMock.addMatcher({ name: 'method', matcher: getMethodMatcher });
230FetchMock.addMatcher({ name: 'headers', matcher: getHeaderMatcher });
231FetchMock.addMatcher({ name: 'params', matcher: getParamsMatcher });
232FetchMock.addMatcher({ name: 'body', matcher: getBodyMatcher, usesBody: true });
233FetchMock.addMatcher({ name: 'functionMatcher', matcher: getFunctionMatcher });
234FetchMock.addMatcher({ name: 'url', matcher: getUrlMatcher });
235
236module.exports = FetchMock;