fetch-mock
Advanced tools
Comparing version 8.3.2 to 9.0.0-beta.2
@@ -0,1 +1,2 @@ | ||
const { getDebug } = require('./debug'); | ||
const generateMatcher = require('./generate-matcher'); | ||
@@ -17,2 +18,3 @@ | ||
(typeof matcher === 'object' && 'href' in matcher); | ||
const isFunctionMatcher = matcher => typeof matcher === 'function'; | ||
@@ -40,8 +42,12 @@ | ||
const sanitizeRoute = route => { | ||
const debug = getDebug('sanitizeRoute()'); | ||
debug('Sanitizing route properties'); | ||
route = Object.assign({}, route); | ||
if (route.method) { | ||
debug(`Converting method ${route.method} to lower case`); | ||
route.method = route.method.toLowerCase(); | ||
} | ||
if (isUrlMatcher(route.matcher)) { | ||
debug('Mock uses a url matcher', route.matcher); | ||
route.url = route.matcher; | ||
@@ -53,3 +59,8 @@ delete route.matcher; | ||
debug('Setting route.identifier...'); | ||
debug(` route.name is ${route.name}`); | ||
debug(` route.url is ${route.url}`); | ||
debug(` route.functionMatcher is ${route.functionMatcher}`); | ||
route.identifier = route.name || route.url || route.functionMatcher; | ||
debug(` -> route.identifier set to ${route.identifier}`); | ||
return route; | ||
@@ -70,7 +81,13 @@ }; | ||
const limitMatcher = route => { | ||
const limit = route => { | ||
const debug = getDebug('limit()'); | ||
debug('Limiting number of requests to handle by route'); | ||
if (!route.repeat) { | ||
debug( | ||
' No `repeat` value set on route. Will match any number of requests' | ||
); | ||
return; | ||
} | ||
debug(` Route set to repeat ${route.repeat} times`); | ||
const matcher = route.matcher; | ||
@@ -89,7 +106,16 @@ let timesLeft = route.repeat; | ||
const delayResponse = route => { | ||
const debug = getDebug('delayResponse()'); | ||
debug(`Applying response delay settings`); | ||
const { delay } = route; | ||
if (delay) { | ||
debug(` Wrapping response in delay of ${delay} miliseconds`); | ||
const response = route.response; | ||
route.response = () => | ||
new Promise(res => setTimeout(() => res(response), delay)); | ||
route.response = () => { | ||
debug(`Delaying response by ${delay} miliseconds`); | ||
return new Promise(res => setTimeout(() => res(response), delay)); | ||
}; | ||
} else { | ||
debug( | ||
` No delay set on route. Will respond 'immediately' (but asynchronously)` | ||
); | ||
} | ||
@@ -99,6 +125,8 @@ }; | ||
const compileRoute = function(args) { | ||
const debug = getDebug('compileRoute()'); | ||
debug('Compiling route'); | ||
const route = sanitizeRoute(argsToRoute(args)); | ||
validateRoute(route); | ||
route.matcher = generateMatcher(route); | ||
limitMatcher(route); | ||
limit(route); | ||
delayResponse(route); | ||
@@ -105,0 +133,0 @@ return route; |
@@ -0,1 +1,2 @@ | ||
const { debug, setDebugPhase, getDebug } = require('./debug'); | ||
const responseBuilder = require('./response-builder'); | ||
@@ -26,2 +27,4 @@ const requestUtils = require('./request-utils'); | ||
) => { | ||
const debug = getDebug('resolve()'); | ||
debug('Recursively resolving function and promise responses'); | ||
// We want to allow things like | ||
@@ -36,11 +39,23 @@ // - function returning a Promise for a response | ||
if (typeof response === 'function') { | ||
debug(' Response is a function'); | ||
// in the case of falling back to the network we need to make sure we're using | ||
// the original Request instance, not our normalised url + options | ||
response = | ||
request && responseIsFetch | ||
? response(request) | ||
: response(url, options, request); | ||
if (responseIsFetch) { | ||
if (request) { | ||
debug(' -> Calling fetch with Request instance'); | ||
return response(request); | ||
} | ||
debug(' -> Calling fetch with url and options'); | ||
return response(url, options); | ||
} else { | ||
debug(' -> Calling response function'); | ||
response = response(url, options, request); | ||
} | ||
} else if (typeof response.then === 'function') { | ||
debug(' Response is a promise'); | ||
debug(' -> Resolving promise'); | ||
response = await response; | ||
} else { | ||
debug(' Response is not a function or a promise'); | ||
debug(' -> Exiting response resolution recursion'); | ||
return response; | ||
@@ -52,2 +67,5 @@ } | ||
FetchMock.fetchHandler = function(url, options, request) { | ||
setDebugPhase('handle'); | ||
const debug = getDebug('fetchHandler()'); | ||
debug('fetch called with:', url, options); | ||
const normalizedRequest = requestUtils.normalizeRequest( | ||
@@ -63,2 +81,8 @@ url, | ||
debug('Request normalised'); | ||
debug(' url', url); | ||
debug(' options', options); | ||
debug(' request', request); | ||
debug(' signal', signal); | ||
const route = this.executeRouter(url, options, request); | ||
@@ -74,3 +98,5 @@ | ||
if (signal) { | ||
debug('signal exists - enabling fetch abort'); | ||
const abort = () => { | ||
debug('aborting fetch'); | ||
// note that DOMException is not available in node.js; even node-fetch uses a custom error class: https://github.com/bitinn/node-fetch/blob/master/src/abort-error.js | ||
@@ -85,2 +111,3 @@ rej( | ||
if (signal.aborted) { | ||
debug('signal is already aborted - aborting the fetch'); | ||
abort(); | ||
@@ -93,3 +120,6 @@ } | ||
.then(res, rej) | ||
.then(done, done); | ||
.then(done, done) | ||
.then(() => { | ||
setDebugPhase(); | ||
}); | ||
}); | ||
@@ -101,3 +131,8 @@ }; | ||
FetchMock.executeRouter = function(url, options, request) { | ||
const debug = getDebug('executeRouter()'); | ||
debug(`Attempting to match request to a route`); | ||
if (this.config.fallbackToNetwork === 'always') { | ||
debug( | ||
' Configured with fallbackToNetwork=always - passing through to fetch' | ||
); | ||
return { response: this.getNativeFetch(), responseIsFetch: true }; | ||
@@ -109,2 +144,3 @@ } | ||
if (match) { | ||
debug(' Matching route found'); | ||
return match; | ||
@@ -120,2 +156,3 @@ } | ||
if (this.fallbackResponse) { | ||
debug(' No matching route found - using fallbackResponse'); | ||
return { response: this.fallbackResponse }; | ||
@@ -132,2 +169,3 @@ } | ||
debug(' Configured to fallbackToNetwork - passing through to fetch'); | ||
return { response: this.getNativeFetch(), responseIsFetch: true }; | ||
@@ -137,2 +175,3 @@ }; | ||
FetchMock.generateResponse = async function(route, url, options, request) { | ||
const debug = getDebug('generateResponse()'); | ||
const response = await resolve(route, url, options, request); | ||
@@ -143,2 +182,3 @@ | ||
if (response.throws && typeof response !== 'function') { | ||
debug('response.throws is defined - throwing an error'); | ||
throw response.throws; | ||
@@ -149,2 +189,3 @@ } | ||
if (this.config.Response.prototype.isPrototypeOf(response)) { | ||
debug('response is already a Response instance - returning it'); | ||
return response; | ||
@@ -163,3 +204,6 @@ } | ||
FetchMock.router = function(url, options, request) { | ||
const route = this.routes.find(route => route.matcher(url, options, request)); | ||
const route = this.routes.find((route, i) => { | ||
debug(`Trying to match route ${i}`); | ||
return route.matcher(url, options, request); | ||
}); | ||
@@ -181,3 +225,3 @@ if (route) { | ||
throw new Error( | ||
'fetch-mock: Falling back to network only available on gloabl fetch-mock, or by setting config.fetch on sandboxed fetch-mock' | ||
'fetch-mock: Falling back to network only available on global fetch-mock, or by setting config.fetch on sandboxed fetch-mock' | ||
); | ||
@@ -189,2 +233,9 @@ } | ||
FetchMock.push = function({ url, options, request, isUnmatched, identifier }) { | ||
debug('Recording fetch call', { | ||
url, | ||
options, | ||
request, | ||
isUnmatched, | ||
identifier | ||
}); | ||
const args = [url, options]; | ||
@@ -191,0 +242,0 @@ args.request = request; |
@@ -0,1 +1,2 @@ | ||
const { debug, setDebugNamespace } = require('./debug'); | ||
const glob = require('glob-to-regexp'); | ||
@@ -12,23 +13,38 @@ const pathToRegexp = require('path-to-regexp'); | ||
const debuggableUrlFunc = func => url => { | ||
debug('Actual url:', url); | ||
return func(url); | ||
}; | ||
const stringMatchers = { | ||
begin: targetString => url => url.indexOf(targetString) === 0, | ||
end: targetString => url => url.substr(-targetString.length) === targetString, | ||
begin: targetString => | ||
debuggableUrlFunc(url => url.indexOf(targetString) === 0), | ||
end: targetString => | ||
debuggableUrlFunc(url => url.substr(-targetString.length) === targetString), | ||
glob: targetString => { | ||
const urlRX = glob(targetString); | ||
return url => urlRX.test(url); | ||
return debuggableUrlFunc(url => urlRX.test(url)); | ||
}, | ||
express: targetString => { | ||
const urlRX = pathToRegexp(targetString); | ||
return url => urlRX.test(getPath(url)); | ||
return debuggableUrlFunc(url => urlRX.test(getPath(url))); | ||
}, | ||
path: targetString => url => getPath(url) === targetString | ||
path: targetString => debuggableUrlFunc(url => getPath(url) === targetString) | ||
}; | ||
const getHeaderMatcher = ({ headers: expectedHeaders }) => { | ||
debug('Generating header matcher'); | ||
if (!expectedHeaders) { | ||
debug(' No header expectations defined - skipping'); | ||
return; | ||
} | ||
const expectation = headerUtils.toLowerCase(expectedHeaders); | ||
debug(' Expected headers:', expectation); | ||
return (url, { headers = {} }) => { | ||
debug('Attempting to match headers'); | ||
const lowerCaseHeaders = headerUtils.toLowerCase( | ||
headerUtils.normalize(headers) | ||
); | ||
debug(' Expected headers:', expectation); | ||
debug(' Actual headers:', lowerCaseHeaders); | ||
return Object.keys(expectation).every(headerName => | ||
@@ -41,10 +57,30 @@ headerUtils.equal(lowerCaseHeaders[headerName], expectation[headerName]) | ||
const getMethodMatcher = ({ method: expectedMethod }) => { | ||
return (url, { method }) => | ||
expectedMethod === (method ? method.toLowerCase() : 'get'); | ||
debug('Generating method matcher'); | ||
if (!expectedMethod) { | ||
debug(' No method expectations defined - skipping'); | ||
return; | ||
} | ||
debug(' Expected method:', expectedMethod); | ||
return (url, { method }) => { | ||
debug('Attempting to match method'); | ||
const actualMethod = method ? method.toLowerCase() : 'get'; | ||
debug(' Expected method:', expectedMethod); | ||
debug(' Actual method:', actualMethod); | ||
return expectedMethod === actualMethod; | ||
}; | ||
}; | ||
const getQueryStringMatcher = ({ query: expectedQuery }) => { | ||
debug('Generating query parameters matcher'); | ||
if (!expectedQuery) { | ||
debug(' No query parameters expectations defined - skipping'); | ||
return; | ||
} | ||
debug(' Expected query parameters:', expectedQuery); | ||
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 => query[key] === expectedQuery[key]); | ||
@@ -54,4 +90,9 @@ }; | ||
const getParamsMatcher = ({ params: expectedParams, url: matcheUrl }) => { | ||
if (!/express:/.test(matcheUrl)) { | ||
const getParamsMatcher = ({ params: expectedParams, url: matcherUrl }) => { | ||
debug('Generating path parameters matcher'); | ||
if (!expectedParams) { | ||
debug(' No path parameters expectations defined - skipping'); | ||
return; | ||
} | ||
if (!/express:/.test(matcherUrl)) { | ||
throw new Error( | ||
@@ -61,6 +102,8 @@ 'fetch-mock: matching on params is only possible when using an express: matcher' | ||
} | ||
debug(' Expected path parameters:', expectedParams); | ||
const expectedKeys = Object.keys(expectedParams); | ||
const keys = []; | ||
const re = pathToRegexp(matcheUrl.replace(/^express:/, ''), keys); | ||
const re = pathToRegexp(matcherUrl.replace(/^express:/, ''), keys); | ||
return url => { | ||
debug('Attempting to match path parameters'); | ||
const vals = re.exec(getPath(url)) || []; | ||
@@ -73,2 +116,4 @@ vals.shift(); | ||
); | ||
debug(' Expected path parameters:', expectedParams); | ||
debug(' Actual path parameters:', params); | ||
return expectedKeys.every(key => params[key] === expectedParams[key]); | ||
@@ -79,4 +124,7 @@ }; | ||
const getBodyMatcher = ({ body: expectedBody }) => { | ||
debug('Generating body matcher'); | ||
return (url, { body, method = 'get' }) => { | ||
debug('Attempting to match body'); | ||
if (method.toLowerCase() === 'get') { | ||
debug(' GET request - skip matching body'); | ||
// GET requests don’t send a body so the body matcher should be ignored for them | ||
@@ -89,4 +137,9 @@ return true; | ||
try { | ||
debug(' Parsing request body as JSON'); | ||
sentBody = JSON.parse(body); | ||
} catch (_) {} | ||
} catch (err) { | ||
debug(' Failed to parse request body as JSON', err); | ||
} | ||
debug('Expected body:', expectedBody); | ||
debug('Actual body:', sentBody); | ||
@@ -102,4 +155,7 @@ return sentBody && isEqual(sentBody, expectedBody); | ||
// from http://it.at.there/ once we start generating Request/Url objects | ||
debug(' Matching using full url', matcherUrl); | ||
const expectedUrl = normalizeUrl(matcherUrl); | ||
debug(' Normalised url to:', matcherUrl); | ||
if (route.identifier === matcherUrl) { | ||
debug(' Updating route identifier to match normalized url:', matcherUrl); | ||
route.identifier = expectedUrl; | ||
@@ -109,3 +165,6 @@ } | ||
return matcherUrl => { | ||
debug('Expected url:', expectedUrl); | ||
debug('Actual url:', matcherUrl); | ||
if (query && expectedUrl.indexOf('?')) { | ||
debug('Ignoring query string when matching url'); | ||
return matcherUrl.indexOf(expectedUrl) === 0; | ||
@@ -117,8 +176,16 @@ } | ||
const getFunctionMatcher = ({ functionMatcher }) => functionMatcher; | ||
const getFunctionMatcher = ({ functionMatcher }) => { | ||
debug('Detected user defined function matcher', functionMatcher); | ||
return (...args) => { | ||
debug('Calling function matcher with arguments', args); | ||
return functionMatcher(...args); | ||
}; | ||
}; | ||
const getUrlMatcher = route => { | ||
debug('Generating url matcher'); | ||
const { url: matcherUrl, query } = route; | ||
if (matcherUrl === '*') { | ||
debug(' Using universal * rule to match any url'); | ||
return () => true; | ||
@@ -128,2 +195,3 @@ } | ||
if (matcherUrl instanceof RegExp) { | ||
debug(' Using regular expression to match url:', matcherUrl); | ||
return url => matcherUrl.test(url); | ||
@@ -133,2 +201,3 @@ } | ||
if (matcherUrl.href) { | ||
debug(` Using URL object to match url`, matcherUrl); | ||
return getFullUrlMatcher(route, matcherUrl.href, query); | ||
@@ -139,2 +208,3 @@ } | ||
if (matcherUrl.indexOf(shorthand + ':') === 0) { | ||
debug(` Using ${shorthand}: pattern to match url`, matcherUrl); | ||
const urlFragment = matcherUrl.replace(new RegExp(`^${shorthand}:`), ''); | ||
@@ -149,2 +219,4 @@ return stringMatchers[shorthand](urlFragment); | ||
module.exports = route => { | ||
setDebugNamespace('generateMatcher()'); | ||
debug('Compiling matcher for route'); | ||
const matchers = [ | ||
@@ -160,4 +232,6 @@ route.query && getQueryStringMatcher(route), | ||
debug('Compiled matcher for route'); | ||
setDebugNamespace(); | ||
return (url, options = {}, request) => | ||
matchers.every(matcher => matcher(url, options, request)); | ||
}; |
@@ -0,1 +1,2 @@ | ||
const { debug } = require('./debug'); | ||
const setUpAndTearDown = require('./set-up-and-tear-down'); | ||
@@ -16,2 +17,3 @@ const fetchHandler = require('./fetch-handler'); | ||
FetchMock.createInstance = function() { | ||
debug('Creating fetch-mock instance'); | ||
const instance = Object.create(FetchMock); | ||
@@ -38,2 +40,3 @@ instance._uncompiledRoutes = (this._uncompiledRoutes || []).slice(); | ||
FetchMock.sandbox = function() { | ||
debug('Creating sandboxed fetch-mock instance'); | ||
// this construct allows us to create a fetch-mock instance which is also | ||
@@ -40,0 +43,0 @@ // a callable function, while circumventing circularity when defining the |
@@ -0,1 +1,2 @@ | ||
const { setDebugPhase, setDebugNamespace, debug } = require('./debug'); | ||
const { normalizeUrl } = require('./request-utils'); | ||
@@ -13,3 +14,13 @@ const FetchMock = {}; | ||
const formatDebug = func => { | ||
return function(...args) { | ||
setDebugPhase('inspect'); | ||
const result = func.call(this, ...args); | ||
setDebugPhase(); | ||
return result; | ||
}; | ||
}; | ||
FetchMock.filterCalls = function(nameOrMatcher, options) { | ||
debug('Filtering fetch calls'); | ||
let calls = this._calls; | ||
@@ -19,8 +30,16 @@ let matcher = '*'; | ||
if ([true, 'matched'].includes(nameOrMatcher)) { | ||
debug(`Filter provided is ${nameOrMatcher}. Returning matched calls only`); | ||
calls = calls.filter(({ isUnmatched }) => !isUnmatched); | ||
} else if ([false, 'unmatched'].includes(nameOrMatcher)) { | ||
debug( | ||
`Filter provided is ${nameOrMatcher}. Returning unmatched calls only` | ||
); | ||
calls = calls.filter(({ isUnmatched }) => isUnmatched); | ||
} else if (typeof nameOrMatcher === 'undefined') { | ||
debug(`Filter provided is undefined. Returning all calls`); | ||
calls = calls; | ||
} else if (isName(nameOrMatcher)) { | ||
debug( | ||
`Filter provided, looks like the name of a named route. Returning only calls handled by that route` | ||
); | ||
calls = calls.filter(({ identifier }) => identifier === nameOrMatcher); | ||
@@ -30,2 +49,5 @@ } else { | ||
if (this.routes.some(({ identifier }) => identifier === matcher)) { | ||
debug( | ||
`Filter provided, ${nameOrMatcher}, identifies a route. Returning only calls handled by that route` | ||
); | ||
calls = calls.filter(call => call.identifier === matcher); | ||
@@ -39,49 +61,82 @@ } | ||
} | ||
debug( | ||
'Compiling filter and options to route in order to filter all calls', | ||
nameOrMatcher | ||
); | ||
calls = filterCallsWithMatcher(matcher, options, calls); | ||
} | ||
debug(`Retrieved ${calls.length} calls`); | ||
return calls; | ||
}; | ||
FetchMock.calls = function(nameOrMatcher, options) { | ||
FetchMock.calls = formatDebug(function(nameOrMatcher, options) { | ||
debug('retrieving matching calls'); | ||
return this.filterCalls(nameOrMatcher, options); | ||
}; | ||
}); | ||
FetchMock.lastCall = function(nameOrMatcher, options) { | ||
FetchMock.lastCall = formatDebug(function(nameOrMatcher, options) { | ||
debug('retrieving last matching call'); | ||
return [...this.filterCalls(nameOrMatcher, options)].pop(); | ||
}; | ||
}); | ||
FetchMock.lastUrl = function(nameOrMatcher, options) { | ||
FetchMock.lastUrl = formatDebug(function(nameOrMatcher, options) { | ||
debug('retrieving url of last matching call'); | ||
return (this.lastCall(nameOrMatcher, options) || [])[0]; | ||
}; | ||
}); | ||
FetchMock.lastOptions = function(nameOrMatcher, options) { | ||
FetchMock.lastOptions = formatDebug(function(nameOrMatcher, options) { | ||
debug('retrieving options of last matching call'); | ||
return (this.lastCall(nameOrMatcher, options) || [])[1]; | ||
}; | ||
}); | ||
FetchMock.called = function(nameOrMatcher, options) { | ||
FetchMock.called = formatDebug(function(nameOrMatcher, options) { | ||
debug('checking if matching call was made'); | ||
return !!this.filterCalls(nameOrMatcher, options).length; | ||
}; | ||
}); | ||
FetchMock.flush = function(waitForResponseMethods) { | ||
FetchMock.flush = formatDebug(async function(waitForResponseMethods) { | ||
setDebugNamespace('flush'); | ||
debug( | ||
`flushing all fetch calls. ${ | ||
waitForResponseMethods ? '' : 'Not ' | ||
}waiting for response bodies to complete download` | ||
); | ||
const queuedPromises = this._holdingPromises; | ||
this._holdingPromises = []; | ||
debug(`${queuedPromises.length} fetch calls to be awaited`); | ||
return Promise.all(queuedPromises).then(() => { | ||
if (waitForResponseMethods && this._holdingPromises.length) { | ||
return this.flush(waitForResponseMethods); | ||
} | ||
}); | ||
}; | ||
await Promise.all(queuedPromises); | ||
debug(`All fetch calls have completed`); | ||
if (waitForResponseMethods && this._holdingPromises.length) { | ||
debug(`Awaiting all fetch bodies to download`); | ||
await this.flush(waitForResponseMethods); | ||
debug(`All fetch bodies have completed downloading`); | ||
} | ||
setDebugNamespace(); | ||
}); | ||
FetchMock.done = function(nameOrMatcher) { | ||
const routesToCheck = | ||
nameOrMatcher && typeof nameOrMatcher !== 'boolean' | ||
? [{ identifier: nameOrMatcher }] | ||
: this.routes; | ||
FetchMock.done = formatDebug(function(nameOrMatcher) { | ||
setDebugPhase('inspect'); | ||
setDebugNamespace('done'); | ||
debug('Checking to see if expected calls have been made'); | ||
let routesToCheck; | ||
if (nameOrMatcher && typeof nameOrMatcher !== 'boolean') { | ||
debug( | ||
'Checking to see if expected calls have been made for single route:', | ||
nameOrMatcher | ||
); | ||
routesToCheck = [{ identifier: nameOrMatcher }]; | ||
} else { | ||
debug('Checking to see if expected calls have been made for all routes'); | ||
routesToCheck = this.routes; | ||
} | ||
// Can't use array.every because would exit after first failure, which would | ||
// break the logging | ||
return routesToCheck | ||
const result = routesToCheck | ||
.map(({ identifier }) => { | ||
if (!this.called(identifier)) { | ||
debug('No calls made for route:', identifier); | ||
console.warn(`Warning: ${identifier} not called`); // eslint-disable-line | ||
@@ -96,6 +151,16 @@ return false; | ||
if (!expectedTimes) { | ||
debug( | ||
'Route has been called at least once, and no expectation of more set:', | ||
identifier | ||
); | ||
return true; | ||
} | ||
const actualTimes = this.filterCalls(identifier).length; | ||
debug(`Route called ${actualTimes} times:`, identifier); | ||
if (expectedTimes > actualTimes) { | ||
debug( | ||
`Route called ${actualTimes} times, but expected ${expectedTimes}:`, | ||
identifier | ||
); | ||
console.warn( | ||
@@ -110,4 +175,8 @@ `Warning: ${identifier} only called ${actualTimes} times, but ${expectedTimes} expected` | ||
.every(isDone => isDone); | ||
}; | ||
setDebugNamespace(); | ||
setDebugPhase(); | ||
return result; | ||
}); | ||
module.exports = FetchMock; |
@@ -0,1 +1,2 @@ | ||
const { getDebug } = require('./debug'); | ||
const responseConfigProps = [ | ||
@@ -11,2 +12,4 @@ 'body', | ||
constructor(options) { | ||
this.debug = getDebug('ResponseBuilder()'); | ||
this.debug('Response builder created with options', options); | ||
Object.assign(this, options); | ||
@@ -16,2 +19,3 @@ } | ||
exec() { | ||
this.debug('building response'); | ||
this.normalizeResponseConfig(); | ||
@@ -44,2 +48,3 @@ this.constructFetchOpts(); | ||
if (typeof this.responseConfig === 'number') { | ||
this.debug('building response using status', this.responseConfig); | ||
this.responseConfig = { | ||
@@ -51,2 +56,3 @@ status: this.responseConfig | ||
} else if (typeof this.responseConfig === 'string' || this.sendAsObject()) { | ||
this.debug('building text response from', this.responseConfig); | ||
this.responseConfig = { | ||
@@ -60,2 +66,3 @@ body: this.responseConfig | ||
if (!status) { | ||
this.debug('No status provided. Defaulting to 200'); | ||
return 200; | ||
@@ -70,2 +77,3 @@ } | ||
) { | ||
this.debug('Valid status provided', status); | ||
return status; | ||
@@ -105,2 +113,3 @@ } | ||
) { | ||
this.debug('Stringifying JSON response body'); | ||
this.body = JSON.stringify(this.body); | ||
@@ -120,2 +129,3 @@ if (!this.options.headers.has('Content-Type')) { | ||
) { | ||
this.debug('Setting content-length header:', this.body.length.toString()); | ||
this.options.headers.set('Content-Length', this.body.length.toString()); | ||
@@ -134,2 +144,3 @@ } | ||
if (this.Stream) { | ||
this.debug('Creating response stream'); | ||
const stream = new this.Stream.Readable(); | ||
@@ -151,2 +162,3 @@ if (this.body != null) { //eslint-disable-line | ||
// promises returned by res.json(), res.text() etc | ||
this.debug('Wrappipng Response in ES proxy for observability'); | ||
return new Proxy(response, { | ||
@@ -156,2 +168,6 @@ get: (originalResponse, name) => { | ||
if (name === 'url') { | ||
this.debug( | ||
'Retrieving redirect url', | ||
this.responseConfig.redirectUrl | ||
); | ||
return this.responseConfig.redirectUrl; | ||
@@ -161,2 +177,3 @@ } | ||
if (name === 'redirected') { | ||
this.debug('Retrieving redirected status', true); | ||
return true; | ||
@@ -167,4 +184,6 @@ } | ||
if (typeof originalResponse[name] === 'function') { | ||
this.debug('Wrapping body promises in ES proxies for observability'); | ||
return new Proxy(originalResponse[name], { | ||
apply: (func, thisArg, args) => { | ||
this.debug(`Calling res.${name}`); | ||
const result = func.apply(response, args); | ||
@@ -171,0 +190,0 @@ if (result.then) { |
@@ -0,1 +1,2 @@ | ||
const { debug, setDebugPhase } = require('./debug'); | ||
const { compileRoute } = require('./compile-route'); | ||
@@ -5,2 +6,3 @@ const FetchMock = {}; | ||
FetchMock.mock = function(...args) { | ||
setDebugPhase('setup'); | ||
if (args.length) { | ||
@@ -14,2 +16,3 @@ this.addRoute(args); | ||
FetchMock.addRoute = function(uncompiledRoute) { | ||
debug('Adding route', uncompiledRoute); | ||
const route = this.compileRoute(uncompiledRoute); | ||
@@ -38,3 +41,2 @@ const clashes = this.routes.filter( | ||
}); | ||
return this.routes; | ||
@@ -59,2 +61,3 @@ } | ||
} | ||
setDebugPhase(); | ||
return this; | ||
@@ -61,0 +64,0 @@ }; |
@@ -21,2 +21,5 @@ 'use strict'; | ||
var _require = require('./debug'), | ||
getDebug = _require.getDebug; | ||
var generateMatcher = require('./generate-matcher'); | ||
@@ -29,2 +32,3 @@ | ||
}; | ||
var isFunctionMatcher = function isFunctionMatcher(matcher) { | ||
@@ -58,8 +62,12 @@ return typeof matcher === 'function'; | ||
var sanitizeRoute = function sanitizeRoute(route) { | ||
var debug = getDebug('sanitizeRoute()'); | ||
debug('Sanitizing route properties'); | ||
route = (0, _assign2.default)({}, route); | ||
if (route.method) { | ||
debug('Converting method ' + route.method + ' to lower case'); | ||
route.method = route.method.toLowerCase(); | ||
} | ||
if (isUrlMatcher(route.matcher)) { | ||
debug('Mock uses a url matcher', route.matcher); | ||
route.url = route.matcher; | ||
@@ -71,3 +79,8 @@ delete route.matcher; | ||
debug('Setting route.identifier...'); | ||
debug(' route.name is ' + route.name); | ||
debug(' route.url is ' + route.url); | ||
debug(' route.functionMatcher is ' + route.functionMatcher); | ||
route.identifier = route.name || route.url || route.functionMatcher; | ||
debug(' -> route.identifier set to ' + route.identifier); | ||
return route; | ||
@@ -88,7 +101,11 @@ }; | ||
var limitMatcher = function limitMatcher(route) { | ||
var limit = function limit(route) { | ||
var debug = getDebug('limit()'); | ||
debug('Limiting number of requests to handle by route'); | ||
if (!route.repeat) { | ||
debug(' No `repeat` value set on route. Will match any number of requests'); | ||
return; | ||
} | ||
debug(' Route set to repeat ' + route.repeat + ' times'); | ||
var matcher = route.matcher; | ||
@@ -109,7 +126,11 @@ var timesLeft = route.repeat; | ||
var delayResponse = function delayResponse(route) { | ||
var debug = getDebug('delayResponse()'); | ||
debug('Applying response delay settings'); | ||
var delay = route.delay; | ||
if (delay) { | ||
debug(' Wrapping response in delay of ' + delay + ' miliseconds'); | ||
var response = route.response; | ||
route.response = function () { | ||
debug('Delaying response by ' + delay + ' miliseconds'); | ||
return new _promise2.default(function (res) { | ||
@@ -121,2 +142,4 @@ return setTimeout(function () { | ||
}; | ||
} else { | ||
debug(' No delay set on route. Will respond \'immediately\' (but asynchronously)'); | ||
} | ||
@@ -126,6 +149,8 @@ }; | ||
var compileRoute = function compileRoute(args) { | ||
var debug = getDebug('compileRoute()'); | ||
debug('Compiling route'); | ||
var route = sanitizeRoute(argsToRoute(args)); | ||
validateRoute(route); | ||
route.matcher = generateMatcher(route); | ||
limitMatcher(route); | ||
limit(route); | ||
delayResponse(route); | ||
@@ -132,0 +157,0 @@ return route; |
@@ -29,2 +29,7 @@ 'use strict'; | ||
var _require = require('./debug'), | ||
debug = _require.debug, | ||
setDebugPhase = _require.setDebugPhase, | ||
getDebug = _require.getDebug; | ||
var responseBuilder = require('./response-builder'); | ||
@@ -63,2 +68,3 @@ var requestUtils = require('./request-utils'); | ||
responseIsFetch = _ref$responseIsFetch === undefined ? false : _ref$responseIsFetch; | ||
var debug; | ||
return _regenerator2.default.wrap(function _callee$(_context) { | ||
@@ -68,4 +74,16 @@ while (1) { | ||
case 0: | ||
debug = getDebug('resolve()'); | ||
debug('Recursively resolving function and promise responses'); | ||
// We want to allow things like | ||
// - function returning a Promise for a response | ||
// - delaying (using a timeout Promise) a function's execution to generate | ||
// a response | ||
// Because of this we can't safely check for function before Promisey-ness, | ||
// or vice versa. So to keep it DRY, and flexible, we keep trying until we | ||
// have something that looks like neither Promise nor function | ||
case 2: | ||
if (!true) { | ||
_context.next = 14; | ||
_context.next = 30; | ||
break; | ||
@@ -75,34 +93,61 @@ } | ||
if (!(typeof response === 'function')) { | ||
_context.next = 5; | ||
_context.next = 17; | ||
break; | ||
} | ||
debug(' Response is a function'); | ||
// in the case of falling back to the network we need to make sure we're using | ||
// the original Request instance, not our normalised url + options | ||
response = request && responseIsFetch ? response(request) : response(url, options, request); | ||
_context.next = 12; | ||
if (!responseIsFetch) { | ||
_context.next = 13; | ||
break; | ||
} | ||
if (!request) { | ||
_context.next = 9; | ||
break; | ||
} | ||
debug(' -> Calling fetch with Request instance'); | ||
return _context.abrupt('return', response(request)); | ||
case 9: | ||
debug(' -> Calling fetch with url and options'); | ||
return _context.abrupt('return', response(url, options)); | ||
case 13: | ||
debug(' -> Calling response function'); | ||
response = response(url, options, request); | ||
case 15: | ||
_context.next = 28; | ||
break; | ||
case 5: | ||
case 17: | ||
if (!(typeof response.then === 'function')) { | ||
_context.next = 11; | ||
_context.next = 25; | ||
break; | ||
} | ||
_context.next = 8; | ||
debug(' Response is a promise'); | ||
debug(' -> Resolving promise'); | ||
_context.next = 22; | ||
return response; | ||
case 8: | ||
case 22: | ||
response = _context.sent; | ||
_context.next = 12; | ||
_context.next = 28; | ||
break; | ||
case 11: | ||
case 25: | ||
debug(' Response is not a function or a promise'); | ||
debug(' -> Exiting response resolution recursion'); | ||
return _context.abrupt('return', response); | ||
case 12: | ||
_context.next = 0; | ||
case 28: | ||
_context.next = 2; | ||
break; | ||
case 14: | ||
case 30: | ||
case 'end': | ||
@@ -123,2 +168,5 @@ return _context.stop(); | ||
setDebugPhase('handle'); | ||
var debug = getDebug('fetchHandler()'); | ||
debug('fetch called with:', url, options); | ||
var normalizedRequest = requestUtils.normalizeRequest(url, options, this.config.Request); | ||
@@ -132,2 +180,8 @@ | ||
debug('Request normalised'); | ||
debug(' url', url); | ||
debug(' options', options); | ||
debug(' request', request); | ||
debug(' signal', signal); | ||
var route = this.executeRouter(url, options, request); | ||
@@ -145,3 +199,5 @@ | ||
if (signal) { | ||
debug('signal exists - enabling fetch abort'); | ||
var abort = function abort() { | ||
debug('aborting fetch'); | ||
// note that DOMException is not available in node.js; even node-fetch uses a custom error class: https://github.com/bitinn/node-fetch/blob/master/src/abort-error.js | ||
@@ -152,2 +208,3 @@ rej(typeof DOMException !== 'undefined' ? new DOMException('The operation was aborted.', 'AbortError') : new AbortError()); | ||
if (signal.aborted) { | ||
debug('signal is already aborted - aborting the fetch'); | ||
abort(); | ||
@@ -158,3 +215,5 @@ } | ||
_this2.generateResponse(route, url, options, request).then(res, rej).then(done, done); | ||
_this2.generateResponse(route, url, options, request).then(res, rej).then(done, done).then(function () { | ||
setDebugPhase(); | ||
}); | ||
}); | ||
@@ -166,3 +225,6 @@ }; | ||
FetchMock.executeRouter = function (url, options, request) { | ||
var debug = getDebug('executeRouter()'); | ||
debug('Attempting to match request to a route'); | ||
if (this.config.fallbackToNetwork === 'always') { | ||
debug(' Configured with fallbackToNetwork=always - passing through to fetch'); | ||
return { response: this.getNativeFetch(), responseIsFetch: true }; | ||
@@ -174,2 +236,3 @@ } | ||
if (match) { | ||
debug(' Matching route found'); | ||
return match; | ||
@@ -185,2 +248,3 @@ } | ||
if (this.fallbackResponse) { | ||
debug(' No matching route found - using fallbackResponse'); | ||
return { response: this.fallbackResponse }; | ||
@@ -193,2 +257,3 @@ } | ||
debug(' Configured to fallbackToNetwork - passing through to fetch'); | ||
return { response: this.getNativeFetch(), responseIsFetch: true }; | ||
@@ -199,3 +264,3 @@ }; | ||
var _ref3 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee2(route, url, options, request) { | ||
var response; | ||
var debug, response; | ||
return _regenerator2.default.wrap(function _callee2$(_context2) { | ||
@@ -205,24 +270,27 @@ while (1) { | ||
case 0: | ||
_context2.next = 2; | ||
debug = getDebug('generateResponse()'); | ||
_context2.next = 3; | ||
return resolve(route, url, options, request); | ||
case 2: | ||
case 3: | ||
response = _context2.sent; | ||
if (!(response.throws && typeof response !== 'function')) { | ||
_context2.next = 5; | ||
_context2.next = 7; | ||
break; | ||
} | ||
debug('response.throws is defined - throwing an error'); | ||
throw response.throws; | ||
case 5: | ||
case 7: | ||
if (!this.config.Response.prototype.isPrototypeOf(response)) { | ||
_context2.next = 7; | ||
_context2.next = 10; | ||
break; | ||
} | ||
debug('response is already a Response instance - returning it'); | ||
return _context2.abrupt('return', response); | ||
case 7: | ||
case 10: | ||
return _context2.abrupt('return', responseBuilder({ | ||
@@ -235,3 +303,3 @@ url: url, | ||
case 8: | ||
case 11: | ||
case 'end': | ||
@@ -250,3 +318,4 @@ return _context2.stop(); | ||
FetchMock.router = function (url, options, request) { | ||
var route = this.routes.find(function (route) { | ||
var route = this.routes.find(function (route, i) { | ||
debug('Trying to match route ' + i); | ||
return route.matcher(url, options, request); | ||
@@ -269,3 +338,3 @@ }); | ||
if (!func) { | ||
throw new Error('fetch-mock: Falling back to network only available on gloabl fetch-mock, or by setting config.fetch on sandboxed fetch-mock'); | ||
throw new Error('fetch-mock: Falling back to network only available on global fetch-mock, or by setting config.fetch on sandboxed fetch-mock'); | ||
} | ||
@@ -282,2 +351,9 @@ return func; | ||
debug('Recording fetch call', { | ||
url: url, | ||
options: options, | ||
request: request, | ||
isUnmatched: isUnmatched, | ||
identifier: identifier | ||
}); | ||
var args = [url, options]; | ||
@@ -284,0 +360,0 @@ args.request = request; |
@@ -17,2 +17,6 @@ 'use strict'; | ||
var _require = require('./debug'), | ||
debug = _require.debug, | ||
setDebugNamespace = _require.setDebugNamespace; | ||
var _glob = require('glob-to-regexp'); | ||
@@ -22,37 +26,44 @@ var pathToRegexp = require('path-to-regexp'); | ||
var _require = require('./request-utils'), | ||
headerUtils = _require.headers, | ||
getPath = _require.getPath, | ||
getQuery = _require.getQuery, | ||
normalizeUrl = _require.normalizeUrl; | ||
var _require2 = require('./request-utils'), | ||
headerUtils = _require2.headers, | ||
getPath = _require2.getPath, | ||
getQuery = _require2.getQuery, | ||
normalizeUrl = _require2.normalizeUrl; | ||
var isEqual = require('lodash.isequal'); | ||
var debuggableUrlFunc = function debuggableUrlFunc(func) { | ||
return function (url) { | ||
debug('Actual url:', url); | ||
return func(url); | ||
}; | ||
}; | ||
var stringMatchers = { | ||
begin: function begin(targetString) { | ||
return function (url) { | ||
return debuggableUrlFunc(function (url) { | ||
return url.indexOf(targetString) === 0; | ||
}; | ||
}); | ||
}, | ||
end: function end(targetString) { | ||
return function (url) { | ||
return debuggableUrlFunc(function (url) { | ||
return url.substr(-targetString.length) === targetString; | ||
}; | ||
}); | ||
}, | ||
glob: function glob(targetString) { | ||
var urlRX = _glob(targetString); | ||
return function (url) { | ||
return debuggableUrlFunc(function (url) { | ||
return urlRX.test(url); | ||
}; | ||
}); | ||
}, | ||
express: function express(targetString) { | ||
var urlRX = pathToRegexp(targetString); | ||
return function (url) { | ||
return debuggableUrlFunc(function (url) { | ||
return urlRX.test(getPath(url)); | ||
}; | ||
}); | ||
}, | ||
path: function path(targetString) { | ||
return function (url) { | ||
return debuggableUrlFunc(function (url) { | ||
return getPath(url) === targetString; | ||
}; | ||
}); | ||
} | ||
@@ -64,3 +75,9 @@ }; | ||
debug('Generating header matcher'); | ||
if (!expectedHeaders) { | ||
debug(' No header expectations defined - skipping'); | ||
return; | ||
} | ||
var expectation = headerUtils.toLowerCase(expectedHeaders); | ||
debug(' Expected headers:', expectation); | ||
return function (url, _ref2) { | ||
@@ -70,4 +87,6 @@ var _ref2$headers = _ref2.headers, | ||
debug('Attempting to match headers'); | ||
var lowerCaseHeaders = headerUtils.toLowerCase(headerUtils.normalize(headers)); | ||
debug(' Expected headers:', expectation); | ||
debug(' Actual headers:', lowerCaseHeaders); | ||
return (0, _keys2.default)(expectation).every(function (headerName) { | ||
@@ -82,5 +101,16 @@ return headerUtils.equal(lowerCaseHeaders[headerName], expectation[headerName]); | ||
debug('Generating method matcher'); | ||
if (!expectedMethod) { | ||
debug(' No method expectations defined - skipping'); | ||
return; | ||
} | ||
debug(' Expected method:', expectedMethod); | ||
return function (url, _ref4) { | ||
var method = _ref4.method; | ||
return expectedMethod === (method ? method.toLowerCase() : 'get'); | ||
debug('Attempting to match method'); | ||
var actualMethod = method ? method.toLowerCase() : 'get'; | ||
debug(' Expected method:', expectedMethod); | ||
debug(' Actual method:', actualMethod); | ||
return expectedMethod === actualMethod; | ||
}; | ||
@@ -92,5 +122,14 @@ }; | ||
debug('Generating query parameters matcher'); | ||
if (!expectedQuery) { | ||
debug(' No query parameters expectations defined - skipping'); | ||
return; | ||
} | ||
debug(' Expected query parameters:', expectedQuery); | ||
var keys = (0, _keys2.default)(expectedQuery); | ||
return function (url) { | ||
debug('Attempting to match query parameters'); | ||
var query = querystring.parse(getQuery(url)); | ||
debug(' Expected query parameters:', expectedQuery); | ||
debug(' Actual query parameters:', query); | ||
return keys.every(function (key) { | ||
@@ -104,11 +143,18 @@ return query[key] === expectedQuery[key]; | ||
var expectedParams = _ref6.params, | ||
matcheUrl = _ref6.url; | ||
matcherUrl = _ref6.url; | ||
if (!/express:/.test(matcheUrl)) { | ||
debug('Generating path parameters matcher'); | ||
if (!expectedParams) { | ||
debug(' No path parameters expectations defined - skipping'); | ||
return; | ||
} | ||
if (!/express:/.test(matcherUrl)) { | ||
throw new Error('fetch-mock: matching on params is only possible when using an express: matcher'); | ||
} | ||
debug(' Expected path parameters:', expectedParams); | ||
var expectedKeys = (0, _keys2.default)(expectedParams); | ||
var keys = []; | ||
var re = pathToRegexp(matcheUrl.replace(/^express:/, ''), keys); | ||
var re = pathToRegexp(matcherUrl.replace(/^express:/, ''), keys); | ||
return function (url) { | ||
debug('Attempting to match path parameters'); | ||
var vals = re.exec(getPath(url)) || []; | ||
@@ -120,2 +166,4 @@ vals.shift(); | ||
}, {}); | ||
debug(' Expected path parameters:', expectedParams); | ||
debug(' Actual path parameters:', params); | ||
return expectedKeys.every(function (key) { | ||
@@ -130,2 +178,3 @@ return params[key] === expectedParams[key]; | ||
debug('Generating body matcher'); | ||
return function (url, _ref9) { | ||
@@ -136,3 +185,5 @@ var body = _ref9.body, | ||
debug('Attempting to match body'); | ||
if (method.toLowerCase() === 'get') { | ||
debug(' GET request - skip matching body'); | ||
// GET requests don’t send a body so the body matcher should be ignored for them | ||
@@ -145,4 +196,9 @@ return true; | ||
try { | ||
debug(' Parsing request body as JSON'); | ||
sentBody = JSON.parse(body); | ||
} catch (_) {} | ||
} catch (err) { | ||
debug(' Failed to parse request body as JSON', err); | ||
} | ||
debug('Expected body:', expectedBody); | ||
debug('Actual body:', sentBody); | ||
@@ -158,4 +214,7 @@ return sentBody && isEqual(sentBody, expectedBody); | ||
// from http://it.at.there/ once we start generating Request/Url objects | ||
debug(' Matching using full url', matcherUrl); | ||
var expectedUrl = normalizeUrl(matcherUrl); | ||
debug(' Normalised url to:', matcherUrl); | ||
if (route.identifier === matcherUrl) { | ||
debug(' Updating route identifier to match normalized url:', matcherUrl); | ||
route.identifier = expectedUrl; | ||
@@ -165,3 +224,6 @@ } | ||
return function (matcherUrl) { | ||
debug('Expected url:', expectedUrl); | ||
debug('Actual url:', matcherUrl); | ||
if (query && expectedUrl.indexOf('?')) { | ||
debug('Ignoring query string when matching url'); | ||
return matcherUrl.indexOf(expectedUrl) === 0; | ||
@@ -175,6 +237,16 @@ } | ||
var functionMatcher = _ref10.functionMatcher; | ||
return functionMatcher; | ||
debug('Detected user defined function matcher', functionMatcher); | ||
return function () { | ||
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { | ||
args[_key] = arguments[_key]; | ||
} | ||
debug('Calling function matcher with arguments', args); | ||
return functionMatcher.apply(undefined, args); | ||
}; | ||
}; | ||
var getUrlMatcher = function getUrlMatcher(route) { | ||
debug('Generating url matcher'); | ||
var matcherUrl = route.url, | ||
@@ -185,2 +257,3 @@ query = route.query; | ||
if (matcherUrl === '*') { | ||
debug(' Using universal * rule to match any url'); | ||
return function () { | ||
@@ -192,2 +265,3 @@ return true; | ||
if (matcherUrl instanceof RegExp) { | ||
debug(' Using regular expression to match url:', matcherUrl); | ||
return function (url) { | ||
@@ -199,2 +273,3 @@ return matcherUrl.test(url); | ||
if (matcherUrl.href) { | ||
debug(' Using URL object to match url', matcherUrl); | ||
return getFullUrlMatcher(route, matcherUrl.href, query); | ||
@@ -205,2 +280,3 @@ } | ||
if (matcherUrl.indexOf(shorthand + ':') === 0) { | ||
debug(' Using ' + shorthand + ': pattern to match url', matcherUrl); | ||
var urlFragment = matcherUrl.replace(new RegExp('^' + shorthand + ':'), ''); | ||
@@ -215,2 +291,4 @@ return stringMatchers[shorthand](urlFragment); | ||
module.exports = function (route) { | ||
setDebugNamespace('generateMatcher()'); | ||
debug('Compiling matcher for route'); | ||
var matchers = [route.query && getQueryStringMatcher(route), route.method && getMethodMatcher(route), route.headers && getHeaderMatcher(route), route.params && getParamsMatcher(route), route.body && getBodyMatcher(route), route.functionMatcher && getFunctionMatcher(route), route.url && getUrlMatcher(route)].filter(function (matcher) { | ||
@@ -220,2 +298,4 @@ return !!matcher; | ||
debug('Compiled matcher for route'); | ||
setDebugNamespace(); | ||
return function (url) { | ||
@@ -222,0 +302,0 @@ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; |
@@ -13,2 +13,5 @@ 'use strict'; | ||
var _require = require('./debug'), | ||
debug = _require.debug; | ||
var setUpAndTearDown = require('./set-up-and-tear-down'); | ||
@@ -29,2 +32,3 @@ var fetchHandler = require('./fetch-handler'); | ||
FetchMock.createInstance = function () { | ||
debug('Creating fetch-mock instance'); | ||
var instance = (0, _create2.default)(FetchMock); | ||
@@ -51,2 +55,3 @@ instance._uncompiledRoutes = (this._uncompiledRoutes || []).slice(); | ||
FetchMock.sandbox = function () { | ||
debug('Creating sandboxed fetch-mock instance'); | ||
// this construct allows us to create a fetch-mock instance which is also | ||
@@ -53,0 +58,0 @@ // a callable function, while circumventing circularity when defining the |
'use strict'; | ||
var _regenerator = require('babel-runtime/regenerator'); | ||
var _regenerator2 = _interopRequireDefault(_regenerator); | ||
var _promise = require('babel-runtime/core-js/promise'); | ||
@@ -7,2 +11,6 @@ | ||
var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator'); | ||
var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); | ||
var _toConsumableArray2 = require('babel-runtime/helpers/toConsumableArray'); | ||
@@ -22,9 +30,14 @@ | ||
var _require = require('./request-utils'), | ||
normalizeUrl = _require.normalizeUrl; | ||
var _require = require('./debug'), | ||
setDebugPhase = _require.setDebugPhase, | ||
setDebugNamespace = _require.setDebugNamespace, | ||
debug = _require.debug; | ||
var _require2 = require('./request-utils'), | ||
normalizeUrl = _require2.normalizeUrl; | ||
var FetchMock = {}; | ||
var _require2 = require('./compile-route'), | ||
sanitizeRoute = _require2.sanitizeRoute; | ||
var _require3 = require('./compile-route'), | ||
sanitizeRoute = _require3.sanitizeRoute; | ||
@@ -50,3 +63,18 @@ var generateMatcher = require('./generate-matcher'); | ||
var formatDebug = function formatDebug(func) { | ||
return function () { | ||
setDebugPhase('inspect'); | ||
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { | ||
args[_key] = arguments[_key]; | ||
} | ||
var result = func.call.apply(func, [this].concat(args)); | ||
setDebugPhase(); | ||
return result; | ||
}; | ||
}; | ||
FetchMock.filterCalls = function (nameOrMatcher, options) { | ||
debug('Filtering fetch calls'); | ||
var calls = this._calls; | ||
@@ -56,2 +84,3 @@ var matcher = '*'; | ||
if ([true, 'matched'].includes(nameOrMatcher)) { | ||
debug('Filter provided is ' + nameOrMatcher + '. Returning matched calls only'); | ||
calls = calls.filter(function (_ref3) { | ||
@@ -62,2 +91,3 @@ var isUnmatched = _ref3.isUnmatched; | ||
} else if ([false, 'unmatched'].includes(nameOrMatcher)) { | ||
debug('Filter provided is ' + nameOrMatcher + '. Returning unmatched calls only'); | ||
calls = calls.filter(function (_ref4) { | ||
@@ -68,4 +98,6 @@ var isUnmatched = _ref4.isUnmatched; | ||
} else if (typeof nameOrMatcher === 'undefined') { | ||
debug('Filter provided is undefined. Returning all calls'); | ||
calls = calls; | ||
} else if (isName(nameOrMatcher)) { | ||
debug('Filter provided, looks like the name of a named route. Returning only calls handled by that route'); | ||
calls = calls.filter(function (_ref5) { | ||
@@ -81,2 +113,3 @@ var identifier = _ref5.identifier; | ||
})) { | ||
debug('Filter provided, ' + nameOrMatcher + ', identifies a route. Returning only calls handled by that route'); | ||
calls = calls.filter(function (call) { | ||
@@ -92,51 +125,106 @@ return call.identifier === matcher; | ||
} | ||
debug('Compiling filter and options to route in order to filter all calls', nameOrMatcher); | ||
calls = filterCallsWithMatcher(matcher, options, calls); | ||
} | ||
debug('Retrieved ' + calls.length + ' calls'); | ||
return calls; | ||
}; | ||
FetchMock.calls = function (nameOrMatcher, options) { | ||
FetchMock.calls = formatDebug(function (nameOrMatcher, options) { | ||
debug('retrieving matching calls'); | ||
return this.filterCalls(nameOrMatcher, options); | ||
}; | ||
}); | ||
FetchMock.lastCall = function (nameOrMatcher, options) { | ||
FetchMock.lastCall = formatDebug(function (nameOrMatcher, options) { | ||
debug('retrieving last matching call'); | ||
return [].concat((0, _toConsumableArray3.default)(this.filterCalls(nameOrMatcher, options))).pop(); | ||
}; | ||
}); | ||
FetchMock.lastUrl = function (nameOrMatcher, options) { | ||
FetchMock.lastUrl = formatDebug(function (nameOrMatcher, options) { | ||
debug('retrieving url of last matching call'); | ||
return (this.lastCall(nameOrMatcher, options) || [])[0]; | ||
}; | ||
}); | ||
FetchMock.lastOptions = function (nameOrMatcher, options) { | ||
FetchMock.lastOptions = formatDebug(function (nameOrMatcher, options) { | ||
debug('retrieving options of last matching call'); | ||
return (this.lastCall(nameOrMatcher, options) || [])[1]; | ||
}; | ||
}); | ||
FetchMock.called = function (nameOrMatcher, options) { | ||
FetchMock.called = formatDebug(function (nameOrMatcher, options) { | ||
debug('checking if matching call was made'); | ||
return !!this.filterCalls(nameOrMatcher, options).length; | ||
}; | ||
}); | ||
FetchMock.flush = function (waitForResponseMethods) { | ||
var _this = this; | ||
FetchMock.flush = formatDebug(function () { | ||
var _ref7 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee(waitForResponseMethods) { | ||
var queuedPromises; | ||
return _regenerator2.default.wrap(function _callee$(_context) { | ||
while (1) { | ||
switch (_context.prev = _context.next) { | ||
case 0: | ||
setDebugNamespace('flush'); | ||
debug('flushing all fetch calls. ' + (waitForResponseMethods ? '' : 'Not ') + 'waiting for response bodies to complete download'); | ||
var queuedPromises = this._holdingPromises; | ||
this._holdingPromises = []; | ||
queuedPromises = this._holdingPromises; | ||
return _promise2.default.all(queuedPromises).then(function () { | ||
if (waitForResponseMethods && _this._holdingPromises.length) { | ||
return _this.flush(waitForResponseMethods); | ||
} | ||
}); | ||
}; | ||
this._holdingPromises = []; | ||
debug(queuedPromises.length + ' fetch calls to be awaited'); | ||
FetchMock.done = function (nameOrMatcher) { | ||
var _this2 = this; | ||
_context.next = 7; | ||
return _promise2.default.all(queuedPromises); | ||
var routesToCheck = nameOrMatcher && typeof nameOrMatcher !== 'boolean' ? [{ identifier: nameOrMatcher }] : this.routes; | ||
case 7: | ||
debug('All fetch calls have completed'); | ||
if (!(waitForResponseMethods && this._holdingPromises.length)) { | ||
_context.next = 13; | ||
break; | ||
} | ||
debug('Awaiting all fetch bodies to download'); | ||
_context.next = 12; | ||
return this.flush(waitForResponseMethods); | ||
case 12: | ||
debug('All fetch bodies have completed downloading'); | ||
case 13: | ||
setDebugNamespace(); | ||
case 14: | ||
case 'end': | ||
return _context.stop(); | ||
} | ||
} | ||
}, _callee, this); | ||
})); | ||
return function (_x2) { | ||
return _ref7.apply(this, arguments); | ||
}; | ||
}()); | ||
FetchMock.done = formatDebug(function (nameOrMatcher) { | ||
var _this = this; | ||
setDebugPhase('inspect'); | ||
setDebugNamespace('done'); | ||
debug('Checking to see if expected calls have been made'); | ||
var routesToCheck = void 0; | ||
if (nameOrMatcher && typeof nameOrMatcher !== 'boolean') { | ||
debug('Checking to see if expected calls have been made for single route:', nameOrMatcher); | ||
routesToCheck = [{ identifier: nameOrMatcher }]; | ||
} else { | ||
debug('Checking to see if expected calls have been made for all routes'); | ||
routesToCheck = this.routes; | ||
} | ||
// Can't use array.every because would exit after first failure, which would | ||
// break the logging | ||
return routesToCheck.map(function (_ref7) { | ||
var identifier = _ref7.identifier; | ||
var result = routesToCheck.map(function (_ref8) { | ||
var identifier = _ref8.identifier; | ||
if (!_this2.called(identifier)) { | ||
if (!_this.called(identifier)) { | ||
debug('No calls made for route:', identifier); | ||
console.warn('Warning: ' + identifier + ' not called'); // eslint-disable-line | ||
@@ -146,3 +234,3 @@ return false; | ||
var expectedTimes = (_this2.routes.find(function (r) { | ||
var expectedTimes = (_this.routes.find(function (r) { | ||
return r.identifier === identifier; | ||
@@ -152,6 +240,10 @@ }) || {}).repeat; | ||
if (!expectedTimes) { | ||
debug('Route has been called at least once, and no expectation of more set:', identifier); | ||
return true; | ||
} | ||
var actualTimes = _this2.filterCalls(identifier).length; | ||
var actualTimes = _this.filterCalls(identifier).length; | ||
debug('Route called ' + actualTimes + ' times:', identifier); | ||
if (expectedTimes > actualTimes) { | ||
debug('Route called ' + actualTimes + ' times, but expected ' + expectedTimes + ':', identifier); | ||
console.warn('Warning: ' + identifier + ' only called ' + actualTimes + ' times, but ' + expectedTimes + ' expected'); // eslint-disable-line | ||
@@ -165,4 +257,8 @@ return false; | ||
}); | ||
}; | ||
setDebugNamespace(); | ||
setDebugPhase(); | ||
return result; | ||
}); | ||
module.exports = FetchMock; |
@@ -29,2 +29,5 @@ 'use strict'; | ||
var _require = require('./debug'), | ||
getDebug = _require.getDebug; | ||
var responseConfigProps = ['body', 'headers', 'throws', 'status', 'redirectUrl']; | ||
@@ -36,2 +39,4 @@ | ||
this.debug = getDebug('ResponseBuilder()'); | ||
this.debug('Response builder created with options', options); | ||
(0, _assign2.default)(this, options); | ||
@@ -43,2 +48,3 @@ } | ||
value: function exec() { | ||
this.debug('building response'); | ||
this.normalizeResponseConfig(); | ||
@@ -73,2 +79,3 @@ this.constructFetchOpts(); | ||
if (typeof this.responseConfig === 'number') { | ||
this.debug('building response using status', this.responseConfig); | ||
this.responseConfig = { | ||
@@ -80,2 +87,3 @@ status: this.responseConfig | ||
} else if (typeof this.responseConfig === 'string' || this.sendAsObject()) { | ||
this.debug('building text response from', this.responseConfig); | ||
this.responseConfig = { | ||
@@ -90,2 +98,3 @@ body: this.responseConfig | ||
if (!status) { | ||
this.debug('No status provided. Defaulting to 200'); | ||
return 200; | ||
@@ -95,2 +104,3 @@ } | ||
if (typeof status === 'number' && parseInt(status, 10) !== status && status >= 200 || status < 600) { | ||
this.debug('Valid status provided', status); | ||
return status; | ||
@@ -124,2 +134,3 @@ } | ||
(0, _typeof3.default)(this.body) === 'object') { | ||
this.debug('Stringifying JSON response body'); | ||
this.body = (0, _stringify2.default)(this.body); | ||
@@ -136,2 +147,3 @@ if (!this.options.headers.has('Content-Type')) { | ||
if (this.getOption('includeContentLength') && typeof this.body === 'string' && !this.options.headers.has('Content-Length')) { | ||
this.debug('Setting content-length header:', this.body.length.toString()); | ||
this.options.headers.set('Content-Length', this.body.length.toString()); | ||
@@ -151,2 +163,3 @@ } | ||
if (this.Stream) { | ||
this.debug('Creating response stream'); | ||
var stream = new this.Stream.Readable(); | ||
@@ -172,2 +185,3 @@ if (this.body != null) { | ||
// promises returned by res.json(), res.text() etc | ||
this.debug('Wrappipng Response in ES proxy for observability'); | ||
return new Proxy(response, { | ||
@@ -177,2 +191,3 @@ get: function get(originalResponse, name) { | ||
if (name === 'url') { | ||
_this2.debug('Retrieving redirect url', _this2.responseConfig.redirectUrl); | ||
return _this2.responseConfig.redirectUrl; | ||
@@ -182,2 +197,3 @@ } | ||
if (name === 'redirected') { | ||
_this2.debug('Retrieving redirected status', true); | ||
return true; | ||
@@ -188,4 +204,6 @@ } | ||
if (typeof originalResponse[name] === 'function') { | ||
_this2.debug('Wrapping body promises in ES proxies for observability'); | ||
return new Proxy(originalResponse[name], { | ||
apply: function apply(func, thisArg, args) { | ||
_this2.debug('Calling res.' + name); | ||
var result = func.apply(response, args); | ||
@@ -192,0 +210,0 @@ if (result.then) { |
@@ -9,8 +9,14 @@ 'use strict'; | ||
var _require = require('./compile-route'), | ||
compileRoute = _require.compileRoute; | ||
var _require = require('./debug'), | ||
debug = _require.debug, | ||
setDebugPhase = _require.setDebugPhase; | ||
var _require2 = require('./compile-route'), | ||
compileRoute = _require2.compileRoute; | ||
var FetchMock = {}; | ||
FetchMock.mock = function () { | ||
setDebugPhase('setup'); | ||
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { | ||
@@ -30,2 +36,3 @@ args[_key] = arguments[_key]; | ||
debug('Adding route', uncompiledRoute); | ||
var route = this.compileRoute(uncompiledRoute); | ||
@@ -51,3 +58,2 @@ var clashes = this.routes.filter(function (_ref) { | ||
}); | ||
return this.routes; | ||
@@ -70,2 +76,3 @@ } | ||
} | ||
setDebugPhase(); | ||
return this; | ||
@@ -72,0 +79,0 @@ }; |
{ | ||
"name": "fetch-mock", | ||
"type": "module", | ||
"version": "8.3.2", | ||
"version": "9.0.0-beta.2", | ||
"description": "Mock http requests made using fetch (or isomorphic-fetch)", | ||
"main": "./cjs/server.js", | ||
"browser": "./esm/client.mjs", | ||
"module": "./esm/server.mjs", | ||
"browser": "./esm/client.js", | ||
"module": "./esm/server.js", | ||
"types": "./types/index.d.ts", | ||
@@ -58,2 +58,3 @@ "scripts": { | ||
"core-js": "^3.0.0", | ||
"debug": "^4.1.1", | ||
"glob-to-regexp": "^0.4.0", | ||
@@ -60,0 +61,0 @@ "lodash.isequal": "^4.5.0", |
@@ -5,3 +5,3 @@ # fetch-mock | ||
*New* If using jest, try the new [fetch-mock-jest](https://www.npmjs.com/package/fetch-mock-jest) wrapper. | ||
_New_ If using jest, try the new [fetch-mock-jest](https://www.npmjs.com/package/fetch-mock-jest) wrapper. | ||
@@ -8,0 +8,0 @@ ![node version](https://img.shields.io/node/v/fetch-mock.svg?style=flat-square) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 5 instances in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
2741628
38
78422
9
1
8
+ Addeddebug@^4.1.1
+ Addeddebug@4.3.5(transitive)
+ Addedms@2.1.2(transitive)