123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- 'use strict'
- 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
- }
|