Spaces:
Runtime error
Runtime error
| const { MockNotMatchedError } = require('./mock-errors') | |
| const { | |
| kDispatches, | |
| kMockAgent, | |
| kOriginalDispatch, | |
| kOrigin, | |
| kGetNetConnect | |
| } = require('./mock-symbols') | |
| const { buildURL, nop } = require('../core/util') | |
| const { STATUS_CODES } = require('http') | |
| const { | |
| types: { | |
| isPromise | |
| } | |
| } = require('util') | |
| function matchValue (match, value) { | |
| if (typeof match === 'string') { | |
| return match === value | |
| } | |
| if (match instanceof RegExp) { | |
| return match.test(value) | |
| } | |
| if (typeof match === 'function') { | |
| return match(value) === true | |
| } | |
| return false | |
| } | |
| function lowerCaseEntries (headers) { | |
| return Object.fromEntries( | |
| Object.entries(headers).map(([headerName, headerValue]) => { | |
| return [headerName.toLocaleLowerCase(), headerValue] | |
| }) | |
| ) | |
| } | |
| /** | |
| * @param {import('../../index').Headers|string[]|Record<string, string>} headers | |
| * @param {string} key | |
| */ | |
| function getHeaderByName (headers, key) { | |
| if (Array.isArray(headers)) { | |
| for (let i = 0; i < headers.length; i += 2) { | |
| if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) { | |
| return headers[i + 1] | |
| } | |
| } | |
| return undefined | |
| } else if (typeof headers.get === 'function') { | |
| return headers.get(key) | |
| } else { | |
| return lowerCaseEntries(headers)[key.toLocaleLowerCase()] | |
| } | |
| } | |
| /** @param {string[]} headers */ | |
| function buildHeadersFromArray (headers) { // fetch HeadersList | |
| const clone = headers.slice() | |
| const entries = [] | |
| for (let index = 0; index < clone.length; index += 2) { | |
| entries.push([clone[index], clone[index + 1]]) | |
| } | |
| return Object.fromEntries(entries) | |
| } | |
| function matchHeaders (mockDispatch, headers) { | |
| if (typeof mockDispatch.headers === 'function') { | |
| if (Array.isArray(headers)) { // fetch HeadersList | |
| headers = buildHeadersFromArray(headers) | |
| } | |
| return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {}) | |
| } | |
| if (typeof mockDispatch.headers === 'undefined') { | |
| return true | |
| } | |
| if (typeof headers !== 'object' || typeof mockDispatch.headers !== 'object') { | |
| return false | |
| } | |
| for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) { | |
| const headerValue = getHeaderByName(headers, matchHeaderName) | |
| if (!matchValue(matchHeaderValue, headerValue)) { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| function safeUrl (path) { | |
| if (typeof path !== 'string') { | |
| return path | |
| } | |
| const pathSegments = path.split('?') | |
| if (pathSegments.length !== 2) { | |
| return path | |
| } | |
| const qp = new URLSearchParams(pathSegments.pop()) | |
| qp.sort() | |
| return [...pathSegments, qp.toString()].join('?') | |
| } | |
| function matchKey (mockDispatch, { path, method, body, headers }) { | |
| const pathMatch = matchValue(mockDispatch.path, path) | |
| const methodMatch = matchValue(mockDispatch.method, method) | |
| const bodyMatch = typeof mockDispatch.body !== 'undefined' ? matchValue(mockDispatch.body, body) : true | |
| const headersMatch = matchHeaders(mockDispatch, headers) | |
| return pathMatch && methodMatch && bodyMatch && headersMatch | |
| } | |
| function getResponseData (data) { | |
| if (Buffer.isBuffer(data)) { | |
| return data | |
| } else if (typeof data === 'object') { | |
| return JSON.stringify(data) | |
| } else { | |
| return data.toString() | |
| } | |
| } | |
| function getMockDispatch (mockDispatches, key) { | |
| const basePath = key.query ? buildURL(key.path, key.query) : key.path | |
| const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath | |
| // Match path | |
| let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(safeUrl(path), resolvedPath)) | |
| if (matchedMockDispatches.length === 0) { | |
| throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`) | |
| } | |
| // Match method | |
| matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method)) | |
| if (matchedMockDispatches.length === 0) { | |
| throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}'`) | |
| } | |
| // Match body | |
| matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== 'undefined' ? matchValue(body, key.body) : true) | |
| if (matchedMockDispatches.length === 0) { | |
| throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}'`) | |
| } | |
| // Match headers | |
| matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers)) | |
| if (matchedMockDispatches.length === 0) { | |
| throw new MockNotMatchedError(`Mock dispatch not matched for headers '${typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers}'`) | |
| } | |
| return matchedMockDispatches[0] | |
| } | |
| function addMockDispatch (mockDispatches, key, data) { | |
| const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false } | |
| const replyData = typeof data === 'function' ? { callback: data } : { ...data } | |
| const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } } | |
| mockDispatches.push(newMockDispatch) | |
| return newMockDispatch | |
| } | |
| function deleteMockDispatch (mockDispatches, key) { | |
| const index = mockDispatches.findIndex(dispatch => { | |
| if (!dispatch.consumed) { | |
| return false | |
| } | |
| return matchKey(dispatch, key) | |
| }) | |
| if (index !== -1) { | |
| mockDispatches.splice(index, 1) | |
| } | |
| } | |
| function buildKey (opts) { | |
| const { path, method, body, headers, query } = opts | |
| return { | |
| path, | |
| method, | |
| body, | |
| headers, | |
| query | |
| } | |
| } | |
| function generateKeyValues (data) { | |
| return Object.entries(data).reduce((keyValuePairs, [key, value]) => [ | |
| ...keyValuePairs, | |
| Buffer.from(`${key}`), | |
| Array.isArray(value) ? value.map(x => Buffer.from(`${x}`)) : Buffer.from(`${value}`) | |
| ], []) | |
| } | |
| /** | |
| * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status | |
| * @param {number} statusCode | |
| */ | |
| function getStatusText (statusCode) { | |
| return STATUS_CODES[statusCode] || 'unknown' | |
| } | |
| async function getResponse (body) { | |
| const buffers = [] | |
| for await (const data of body) { | |
| buffers.push(data) | |
| } | |
| return Buffer.concat(buffers).toString('utf8') | |
| } | |
| /** | |
| * Mock dispatch function used to simulate undici dispatches | |
| */ | |
| function mockDispatch (opts, handler) { | |
| // Get mock dispatch from built key | |
| const key = buildKey(opts) | |
| const mockDispatch = getMockDispatch(this[kDispatches], key) | |
| mockDispatch.timesInvoked++ | |
| // Here's where we resolve a callback if a callback is present for the dispatch data. | |
| if (mockDispatch.data.callback) { | |
| mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) } | |
| } | |
| // Parse mockDispatch data | |
| const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch | |
| const { timesInvoked, times } = mockDispatch | |
| // If it's used up and not persistent, mark as consumed | |
| mockDispatch.consumed = !persist && timesInvoked >= times | |
| mockDispatch.pending = timesInvoked < times | |
| // If specified, trigger dispatch error | |
| if (error !== null) { | |
| deleteMockDispatch(this[kDispatches], key) | |
| handler.onError(error) | |
| return true | |
| } | |
| // Handle the request with a delay if necessary | |
| if (typeof delay === 'number' && delay > 0) { | |
| setTimeout(() => { | |
| handleReply(this[kDispatches]) | |
| }, delay) | |
| } else { | |
| handleReply(this[kDispatches]) | |
| } | |
| function handleReply (mockDispatches, _data = data) { | |
| // fetch's HeadersList is a 1D string array | |
| const optsHeaders = Array.isArray(opts.headers) | |
| ? buildHeadersFromArray(opts.headers) | |
| : opts.headers | |
| const body = typeof _data === 'function' | |
| ? _data({ ...opts, headers: optsHeaders }) | |
| : _data | |
| // util.types.isPromise is likely needed for jest. | |
| if (isPromise(body)) { | |
| // If handleReply is asynchronous, throwing an error | |
| // in the callback will reject the promise, rather than | |
| // synchronously throw the error, which breaks some tests. | |
| // Rather, we wait for the callback to resolve if it is a | |
| // promise, and then re-run handleReply with the new body. | |
| body.then((newData) => handleReply(mockDispatches, newData)) | |
| return | |
| } | |
| const responseData = getResponseData(body) | |
| const responseHeaders = generateKeyValues(headers) | |
| const responseTrailers = generateKeyValues(trailers) | |
| handler.abort = nop | |
| handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode)) | |
| handler.onData(Buffer.from(responseData)) | |
| handler.onComplete(responseTrailers) | |
| deleteMockDispatch(mockDispatches, key) | |
| } | |
| function resume () {} | |
| return true | |
| } | |
| function buildMockDispatch () { | |
| const agent = this[kMockAgent] | |
| const origin = this[kOrigin] | |
| const originalDispatch = this[kOriginalDispatch] | |
| return function dispatch (opts, handler) { | |
| if (agent.isMockActive) { | |
| try { | |
| mockDispatch.call(this, opts, handler) | |
| } catch (error) { | |
| if (error instanceof MockNotMatchedError) { | |
| const netConnect = agent[kGetNetConnect]() | |
| if (netConnect === false) { | |
| throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`) | |
| } | |
| if (checkNetConnect(netConnect, origin)) { | |
| originalDispatch.call(this, opts, handler) | |
| } else { | |
| throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`) | |
| } | |
| } else { | |
| throw error | |
| } | |
| } | |
| } else { | |
| originalDispatch.call(this, opts, handler) | |
| } | |
| } | |
| } | |
| function checkNetConnect (netConnect, origin) { | |
| const url = new URL(origin) | |
| if (netConnect === true) { | |
| return true | |
| } else if (Array.isArray(netConnect) && netConnect.some((matcher) => matchValue(matcher, url.host))) { | |
| return true | |
| } | |
| return false | |
| } | |
| function buildMockOptions (opts) { | |
| if (opts) { | |
| const { agent, ...mockOptions } = opts | |
| return mockOptions | |
| } | |
| } | |
| module.exports = { | |
| getResponseData, | |
| getMockDispatch, | |
| addMockDispatch, | |
| deleteMockDispatch, | |
| buildKey, | |
| generateKeyValues, | |
| matchValue, | |
| getResponse, | |
| getStatusText, | |
| mockDispatch, | |
| buildMockDispatch, | |
| checkNetConnect, | |
| buildMockOptions, | |
| getHeaderByName | |
| } | |