1 | const { debug } = require('./debug');
|
2 | const glob = require('glob-to-regexp');
|
3 | const pathToRegexp = require('path-to-regexp');
|
4 | const querystring = require('querystring');
|
5 | const isSubset = require('is-subset');
|
6 | const {
|
7 | headers: headerUtils,
|
8 | getPath,
|
9 | getQuery,
|
10 | normalizeUrl,
|
11 | } = require('./request-utils');
|
12 | const isEqual = require('lodash.isequal');
|
13 |
|
14 | const debuggableUrlFunc = (func) => (url) => {
|
15 | debug('Actual url:', url);
|
16 | return func(url);
|
17 | };
|
18 |
|
19 | const 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 |
|
38 | const 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 |
|
59 | const 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 |
|
75 | const 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 |
|
92 | const 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 |
|
122 | const 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 |
|
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 |
|
158 | const getFullUrlMatcher = (route, matcherUrl, query) => {
|
159 |
|
160 |
|
161 |
|
162 |
|
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 |
|
182 | const 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 |
|
190 | const 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 |
|
220 | const FetchMock = {};
|
221 |
|
222 | FetchMock._matchers = [];
|
223 |
|
224 | FetchMock.addMatcher = function (matcher) {
|
225 | this._matchers.push(matcher);
|
226 | };
|
227 |
|
228 | FetchMock.addMatcher({ name: 'query', matcher: getQueryStringMatcher });
|
229 | FetchMock.addMatcher({ name: 'method', matcher: getMethodMatcher });
|
230 | FetchMock.addMatcher({ name: 'headers', matcher: getHeaderMatcher });
|
231 | FetchMock.addMatcher({ name: 'params', matcher: getParamsMatcher });
|
232 | FetchMock.addMatcher({ name: 'body', matcher: getBodyMatcher, usesBody: true });
|
233 | FetchMock.addMatcher({ name: 'functionMatcher', matcher: getFunctionMatcher });
|
234 | FetchMock.addMatcher({ name: 'url', matcher: getUrlMatcher });
|
235 |
|
236 | module.exports = FetchMock;
|