diff options
Diffstat (limited to 'vanilla/node_modules/undici/lib/mock')
12 files changed, 0 insertions, 2507 deletions
diff --git a/vanilla/node_modules/undici/lib/mock/mock-agent.js b/vanilla/node_modules/undici/lib/mock/mock-agent.js deleted file mode 100644 index 61449e0..0000000 --- a/vanilla/node_modules/undici/lib/mock/mock-agent.js +++ /dev/null @@ -1,232 +0,0 @@ -'use strict' - -const { kClients } = require('../core/symbols') -const Agent = require('../dispatcher/agent') -const { - kAgent, - kMockAgentSet, - kMockAgentGet, - kDispatches, - kIsMockActive, - kNetConnect, - kGetNetConnect, - kOptions, - kFactory, - kMockAgentRegisterCallHistory, - kMockAgentIsCallHistoryEnabled, - kMockAgentAddCallHistoryLog, - kMockAgentMockCallHistoryInstance, - kMockAgentAcceptsNonStandardSearchParameters, - kMockCallHistoryAddLog, - kIgnoreTrailingSlash -} = require('./mock-symbols') -const MockClient = require('./mock-client') -const MockPool = require('./mock-pool') -const { matchValue, normalizeSearchParams, buildAndValidateMockOptions, normalizeOrigin } = require('./mock-utils') -const { InvalidArgumentError, UndiciError } = require('../core/errors') -const Dispatcher = require('../dispatcher/dispatcher') -const PendingInterceptorsFormatter = require('./pending-interceptors-formatter') -const { MockCallHistory } = require('./mock-call-history') - -class MockAgent extends Dispatcher { - constructor (opts = {}) { - super(opts) - - const mockOptions = buildAndValidateMockOptions(opts) - - this[kNetConnect] = true - this[kIsMockActive] = true - this[kMockAgentIsCallHistoryEnabled] = mockOptions.enableCallHistory ?? false - this[kMockAgentAcceptsNonStandardSearchParameters] = mockOptions.acceptNonStandardSearchParameters ?? false - this[kIgnoreTrailingSlash] = mockOptions.ignoreTrailingSlash ?? false - - // Instantiate Agent and encapsulate - if (opts?.agent && typeof opts.agent.dispatch !== 'function') { - throw new InvalidArgumentError('Argument opts.agent must implement Agent') - } - const agent = opts?.agent ? opts.agent : new Agent(opts) - this[kAgent] = agent - - this[kClients] = agent[kClients] - this[kOptions] = mockOptions - - if (this[kMockAgentIsCallHistoryEnabled]) { - this[kMockAgentRegisterCallHistory]() - } - } - - get (origin) { - // Normalize origin to handle URL objects and case-insensitive hostnames - const normalizedOrigin = normalizeOrigin(origin) - const originKey = this[kIgnoreTrailingSlash] ? normalizedOrigin.replace(/\/$/, '') : normalizedOrigin - - let dispatcher = this[kMockAgentGet](originKey) - - if (!dispatcher) { - dispatcher = this[kFactory](originKey) - this[kMockAgentSet](originKey, dispatcher) - } - return dispatcher - } - - dispatch (opts, handler) { - opts.origin = normalizeOrigin(opts.origin) - - // Call MockAgent.get to perform additional setup before dispatching as normal - this.get(opts.origin) - - this[kMockAgentAddCallHistoryLog](opts) - - const acceptNonStandardSearchParameters = this[kMockAgentAcceptsNonStandardSearchParameters] - - const dispatchOpts = { ...opts } - - if (acceptNonStandardSearchParameters && dispatchOpts.path) { - const [path, searchParams] = dispatchOpts.path.split('?') - const normalizedSearchParams = normalizeSearchParams(searchParams, acceptNonStandardSearchParameters) - dispatchOpts.path = `${path}?${normalizedSearchParams}` - } - - return this[kAgent].dispatch(dispatchOpts, handler) - } - - async close () { - this.clearCallHistory() - await this[kAgent].close() - this[kClients].clear() - } - - deactivate () { - this[kIsMockActive] = false - } - - activate () { - this[kIsMockActive] = true - } - - enableNetConnect (matcher) { - if (typeof matcher === 'string' || typeof matcher === 'function' || matcher instanceof RegExp) { - if (Array.isArray(this[kNetConnect])) { - this[kNetConnect].push(matcher) - } else { - this[kNetConnect] = [matcher] - } - } else if (typeof matcher === 'undefined') { - this[kNetConnect] = true - } else { - throw new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.') - } - } - - disableNetConnect () { - this[kNetConnect] = false - } - - enableCallHistory () { - this[kMockAgentIsCallHistoryEnabled] = true - - return this - } - - disableCallHistory () { - this[kMockAgentIsCallHistoryEnabled] = false - - return this - } - - getCallHistory () { - return this[kMockAgentMockCallHistoryInstance] - } - - clearCallHistory () { - if (this[kMockAgentMockCallHistoryInstance] !== undefined) { - this[kMockAgentMockCallHistoryInstance].clear() - } - } - - // This is required to bypass issues caused by using global symbols - see: - // https://github.com/nodejs/undici/issues/1447 - get isMockActive () { - return this[kIsMockActive] - } - - [kMockAgentRegisterCallHistory] () { - if (this[kMockAgentMockCallHistoryInstance] === undefined) { - this[kMockAgentMockCallHistoryInstance] = new MockCallHistory() - } - } - - [kMockAgentAddCallHistoryLog] (opts) { - if (this[kMockAgentIsCallHistoryEnabled]) { - // additional setup when enableCallHistory class method is used after mockAgent instantiation - this[kMockAgentRegisterCallHistory]() - - // add call history log on every call (intercepted or not) - this[kMockAgentMockCallHistoryInstance][kMockCallHistoryAddLog](opts) - } - } - - [kMockAgentSet] (origin, dispatcher) { - this[kClients].set(origin, { count: 0, dispatcher }) - } - - [kFactory] (origin) { - const mockOptions = Object.assign({ agent: this }, this[kOptions]) - return this[kOptions] && this[kOptions].connections === 1 - ? new MockClient(origin, mockOptions) - : new MockPool(origin, mockOptions) - } - - [kMockAgentGet] (origin) { - // First check if we can immediately find it - const result = this[kClients].get(origin) - if (result?.dispatcher) { - return result.dispatcher - } - - // If the origin is not a string create a dummy parent pool and return to user - if (typeof origin !== 'string') { - const dispatcher = this[kFactory]('http://localhost:9999') - this[kMockAgentSet](origin, dispatcher) - return dispatcher - } - - // If we match, create a pool and assign the same dispatches - for (const [keyMatcher, result] of Array.from(this[kClients])) { - if (result && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) { - const dispatcher = this[kFactory](origin) - this[kMockAgentSet](origin, dispatcher) - dispatcher[kDispatches] = result.dispatcher[kDispatches] - return dispatcher - } - } - } - - [kGetNetConnect] () { - return this[kNetConnect] - } - - pendingInterceptors () { - const mockAgentClients = this[kClients] - - return Array.from(mockAgentClients.entries()) - .flatMap(([origin, result]) => result.dispatcher[kDispatches].map(dispatch => ({ ...dispatch, origin }))) - .filter(({ pending }) => pending) - } - - assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) { - const pending = this.pendingInterceptors() - - if (pending.length === 0) { - return - } - - throw new UndiciError( - pending.length === 1 - ? `1 interceptor is pending:\n\n${pendingInterceptorsFormatter.format(pending)}`.trim() - : `${pending.length} interceptors are pending:\n\n${pendingInterceptorsFormatter.format(pending)}`.trim() - ) - } -} - -module.exports = MockAgent diff --git a/vanilla/node_modules/undici/lib/mock/mock-call-history.js b/vanilla/node_modules/undici/lib/mock/mock-call-history.js deleted file mode 100644 index d4a92b2..0000000 --- a/vanilla/node_modules/undici/lib/mock/mock-call-history.js +++ /dev/null @@ -1,248 +0,0 @@ -'use strict' - -const { kMockCallHistoryAddLog } = require('./mock-symbols') -const { InvalidArgumentError } = require('../core/errors') - -function handleFilterCallsWithOptions (criteria, options, handler, store) { - switch (options.operator) { - case 'OR': - store.push(...handler(criteria)) - - return store - case 'AND': - return handler.call({ logs: store }, criteria) - default: - // guard -- should never happens because buildAndValidateFilterCallsOptions is called before - throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'') - } -} - -function buildAndValidateFilterCallsOptions (options = {}) { - const finalOptions = {} - - if ('operator' in options) { - if (typeof options.operator !== 'string' || (options.operator.toUpperCase() !== 'OR' && options.operator.toUpperCase() !== 'AND')) { - throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'') - } - - return { - ...finalOptions, - operator: options.operator.toUpperCase() - } - } - - return finalOptions -} - -function makeFilterCalls (parameterName) { - return (parameterValue) => { - if (typeof parameterValue === 'string' || parameterValue == null) { - return this.logs.filter((log) => { - return log[parameterName] === parameterValue - }) - } - if (parameterValue instanceof RegExp) { - return this.logs.filter((log) => { - return parameterValue.test(log[parameterName]) - }) - } - - throw new InvalidArgumentError(`${parameterName} parameter should be one of string, regexp, undefined or null`) - } -} -function computeUrlWithMaybeSearchParameters (requestInit) { - // path can contains query url parameters - // or query can contains query url parameters - try { - const url = new URL(requestInit.path, requestInit.origin) - - // requestInit.path contains query url parameters - // requestInit.query is then undefined - if (url.search.length !== 0) { - return url - } - - // requestInit.query can be populated here - url.search = new URLSearchParams(requestInit.query).toString() - - return url - } catch (error) { - throw new InvalidArgumentError('An error occurred when computing MockCallHistoryLog.url', { cause: error }) - } -} - -class MockCallHistoryLog { - constructor (requestInit = {}) { - this.body = requestInit.body - this.headers = requestInit.headers - this.method = requestInit.method - - const url = computeUrlWithMaybeSearchParameters(requestInit) - - this.fullUrl = url.toString() - this.origin = url.origin - this.path = url.pathname - this.searchParams = Object.fromEntries(url.searchParams) - this.protocol = url.protocol - this.host = url.host - this.port = url.port - this.hash = url.hash - } - - toMap () { - return new Map([ - ['protocol', this.protocol], - ['host', this.host], - ['port', this.port], - ['origin', this.origin], - ['path', this.path], - ['hash', this.hash], - ['searchParams', this.searchParams], - ['fullUrl', this.fullUrl], - ['method', this.method], - ['body', this.body], - ['headers', this.headers]] - ) - } - - toString () { - const options = { betweenKeyValueSeparator: '->', betweenPairSeparator: '|' } - let result = '' - - this.toMap().forEach((value, key) => { - if (typeof value === 'string' || value === undefined || value === null) { - result = `${result}${key}${options.betweenKeyValueSeparator}${value}${options.betweenPairSeparator}` - } - if ((typeof value === 'object' && value !== null) || Array.isArray(value)) { - result = `${result}${key}${options.betweenKeyValueSeparator}${JSON.stringify(value)}${options.betweenPairSeparator}` - } - // maybe miss something for non Record / Array headers and searchParams here - }) - - // delete last betweenPairSeparator - return result.slice(0, -1) - } -} - -class MockCallHistory { - logs = [] - - calls () { - return this.logs - } - - firstCall () { - return this.logs.at(0) - } - - lastCall () { - return this.logs.at(-1) - } - - nthCall (number) { - if (typeof number !== 'number') { - throw new InvalidArgumentError('nthCall must be called with a number') - } - if (!Number.isInteger(number)) { - throw new InvalidArgumentError('nthCall must be called with an integer') - } - if (Math.sign(number) !== 1) { - throw new InvalidArgumentError('nthCall must be called with a positive value. use firstCall or lastCall instead') - } - - // non zero based index. this is more human readable - return this.logs.at(number - 1) - } - - filterCalls (criteria, options) { - // perf - if (this.logs.length === 0) { - return this.logs - } - if (typeof criteria === 'function') { - return this.logs.filter(criteria) - } - if (criteria instanceof RegExp) { - return this.logs.filter((log) => { - return criteria.test(log.toString()) - }) - } - if (typeof criteria === 'object' && criteria !== null) { - // no criteria - returning all logs - if (Object.keys(criteria).length === 0) { - return this.logs - } - - const finalOptions = { operator: 'OR', ...buildAndValidateFilterCallsOptions(options) } - - let maybeDuplicatedLogsFiltered = [] - if ('protocol' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered) - } - if ('host' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered) - } - if ('port' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered) - } - if ('origin' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered) - } - if ('path' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered) - } - if ('hash' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered) - } - if ('fullUrl' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered) - } - if ('method' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered) - } - - const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)] - - return uniqLogsFiltered - } - - throw new InvalidArgumentError('criteria parameter should be one of function, regexp, or object') - } - - filterCallsByProtocol = makeFilterCalls.call(this, 'protocol') - - filterCallsByHost = makeFilterCalls.call(this, 'host') - - filterCallsByPort = makeFilterCalls.call(this, 'port') - - filterCallsByOrigin = makeFilterCalls.call(this, 'origin') - - filterCallsByPath = makeFilterCalls.call(this, 'path') - - filterCallsByHash = makeFilterCalls.call(this, 'hash') - - filterCallsByFullUrl = makeFilterCalls.call(this, 'fullUrl') - - filterCallsByMethod = makeFilterCalls.call(this, 'method') - - clear () { - this.logs = [] - } - - [kMockCallHistoryAddLog] (requestInit) { - const log = new MockCallHistoryLog(requestInit) - - this.logs.push(log) - - return log - } - - * [Symbol.iterator] () { - for (const log of this.calls()) { - yield log - } - } -} - -module.exports.MockCallHistory = MockCallHistory -module.exports.MockCallHistoryLog = MockCallHistoryLog diff --git a/vanilla/node_modules/undici/lib/mock/mock-client.js b/vanilla/node_modules/undici/lib/mock/mock-client.js deleted file mode 100644 index b3be7ab..0000000 --- a/vanilla/node_modules/undici/lib/mock/mock-client.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict' - -const { promisify } = require('node:util') -const Client = require('../dispatcher/client') -const { buildMockDispatch } = require('./mock-utils') -const { - kDispatches, - kMockAgent, - kClose, - kOriginalClose, - kOrigin, - kOriginalDispatch, - kConnected, - kIgnoreTrailingSlash -} = require('./mock-symbols') -const { MockInterceptor } = require('./mock-interceptor') -const Symbols = require('../core/symbols') -const { InvalidArgumentError } = require('../core/errors') - -/** - * MockClient provides an API that extends the Client to influence the mockDispatches. - */ -class MockClient extends Client { - constructor (origin, opts) { - if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { - throw new InvalidArgumentError('Argument opts.agent must implement Agent') - } - - super(origin, opts) - - this[kMockAgent] = opts.agent - this[kOrigin] = origin - this[kIgnoreTrailingSlash] = opts.ignoreTrailingSlash ?? false - this[kDispatches] = [] - this[kConnected] = 1 - this[kOriginalDispatch] = this.dispatch - this[kOriginalClose] = this.close.bind(this) - - this.dispatch = buildMockDispatch.call(this) - this.close = this[kClose] - } - - get [Symbols.kConnected] () { - return this[kConnected] - } - - /** - * Sets up the base interceptor for mocking replies from undici. - */ - intercept (opts) { - return new MockInterceptor( - opts && { ignoreTrailingSlash: this[kIgnoreTrailingSlash], ...opts }, - this[kDispatches] - ) - } - - cleanMocks () { - this[kDispatches] = [] - } - - async [kClose] () { - await promisify(this[kOriginalClose])() - this[kConnected] = 0 - this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) - } -} - -module.exports = MockClient diff --git a/vanilla/node_modules/undici/lib/mock/mock-errors.js b/vanilla/node_modules/undici/lib/mock/mock-errors.js deleted file mode 100644 index 69e4f9c..0000000 --- a/vanilla/node_modules/undici/lib/mock/mock-errors.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict' - -const { UndiciError } = require('../core/errors') - -const kMockNotMatchedError = Symbol.for('undici.error.UND_MOCK_ERR_MOCK_NOT_MATCHED') - -/** - * The request does not match any registered mock dispatches. - */ -class MockNotMatchedError extends UndiciError { - constructor (message) { - super(message) - this.name = 'MockNotMatchedError' - this.message = message || 'The request does not match any registered mock dispatches' - this.code = 'UND_MOCK_ERR_MOCK_NOT_MATCHED' - } - - static [Symbol.hasInstance] (instance) { - return instance && instance[kMockNotMatchedError] === true - } - - get [kMockNotMatchedError] () { - return true - } -} - -module.exports = { - MockNotMatchedError -} diff --git a/vanilla/node_modules/undici/lib/mock/mock-interceptor.js b/vanilla/node_modules/undici/lib/mock/mock-interceptor.js deleted file mode 100644 index 1ea7aac..0000000 --- a/vanilla/node_modules/undici/lib/mock/mock-interceptor.js +++ /dev/null @@ -1,209 +0,0 @@ -'use strict' - -const { getResponseData, buildKey, addMockDispatch } = require('./mock-utils') -const { - kDispatches, - kDispatchKey, - kDefaultHeaders, - kDefaultTrailers, - kContentLength, - kMockDispatch, - kIgnoreTrailingSlash -} = require('./mock-symbols') -const { InvalidArgumentError } = require('../core/errors') -const { serializePathWithQuery } = require('../core/util') - -/** - * Defines the scope API for an interceptor reply - */ -class MockScope { - constructor (mockDispatch) { - this[kMockDispatch] = mockDispatch - } - - /** - * Delay a reply by a set amount in ms. - */ - delay (waitInMs) { - if (typeof waitInMs !== 'number' || !Number.isInteger(waitInMs) || waitInMs <= 0) { - throw new InvalidArgumentError('waitInMs must be a valid integer > 0') - } - - this[kMockDispatch].delay = waitInMs - return this - } - - /** - * For a defined reply, never mark as consumed. - */ - persist () { - this[kMockDispatch].persist = true - return this - } - - /** - * Allow one to define a reply for a set amount of matching requests. - */ - times (repeatTimes) { - if (typeof repeatTimes !== 'number' || !Number.isInteger(repeatTimes) || repeatTimes <= 0) { - throw new InvalidArgumentError('repeatTimes must be a valid integer > 0') - } - - this[kMockDispatch].times = repeatTimes - return this - } -} - -/** - * Defines an interceptor for a Mock - */ -class MockInterceptor { - constructor (opts, mockDispatches) { - if (typeof opts !== 'object') { - throw new InvalidArgumentError('opts must be an object') - } - if (typeof opts.path === 'undefined') { - throw new InvalidArgumentError('opts.path must be defined') - } - if (typeof opts.method === 'undefined') { - opts.method = 'GET' - } - // See https://github.com/nodejs/undici/issues/1245 - // As per RFC 3986, clients are not supposed to send URI - // fragments to servers when they retrieve a document, - if (typeof opts.path === 'string') { - if (opts.query) { - opts.path = serializePathWithQuery(opts.path, opts.query) - } else { - // Matches https://github.com/nodejs/undici/blob/main/lib/web/fetch/index.js#L1811 - const parsedURL = new URL(opts.path, 'data://') - opts.path = parsedURL.pathname + parsedURL.search - } - } - if (typeof opts.method === 'string') { - opts.method = opts.method.toUpperCase() - } - - this[kDispatchKey] = buildKey(opts) - this[kDispatches] = mockDispatches - this[kIgnoreTrailingSlash] = opts.ignoreTrailingSlash ?? false - this[kDefaultHeaders] = {} - this[kDefaultTrailers] = {} - this[kContentLength] = false - } - - createMockScopeDispatchData ({ statusCode, data, responseOptions }) { - const responseData = getResponseData(data) - const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {} - const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers } - const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers } - - return { statusCode, data, headers, trailers } - } - - validateReplyParameters (replyParameters) { - if (typeof replyParameters.statusCode === 'undefined') { - throw new InvalidArgumentError('statusCode must be defined') - } - if (typeof replyParameters.responseOptions !== 'object' || replyParameters.responseOptions === null) { - throw new InvalidArgumentError('responseOptions must be an object') - } - } - - /** - * Mock an undici request with a defined reply. - */ - reply (replyOptionsCallbackOrStatusCode) { - // Values of reply aren't available right now as they - // can only be available when the reply callback is invoked. - if (typeof replyOptionsCallbackOrStatusCode === 'function') { - // We'll first wrap the provided callback in another function, - // this function will properly resolve the data from the callback - // when invoked. - const wrappedDefaultsCallback = (opts) => { - // Our reply options callback contains the parameter for statusCode, data and options. - const resolvedData = replyOptionsCallbackOrStatusCode(opts) - - // Check if it is in the right format - if (typeof resolvedData !== 'object' || resolvedData === null) { - throw new InvalidArgumentError('reply options callback must return an object') - } - - const replyParameters = { data: '', responseOptions: {}, ...resolvedData } - this.validateReplyParameters(replyParameters) - // Since the values can be obtained immediately we return them - // from this higher order function that will be resolved later. - return { - ...this.createMockScopeDispatchData(replyParameters) - } - } - - // Add usual dispatch data, but this time set the data parameter to function that will eventually provide data. - const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] }) - return new MockScope(newMockDispatch) - } - - // We can have either one or three parameters, if we get here, - // we should have 1-3 parameters. So we spread the arguments of - // this function to obtain the parameters, since replyData will always - // just be the statusCode. - const replyParameters = { - statusCode: replyOptionsCallbackOrStatusCode, - data: arguments[1] === undefined ? '' : arguments[1], - responseOptions: arguments[2] === undefined ? {} : arguments[2] - } - this.validateReplyParameters(replyParameters) - - // Send in-already provided data like usual - const dispatchData = this.createMockScopeDispatchData(replyParameters) - const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] }) - return new MockScope(newMockDispatch) - } - - /** - * Mock an undici request with a defined error. - */ - replyWithError (error) { - if (typeof error === 'undefined') { - throw new InvalidArgumentError('error must be defined') - } - - const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error }, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] }) - return new MockScope(newMockDispatch) - } - - /** - * Set default reply headers on the interceptor for subsequent replies - */ - defaultReplyHeaders (headers) { - if (typeof headers === 'undefined') { - throw new InvalidArgumentError('headers must be defined') - } - - this[kDefaultHeaders] = headers - return this - } - - /** - * Set default reply trailers on the interceptor for subsequent replies - */ - defaultReplyTrailers (trailers) { - if (typeof trailers === 'undefined') { - throw new InvalidArgumentError('trailers must be defined') - } - - this[kDefaultTrailers] = trailers - return this - } - - /** - * Set reply content length header for replies on the interceptor - */ - replyContentLength () { - this[kContentLength] = true - return this - } -} - -module.exports.MockInterceptor = MockInterceptor -module.exports.MockScope = MockScope diff --git a/vanilla/node_modules/undici/lib/mock/mock-pool.js b/vanilla/node_modules/undici/lib/mock/mock-pool.js deleted file mode 100644 index 2121e3c..0000000 --- a/vanilla/node_modules/undici/lib/mock/mock-pool.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict' - -const { promisify } = require('node:util') -const Pool = require('../dispatcher/pool') -const { buildMockDispatch } = require('./mock-utils') -const { - kDispatches, - kMockAgent, - kClose, - kOriginalClose, - kOrigin, - kOriginalDispatch, - kConnected, - kIgnoreTrailingSlash -} = require('./mock-symbols') -const { MockInterceptor } = require('./mock-interceptor') -const Symbols = require('../core/symbols') -const { InvalidArgumentError } = require('../core/errors') - -/** - * MockPool provides an API that extends the Pool to influence the mockDispatches. - */ -class MockPool extends Pool { - constructor (origin, opts) { - if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { - throw new InvalidArgumentError('Argument opts.agent must implement Agent') - } - - super(origin, opts) - - this[kMockAgent] = opts.agent - this[kOrigin] = origin - this[kIgnoreTrailingSlash] = opts.ignoreTrailingSlash ?? false - this[kDispatches] = [] - this[kConnected] = 1 - this[kOriginalDispatch] = this.dispatch - this[kOriginalClose] = this.close.bind(this) - - this.dispatch = buildMockDispatch.call(this) - this.close = this[kClose] - } - - get [Symbols.kConnected] () { - return this[kConnected] - } - - /** - * Sets up the base interceptor for mocking replies from undici. - */ - intercept (opts) { - return new MockInterceptor( - opts && { ignoreTrailingSlash: this[kIgnoreTrailingSlash], ...opts }, - this[kDispatches] - ) - } - - cleanMocks () { - this[kDispatches] = [] - } - - async [kClose] () { - await promisify(this[kOriginalClose])() - this[kConnected] = 0 - this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) - } -} - -module.exports = MockPool diff --git a/vanilla/node_modules/undici/lib/mock/mock-symbols.js b/vanilla/node_modules/undici/lib/mock/mock-symbols.js deleted file mode 100644 index 940dbe6..0000000 --- a/vanilla/node_modules/undici/lib/mock/mock-symbols.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict' - -module.exports = { - kAgent: Symbol('agent'), - kOptions: Symbol('options'), - kFactory: Symbol('factory'), - kDispatches: Symbol('dispatches'), - kDispatchKey: Symbol('dispatch key'), - kDefaultHeaders: Symbol('default headers'), - kDefaultTrailers: Symbol('default trailers'), - kContentLength: Symbol('content length'), - kMockAgent: Symbol('mock agent'), - kMockAgentSet: Symbol('mock agent set'), - kMockAgentGet: Symbol('mock agent get'), - kMockDispatch: Symbol('mock dispatch'), - kClose: Symbol('close'), - kOriginalClose: Symbol('original agent close'), - kOriginalDispatch: Symbol('original dispatch'), - kOrigin: Symbol('origin'), - kIsMockActive: Symbol('is mock active'), - kNetConnect: Symbol('net connect'), - kGetNetConnect: Symbol('get net connect'), - kConnected: Symbol('connected'), - kIgnoreTrailingSlash: Symbol('ignore trailing slash'), - kMockAgentMockCallHistoryInstance: Symbol('mock agent mock call history name'), - kMockAgentRegisterCallHistory: Symbol('mock agent register mock call history'), - kMockAgentAddCallHistoryLog: Symbol('mock agent add call history log'), - kMockAgentIsCallHistoryEnabled: Symbol('mock agent is call history enabled'), - kMockAgentAcceptsNonStandardSearchParameters: Symbol('mock agent accepts non standard search parameters'), - kMockCallHistoryAddLog: Symbol('mock call history add log') -} diff --git a/vanilla/node_modules/undici/lib/mock/mock-utils.js b/vanilla/node_modules/undici/lib/mock/mock-utils.js deleted file mode 100644 index 291a857..0000000 --- a/vanilla/node_modules/undici/lib/mock/mock-utils.js +++ /dev/null @@ -1,480 +0,0 @@ -'use strict' - -const { MockNotMatchedError } = require('./mock-errors') -const { - kDispatches, - kMockAgent, - kOriginalDispatch, - kOrigin, - kGetNetConnect -} = require('./mock-symbols') -const { serializePathWithQuery } = require('../core/util') -const { STATUS_CODES } = require('node:http') -const { - types: { - isPromise - } -} = require('node:util') -const { InvalidArgumentError } = require('../core/errors') - -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 normalizeSearchParams (query) { - if (typeof query !== 'string') { - return query - } - - const originalQp = new URLSearchParams(query) - const normalizedQp = new URLSearchParams() - - for (let [key, value] of originalQp.entries()) { - key = key.replace('[]', '') - - const valueRepresentsString = /^(['"]).*\1$/.test(value) - if (valueRepresentsString) { - normalizedQp.append(key, value) - continue - } - - if (value.includes(',')) { - const values = value.split(',') - for (const v of values) { - normalizedQp.append(key, v) - } - continue - } - - normalizedQp.append(key, value) - } - - return normalizedQp -} - -function safeUrl (path) { - if (typeof path !== 'string') { - return path - } - const pathSegments = path.split('?', 3) - 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 (data instanceof Uint8Array) { - return data - } else if (data instanceof ArrayBuffer) { - return data - } else if (typeof data === 'object') { - return JSON.stringify(data) - } else if (data) { - return data.toString() - } else { - return '' - } -} - -function getMockDispatch (mockDispatches, key) { - const basePath = key.query ? serializePathWithQuery(key.path, key.query) : key.path - const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath - - const resolvedPathWithoutTrailingSlash = removeTrailingSlash(resolvedPath) - - // Match path - let matchedMockDispatches = mockDispatches - .filter(({ consumed }) => !consumed) - .filter(({ path, ignoreTrailingSlash }) => { - return ignoreTrailingSlash - ? matchValue(removeTrailingSlash(safeUrl(path)), resolvedPathWithoutTrailingSlash) - : 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}' on path '${resolvedPath}'`) - } - - // 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}' on path '${resolvedPath}'`) - } - - // Match headers - matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers)) - if (matchedMockDispatches.length === 0) { - const headers = typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers - throw new MockNotMatchedError(`Mock dispatch not matched for headers '${headers}' on path '${resolvedPath}'`) - } - - return matchedMockDispatches[0] -} - -function addMockDispatch (mockDispatches, key, data, opts) { - const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false, ...opts } - 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) - } -} - -/** - * @param {string} path Path to remove trailing slash from - */ -function removeTrailingSlash (path) { - while (path.endsWith('/')) { - path = path.slice(0, -1) - } - - if (path.length === 0) { - path = '/' - } - - return path -} - -function buildKey (opts) { - const { path, method, body, headers, query } = opts - - return { - path, - method, - body, - headers, - query - } -} - -function generateKeyValues (data) { - const keys = Object.keys(data) - const result = [] - for (let i = 0; i < keys.length; ++i) { - const key = keys[i] - const value = data[key] - const name = Buffer.from(`${key}`) - if (Array.isArray(value)) { - for (let j = 0; j < value.length; ++j) { - result.push(name, Buffer.from(`${value[j]}`)) - } - } else { - result.push(name, Buffer.from(`${value}`)) - } - } - return result -} - -/** - * @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 - } - - // Track whether the request has been aborted - let aborted = false - let timer = null - - function abort (err) { - if (aborted) { - return - } - aborted = true - - // Clear the pending delayed response if any - if (timer !== null) { - clearTimeout(timer) - timer = null - } - - // Notify the handler of the abort - handler.onError(err) - } - - // Call onConnect to allow the handler to register the abort callback - handler.onConnect?.(abort, null) - - // Handle the request with a delay if necessary - if (typeof delay === 'number' && delay > 0) { - timer = setTimeout(() => { - timer = null - handleReply(this[kDispatches]) - }, delay) - } else { - handleReply(this[kDispatches]) - } - - function handleReply (mockDispatches, _data = data) { - // Don't send response if the request was aborted - if (aborted) { - return - } - - // 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. - return body.then((newData) => handleReply(mockDispatches, newData)) - } - - // Check again if aborted after async body resolution - if (aborted) { - return - } - - const responseData = getResponseData(body) - const responseHeaders = generateKeyValues(headers) - const responseTrailers = generateKeyValues(trailers) - - 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.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') { - 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 normalizeOrigin (origin) { - if (typeof origin !== 'string' && !(origin instanceof URL)) { - return origin - } - - if (origin instanceof URL) { - return origin.origin - } - - return origin.toLowerCase() -} - -function buildAndValidateMockOptions (opts) { - const { agent, ...mockOptions } = opts - - if ('enableCallHistory' in mockOptions && typeof mockOptions.enableCallHistory !== 'boolean') { - throw new InvalidArgumentError('options.enableCallHistory must to be a boolean') - } - - if ('acceptNonStandardSearchParameters' in mockOptions && typeof mockOptions.acceptNonStandardSearchParameters !== 'boolean') { - throw new InvalidArgumentError('options.acceptNonStandardSearchParameters must to be a boolean') - } - - if ('ignoreTrailingSlash' in mockOptions && typeof mockOptions.ignoreTrailingSlash !== 'boolean') { - throw new InvalidArgumentError('options.ignoreTrailingSlash must to be a boolean') - } - - return mockOptions -} - -module.exports = { - getResponseData, - getMockDispatch, - addMockDispatch, - deleteMockDispatch, - buildKey, - generateKeyValues, - matchValue, - getResponse, - getStatusText, - mockDispatch, - buildMockDispatch, - checkNetConnect, - buildAndValidateMockOptions, - getHeaderByName, - buildHeadersFromArray, - normalizeSearchParams, - normalizeOrigin -} diff --git a/vanilla/node_modules/undici/lib/mock/pending-interceptors-formatter.js b/vanilla/node_modules/undici/lib/mock/pending-interceptors-formatter.js deleted file mode 100644 index ccca951..0000000 --- a/vanilla/node_modules/undici/lib/mock/pending-interceptors-formatter.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict' - -const { Transform } = require('node:stream') -const { Console } = require('node:console') - -const PERSISTENT = process.versions.icu ? '✅' : 'Y ' -const NOT_PERSISTENT = process.versions.icu ? '❌' : 'N ' - -/** - * Gets the output of `console.table(…)` as a string. - */ -module.exports = class PendingInterceptorsFormatter { - constructor ({ disableColors } = {}) { - this.transform = new Transform({ - transform (chunk, _enc, cb) { - cb(null, chunk) - } - }) - - this.logger = new Console({ - stdout: this.transform, - inspectOptions: { - colors: !disableColors && !process.env.CI - } - }) - } - - format (pendingInterceptors) { - const withPrettyHeaders = pendingInterceptors.map( - ({ method, path, data: { statusCode }, persist, times, timesInvoked, origin }) => ({ - Method: method, - Origin: origin, - Path: path, - 'Status code': statusCode, - Persistent: persist ? PERSISTENT : NOT_PERSISTENT, - Invocations: timesInvoked, - Remaining: persist ? Infinity : times - timesInvoked - })) - - this.logger.table(withPrettyHeaders) - return this.transform.read().toString() - } -} diff --git a/vanilla/node_modules/undici/lib/mock/snapshot-agent.js b/vanilla/node_modules/undici/lib/mock/snapshot-agent.js deleted file mode 100644 index 8028011..0000000 --- a/vanilla/node_modules/undici/lib/mock/snapshot-agent.js +++ /dev/null @@ -1,353 +0,0 @@ -'use strict' - -const Agent = require('../dispatcher/agent') -const MockAgent = require('./mock-agent') -const { SnapshotRecorder } = require('./snapshot-recorder') -const WrapHandler = require('../handler/wrap-handler') -const { InvalidArgumentError, UndiciError } = require('../core/errors') -const { validateSnapshotMode } = require('./snapshot-utils') - -const kSnapshotRecorder = Symbol('kSnapshotRecorder') -const kSnapshotMode = Symbol('kSnapshotMode') -const kSnapshotPath = Symbol('kSnapshotPath') -const kSnapshotLoaded = Symbol('kSnapshotLoaded') -const kRealAgent = Symbol('kRealAgent') - -// Static flag to ensure warning is only emitted once per process -let warningEmitted = false - -class SnapshotAgent extends MockAgent { - constructor (opts = {}) { - // Emit experimental warning only once - if (!warningEmitted) { - process.emitWarning( - 'SnapshotAgent is experimental and subject to change', - 'ExperimentalWarning' - ) - warningEmitted = true - } - - const { - mode = 'record', - snapshotPath = null, - ...mockAgentOpts - } = opts - - super(mockAgentOpts) - - validateSnapshotMode(mode) - - // Validate snapshotPath is provided when required - if ((mode === 'playback' || mode === 'update') && !snapshotPath) { - throw new InvalidArgumentError(`snapshotPath is required when mode is '${mode}'`) - } - - this[kSnapshotMode] = mode - this[kSnapshotPath] = snapshotPath - - this[kSnapshotRecorder] = new SnapshotRecorder({ - snapshotPath: this[kSnapshotPath], - mode: this[kSnapshotMode], - maxSnapshots: opts.maxSnapshots, - autoFlush: opts.autoFlush, - flushInterval: opts.flushInterval, - matchHeaders: opts.matchHeaders, - ignoreHeaders: opts.ignoreHeaders, - excludeHeaders: opts.excludeHeaders, - matchBody: opts.matchBody, - matchQuery: opts.matchQuery, - caseSensitive: opts.caseSensitive, - shouldRecord: opts.shouldRecord, - shouldPlayback: opts.shouldPlayback, - excludeUrls: opts.excludeUrls - }) - this[kSnapshotLoaded] = false - - // For recording/update mode, we need a real agent to make actual requests - // For playback mode, we need a real agent if there are excluded URLs - if (this[kSnapshotMode] === 'record' || this[kSnapshotMode] === 'update' || - (this[kSnapshotMode] === 'playback' && opts.excludeUrls && opts.excludeUrls.length > 0)) { - this[kRealAgent] = new Agent(opts) - } - - // Auto-load snapshots in playback/update mode - if ((this[kSnapshotMode] === 'playback' || this[kSnapshotMode] === 'update') && this[kSnapshotPath]) { - this.loadSnapshots().catch(() => { - // Ignore load errors - file might not exist yet - }) - } - } - - dispatch (opts, handler) { - handler = WrapHandler.wrap(handler) - const mode = this[kSnapshotMode] - - // Check if URL should be excluded (pass through without mocking/recording) - if (this[kSnapshotRecorder].isUrlExcluded(opts)) { - // Real agent is guaranteed by constructor when excludeUrls is configured - return this[kRealAgent].dispatch(opts, handler) - } - - if (mode === 'playback' || mode === 'update') { - // Ensure snapshots are loaded - if (!this[kSnapshotLoaded]) { - // Need to load asynchronously, delegate to async version - return this.#asyncDispatch(opts, handler) - } - - // Try to find existing snapshot (synchronous) - const snapshot = this[kSnapshotRecorder].findSnapshot(opts) - - if (snapshot) { - // Use recorded response (synchronous) - return this.#replaySnapshot(snapshot, handler) - } else if (mode === 'update') { - // Make real request and record it (async required) - return this.#recordAndReplay(opts, handler) - } else { - // Playback mode but no snapshot found - const error = new UndiciError(`No snapshot found for ${opts.method || 'GET'} ${opts.path}`) - if (handler.onError) { - handler.onError(error) - return - } - throw error - } - } else if (mode === 'record') { - // Record mode - make real request and save response (async required) - return this.#recordAndReplay(opts, handler) - } - } - - /** - * Async version of dispatch for when we need to load snapshots first - */ - async #asyncDispatch (opts, handler) { - await this.loadSnapshots() - return this.dispatch(opts, handler) - } - - /** - * Records a real request and replays the response - */ - #recordAndReplay (opts, handler) { - const responseData = { - statusCode: null, - headers: {}, - trailers: {}, - body: [] - } - - const self = this // Capture 'this' context for use within nested handler callbacks - - const recordingHandler = { - onRequestStart (controller, context) { - return handler.onRequestStart(controller, { ...context, history: this.history }) - }, - - onRequestUpgrade (controller, statusCode, headers, socket) { - return handler.onRequestUpgrade(controller, statusCode, headers, socket) - }, - - onResponseStart (controller, statusCode, headers, statusMessage) { - responseData.statusCode = statusCode - responseData.headers = headers - return handler.onResponseStart(controller, statusCode, headers, statusMessage) - }, - - onResponseData (controller, chunk) { - responseData.body.push(chunk) - return handler.onResponseData(controller, chunk) - }, - - onResponseEnd (controller, trailers) { - responseData.trailers = trailers - - // Record the interaction using captured 'self' context (fire and forget) - const responseBody = Buffer.concat(responseData.body) - self[kSnapshotRecorder].record(opts, { - statusCode: responseData.statusCode, - headers: responseData.headers, - body: responseBody, - trailers: responseData.trailers - }) - .then(() => handler.onResponseEnd(controller, trailers)) - .catch((error) => handler.onResponseError(controller, error)) - } - } - - // Use composed agent if available (includes interceptors), otherwise use real agent - const agent = this[kRealAgent] - return agent.dispatch(opts, recordingHandler) - } - - /** - * Replays a recorded response - * - * @param {Object} snapshot - The recorded snapshot to replay. - * @param {Object} handler - The handler to call with the response data. - * @returns {void} - */ - #replaySnapshot (snapshot, handler) { - try { - const { response } = snapshot - - const controller = { - pause () { }, - resume () { }, - abort (reason) { - this.aborted = true - this.reason = reason - }, - - aborted: false, - paused: false - } - - handler.onRequestStart(controller) - - handler.onResponseStart(controller, response.statusCode, response.headers) - - // Body is always stored as base64 string - const body = Buffer.from(response.body, 'base64') - handler.onResponseData(controller, body) - - handler.onResponseEnd(controller, response.trailers) - } catch (error) { - handler.onError?.(error) - } - } - - /** - * Loads snapshots from file - * - * @param {string} [filePath] - Optional file path to load snapshots from. - * @returns {Promise<void>} - Resolves when snapshots are loaded. - */ - async loadSnapshots (filePath) { - await this[kSnapshotRecorder].loadSnapshots(filePath || this[kSnapshotPath]) - this[kSnapshotLoaded] = true - - // In playback mode, set up MockAgent interceptors for all snapshots - if (this[kSnapshotMode] === 'playback') { - this.#setupMockInterceptors() - } - } - - /** - * Saves snapshots to file - * - * @param {string} [filePath] - Optional file path to save snapshots to. - * @returns {Promise<void>} - Resolves when snapshots are saved. - */ - async saveSnapshots (filePath) { - return this[kSnapshotRecorder].saveSnapshots(filePath || this[kSnapshotPath]) - } - - /** - * Sets up MockAgent interceptors based on recorded snapshots. - * - * This method creates MockAgent interceptors for each recorded snapshot, - * allowing the SnapshotAgent to fall back to MockAgent's standard intercept - * mechanism in playback mode. Each interceptor is configured to persist - * (remain active for multiple requests) and responds with the recorded - * response data. - * - * Called automatically when loading snapshots in playback mode. - * - * @returns {void} - */ - #setupMockInterceptors () { - for (const snapshot of this[kSnapshotRecorder].getSnapshots()) { - const { request, responses, response } = snapshot - const url = new URL(request.url) - - const mockPool = this.get(url.origin) - - // Handle both new format (responses array) and legacy format (response object) - const responseData = responses ? responses[0] : response - if (!responseData) continue - - mockPool.intercept({ - path: url.pathname + url.search, - method: request.method, - headers: request.headers, - body: request.body - }).reply(responseData.statusCode, responseData.body, { - headers: responseData.headers, - trailers: responseData.trailers - }).persist() - } - } - - /** - * Gets the snapshot recorder - * @return {SnapshotRecorder} - The snapshot recorder instance - */ - getRecorder () { - return this[kSnapshotRecorder] - } - - /** - * Gets the current mode - * @return {import('./snapshot-utils').SnapshotMode} - The current snapshot mode - */ - getMode () { - return this[kSnapshotMode] - } - - /** - * Clears all snapshots - * @returns {void} - */ - clearSnapshots () { - this[kSnapshotRecorder].clear() - } - - /** - * Resets call counts for all snapshots (useful for test cleanup) - * @returns {void} - */ - resetCallCounts () { - this[kSnapshotRecorder].resetCallCounts() - } - - /** - * Deletes a specific snapshot by request options - * @param {import('./snapshot-recorder').SnapshotRequestOptions} requestOpts - Request options to identify the snapshot - * @return {Promise<boolean>} - Returns true if the snapshot was deleted, false if not found - */ - deleteSnapshot (requestOpts) { - return this[kSnapshotRecorder].deleteSnapshot(requestOpts) - } - - /** - * Gets information about a specific snapshot - * @returns {import('./snapshot-recorder').SnapshotInfo|null} - Snapshot information or null if not found - */ - getSnapshotInfo (requestOpts) { - return this[kSnapshotRecorder].getSnapshotInfo(requestOpts) - } - - /** - * Replaces all snapshots with new data (full replacement) - * @param {Array<{hash: string; snapshot: import('./snapshot-recorder').SnapshotEntryshotEntry}>|Record<string, import('./snapshot-recorder').SnapshotEntry>} snapshotData - New snapshot data to replace existing snapshots - * @returns {void} - */ - replaceSnapshots (snapshotData) { - this[kSnapshotRecorder].replaceSnapshots(snapshotData) - } - - /** - * Closes the agent, saving snapshots and cleaning up resources. - * - * @returns {Promise<void>} - */ - async close () { - await this[kSnapshotRecorder].close() - await this[kRealAgent]?.close() - await super.close() - } -} - -module.exports = SnapshotAgent diff --git a/vanilla/node_modules/undici/lib/mock/snapshot-recorder.js b/vanilla/node_modules/undici/lib/mock/snapshot-recorder.js deleted file mode 100644 index b5d07fa..0000000 --- a/vanilla/node_modules/undici/lib/mock/snapshot-recorder.js +++ /dev/null @@ -1,588 +0,0 @@ -'use strict' - -const { writeFile, readFile, mkdir } = require('node:fs/promises') -const { dirname, resolve } = require('node:path') -const { setTimeout, clearTimeout } = require('node:timers') -const { InvalidArgumentError, UndiciError } = require('../core/errors') -const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require('./snapshot-utils') - -/** - * @typedef {Object} SnapshotRequestOptions - * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.) - * @property {string} path - Request path - * @property {string} origin - Request origin (base URL) - * @property {import('./snapshot-utils').Headers|import('./snapshot-utils').UndiciHeaders} headers - Request headers - * @property {import('./snapshot-utils').NormalizedHeaders} _normalizedHeaders - Request headers as a lowercase object - * @property {string|Buffer} [body] - Request body (optional) - */ - -/** - * @typedef {Object} SnapshotEntryRequest - * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.) - * @property {string} url - Full URL of the request - * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object - * @property {string|Buffer} [body] - Request body (optional) - */ - -/** - * @typedef {Object} SnapshotEntryResponse - * @property {number} statusCode - HTTP status code of the response - * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized response headers as a lowercase object - * @property {string} body - Response body as a base64url encoded string - * @property {Object} [trailers] - Optional response trailers - */ - -/** - * @typedef {Object} SnapshotEntry - * @property {SnapshotEntryRequest} request - The request object - * @property {Array<SnapshotEntryResponse>} responses - Array of response objects - * @property {number} callCount - Number of times this snapshot has been called - * @property {string} timestamp - ISO timestamp of when the snapshot was created - */ - -/** - * @typedef {Object} SnapshotRecorderMatchOptions - * @property {Array<string>} [matchHeaders=[]] - Headers to match (empty array means match all headers) - * @property {Array<string>} [ignoreHeaders=[]] - Headers to ignore for matching - * @property {Array<string>} [excludeHeaders=[]] - Headers to exclude from matching - * @property {boolean} [matchBody=true] - Whether to match request body - * @property {boolean} [matchQuery=true] - Whether to match query properties - * @property {boolean} [caseSensitive=false] - Whether header matching is case-sensitive - */ - -/** - * @typedef {Object} SnapshotRecorderOptions - * @property {string} [snapshotPath] - Path to save/load snapshots - * @property {import('./snapshot-utils').SnapshotMode} [mode='record'] - Mode: 'record' or 'playback' - * @property {number} [maxSnapshots=Infinity] - Maximum number of snapshots to keep - * @property {boolean} [autoFlush=false] - Whether to automatically flush snapshots to disk - * @property {number} [flushInterval=30000] - Auto-flush interval in milliseconds (default: 30 seconds) - * @property {Array<string|RegExp>} [excludeUrls=[]] - URLs to exclude from recording - * @property {function} [shouldRecord=null] - Function to filter requests for recording - * @property {function} [shouldPlayback=null] - Function to filter requests - */ - -/** - * @typedef {Object} SnapshotFormattedRequest - * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.) - * @property {string} url - Full URL of the request (with query parameters if matchQuery is true) - * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object - * @property {string} body - Request body (optional, only if matchBody is true) - */ - -/** - * @typedef {Object} SnapshotInfo - * @property {string} hash - Hash key for the snapshot - * @property {SnapshotEntryRequest} request - The request object - * @property {number} responseCount - Number of responses recorded for this request - * @property {number} callCount - Number of times this snapshot has been called - * @property {string} timestamp - ISO timestamp of when the snapshot was created - */ - -/** - * Formats a request for consistent snapshot storage - * Caches normalized headers to avoid repeated processing - * - * @param {SnapshotRequestOptions} opts - Request options - * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached header sets for performance - * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers and body - * @returns {SnapshotFormattedRequest} - Formatted request object - */ -function formatRequestKey (opts, headerFilters, matchOptions = {}) { - const url = new URL(opts.path, opts.origin) - - // Cache normalized headers if not already done - const normalized = opts._normalizedHeaders || normalizeHeaders(opts.headers) - if (!opts._normalizedHeaders) { - opts._normalizedHeaders = normalized - } - - return { - method: opts.method || 'GET', - url: matchOptions.matchQuery !== false ? url.toString() : `${url.origin}${url.pathname}`, - headers: filterHeadersForMatching(normalized, headerFilters, matchOptions), - body: matchOptions.matchBody !== false && opts.body ? String(opts.body) : '' - } -} - -/** - * Filters headers based on matching configuration - * - * @param {import('./snapshot-utils').Headers} headers - Headers to filter - * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers - * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers - */ -function filterHeadersForMatching (headers, headerFilters, matchOptions = {}) { - if (!headers || typeof headers !== 'object') return {} - - const { - caseSensitive = false - } = matchOptions - - const filtered = {} - const { ignore, exclude, match } = headerFilters - - for (const [key, value] of Object.entries(headers)) { - const headerKey = caseSensitive ? key : key.toLowerCase() - - // Skip if in exclude list (for security) - if (exclude.has(headerKey)) continue - - // Skip if in ignore list (for matching) - if (ignore.has(headerKey)) continue - - // If matchHeaders is specified, only include those headers - if (match.size !== 0) { - if (!match.has(headerKey)) continue - } - - filtered[headerKey] = value - } - - return filtered -} - -/** - * Filters headers for storage (only excludes sensitive headers) - * - * @param {import('./snapshot-utils').Headers} headers - Headers to filter - * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers - * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers - */ -function filterHeadersForStorage (headers, headerFilters, matchOptions = {}) { - if (!headers || typeof headers !== 'object') return {} - - const { - caseSensitive = false - } = matchOptions - - const filtered = {} - const { exclude: excludeSet } = headerFilters - - for (const [key, value] of Object.entries(headers)) { - const headerKey = caseSensitive ? key : key.toLowerCase() - - // Skip if in exclude list (for security) - if (excludeSet.has(headerKey)) continue - - filtered[headerKey] = value - } - - return filtered -} - -/** - * Creates a hash key for request matching - * Properly orders headers to avoid conflicts and uses crypto hashing when available - * - * @param {SnapshotFormattedRequest} formattedRequest - Request object - * @returns {string} - Base64url encoded hash of the request - */ -function createRequestHash (formattedRequest) { - const parts = [ - formattedRequest.method, - formattedRequest.url - ] - - // Process headers in a deterministic way to avoid conflicts - if (formattedRequest.headers && typeof formattedRequest.headers === 'object') { - const headerKeys = Object.keys(formattedRequest.headers).sort() - for (const key of headerKeys) { - const values = Array.isArray(formattedRequest.headers[key]) - ? formattedRequest.headers[key] - : [formattedRequest.headers[key]] - - // Add header name - parts.push(key) - - // Add all values for this header, sorted for consistency - for (const value of values.sort()) { - parts.push(String(value)) - } - } - } - - // Add body - parts.push(formattedRequest.body) - - const content = parts.join('|') - - return hashId(content) -} - -class SnapshotRecorder { - /** @type {NodeJS.Timeout | null} */ - #flushTimeout - - /** @type {import('./snapshot-utils').IsUrlExcluded} */ - #isUrlExcluded - - /** @type {Map<string, SnapshotEntry>} */ - #snapshots = new Map() - - /** @type {string|undefined} */ - #snapshotPath - - /** @type {number} */ - #maxSnapshots = Infinity - - /** @type {boolean} */ - #autoFlush = false - - /** @type {import('./snapshot-utils').HeaderFilters} */ - #headerFilters - - /** - * Creates a new SnapshotRecorder instance - * @param {SnapshotRecorderOptions&SnapshotRecorderMatchOptions} [options={}] - Configuration options for the recorder - */ - constructor (options = {}) { - this.#snapshotPath = options.snapshotPath - this.#maxSnapshots = options.maxSnapshots || Infinity - this.#autoFlush = options.autoFlush || false - this.flushInterval = options.flushInterval || 30000 // 30 seconds default - this._flushTimer = null - - // Matching configuration - /** @type {Required<SnapshotRecorderMatchOptions>} */ - this.matchOptions = { - matchHeaders: options.matchHeaders || [], // empty means match all headers - ignoreHeaders: options.ignoreHeaders || [], - excludeHeaders: options.excludeHeaders || [], - matchBody: options.matchBody !== false, // default: true - matchQuery: options.matchQuery !== false, // default: true - caseSensitive: options.caseSensitive || false - } - - // Cache processed header sets to avoid recreating them on every request - this.#headerFilters = createHeaderFilters(this.matchOptions) - - // Request filtering callbacks - this.shouldRecord = options.shouldRecord || (() => true) // function(requestOpts) -> boolean - this.shouldPlayback = options.shouldPlayback || (() => true) // function(requestOpts) -> boolean - - // URL pattern filtering - this.#isUrlExcluded = isUrlExcludedFactory(options.excludeUrls) // Array of regex patterns or strings - - // Start auto-flush timer if enabled - if (this.#autoFlush && this.#snapshotPath) { - this.#startAutoFlush() - } - } - - /** - * Records a request-response interaction - * @param {SnapshotRequestOptions} requestOpts - Request options - * @param {SnapshotEntryResponse} response - Response data to record - * @return {Promise<void>} - Resolves when the recording is complete - */ - async record (requestOpts, response) { - // Check if recording should be filtered out - if (!this.shouldRecord(requestOpts)) { - return // Skip recording - } - - // Check URL exclusion patterns - if (this.isUrlExcluded(requestOpts)) { - return // Skip recording - } - - const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) - const hash = createRequestHash(request) - - // Extract response data - always store body as base64 - const normalizedHeaders = normalizeHeaders(response.headers) - - /** @type {SnapshotEntryResponse} */ - const responseData = { - statusCode: response.statusCode, - headers: filterHeadersForStorage(normalizedHeaders, this.#headerFilters, this.matchOptions), - body: Buffer.isBuffer(response.body) - ? response.body.toString('base64') - : Buffer.from(String(response.body || '')).toString('base64'), - trailers: response.trailers - } - - // Remove oldest snapshot if we exceed maxSnapshots limit - if (this.#snapshots.size >= this.#maxSnapshots && !this.#snapshots.has(hash)) { - const oldestKey = this.#snapshots.keys().next().value - this.#snapshots.delete(oldestKey) - } - - // Support sequential responses - if snapshot exists, add to responses array - const existingSnapshot = this.#snapshots.get(hash) - if (existingSnapshot && existingSnapshot.responses) { - existingSnapshot.responses.push(responseData) - existingSnapshot.timestamp = new Date().toISOString() - } else { - this.#snapshots.set(hash, { - request, - responses: [responseData], // Always store as array for consistency - callCount: 0, - timestamp: new Date().toISOString() - }) - } - - // Auto-flush if enabled - if (this.#autoFlush && this.#snapshotPath) { - this.#scheduleFlush() - } - } - - /** - * Checks if a URL should be excluded from recording/playback - * @param {SnapshotRequestOptions} requestOpts - Request options to check - * @returns {boolean} - True if URL is excluded - */ - isUrlExcluded (requestOpts) { - const url = new URL(requestOpts.path, requestOpts.origin).toString() - return this.#isUrlExcluded(url) - } - - /** - * Finds a matching snapshot for the given request - * Returns the appropriate response based on call count for sequential responses - * - * @param {SnapshotRequestOptions} requestOpts - Request options to match - * @returns {SnapshotEntry&Record<'response', SnapshotEntryResponse>|undefined} - Matching snapshot response or undefined if not found - */ - findSnapshot (requestOpts) { - // Check if playback should be filtered out - if (!this.shouldPlayback(requestOpts)) { - return undefined // Skip playback - } - - // Check URL exclusion patterns - if (this.isUrlExcluded(requestOpts)) { - return undefined // Skip playback - } - - const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) - const hash = createRequestHash(request) - const snapshot = this.#snapshots.get(hash) - - if (!snapshot) return undefined - - // Handle sequential responses - const currentCallCount = snapshot.callCount || 0 - const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1) - snapshot.callCount = currentCallCount + 1 - - return { - ...snapshot, - response: snapshot.responses[responseIndex] - } - } - - /** - * Loads snapshots from file - * @param {string} [filePath] - Optional file path to load snapshots from - * @return {Promise<void>} - Resolves when snapshots are loaded - */ - async loadSnapshots (filePath) { - const path = filePath || this.#snapshotPath - if (!path) { - throw new InvalidArgumentError('Snapshot path is required') - } - - try { - const data = await readFile(resolve(path), 'utf8') - const parsed = JSON.parse(data) - - // Convert array format back to Map - if (Array.isArray(parsed)) { - this.#snapshots.clear() - for (const { hash, snapshot } of parsed) { - this.#snapshots.set(hash, snapshot) - } - } else { - // Legacy object format - this.#snapshots = new Map(Object.entries(parsed)) - } - } catch (error) { - if (error.code === 'ENOENT') { - // File doesn't exist yet - that's ok for recording mode - this.#snapshots.clear() - } else { - throw new UndiciError(`Failed to load snapshots from ${path}`, { cause: error }) - } - } - } - - /** - * Saves snapshots to file - * - * @param {string} [filePath] - Optional file path to save snapshots - * @returns {Promise<void>} - Resolves when snapshots are saved - */ - async saveSnapshots (filePath) { - const path = filePath || this.#snapshotPath - if (!path) { - throw new InvalidArgumentError('Snapshot path is required') - } - - const resolvedPath = resolve(path) - - // Ensure directory exists - await mkdir(dirname(resolvedPath), { recursive: true }) - - // Convert Map to serializable format - const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({ - hash, - snapshot - })) - - await writeFile(resolvedPath, JSON.stringify(data, null, 2), { flush: true }) - } - - /** - * Clears all recorded snapshots - * @returns {void} - */ - clear () { - this.#snapshots.clear() - } - - /** - * Gets all recorded snapshots - * @return {Array<SnapshotEntry>} - Array of all recorded snapshots - */ - getSnapshots () { - return Array.from(this.#snapshots.values()) - } - - /** - * Gets snapshot count - * @return {number} - Number of recorded snapshots - */ - size () { - return this.#snapshots.size - } - - /** - * Resets call counts for all snapshots (useful for test cleanup) - * @returns {void} - */ - resetCallCounts () { - for (const snapshot of this.#snapshots.values()) { - snapshot.callCount = 0 - } - } - - /** - * Deletes a specific snapshot by request options - * @param {SnapshotRequestOptions} requestOpts - Request options to match - * @returns {boolean} - True if snapshot was deleted, false if not found - */ - deleteSnapshot (requestOpts) { - const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) - const hash = createRequestHash(request) - return this.#snapshots.delete(hash) - } - - /** - * Gets information about a specific snapshot - * @param {SnapshotRequestOptions} requestOpts - Request options to match - * @returns {SnapshotInfo|null} - Snapshot information or null if not found - */ - getSnapshotInfo (requestOpts) { - const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) - const hash = createRequestHash(request) - const snapshot = this.#snapshots.get(hash) - - if (!snapshot) return null - - return { - hash, - request: snapshot.request, - responseCount: snapshot.responses ? snapshot.responses.length : (snapshot.response ? 1 : 0), // .response for legacy snapshots - callCount: snapshot.callCount || 0, - timestamp: snapshot.timestamp - } - } - - /** - * Replaces all snapshots with new data (full replacement) - * @param {Array<{hash: string; snapshot: SnapshotEntry}>|Record<string, SnapshotEntry>} snapshotData - New snapshot data to replace existing ones - * @returns {void} - */ - replaceSnapshots (snapshotData) { - this.#snapshots.clear() - - if (Array.isArray(snapshotData)) { - for (const { hash, snapshot } of snapshotData) { - this.#snapshots.set(hash, snapshot) - } - } else if (snapshotData && typeof snapshotData === 'object') { - // Legacy object format - this.#snapshots = new Map(Object.entries(snapshotData)) - } - } - - /** - * Starts the auto-flush timer - * @returns {void} - */ - #startAutoFlush () { - return this.#scheduleFlush() - } - - /** - * Stops the auto-flush timer - * @returns {void} - */ - #stopAutoFlush () { - if (this.#flushTimeout) { - clearTimeout(this.#flushTimeout) - // Ensure any pending flush is completed - this.saveSnapshots().catch(() => { - // Ignore flush errors - }) - this.#flushTimeout = null - } - } - - /** - * Schedules a flush (debounced to avoid excessive writes) - */ - #scheduleFlush () { - this.#flushTimeout = setTimeout(() => { - this.saveSnapshots().catch(() => { - // Ignore flush errors - }) - if (this.#autoFlush) { - this.#flushTimeout?.refresh() - } else { - this.#flushTimeout = null - } - }, 1000) // 1 second debounce - } - - /** - * Cleanup method to stop timers - * @returns {void} - */ - destroy () { - this.#stopAutoFlush() - if (this.#flushTimeout) { - clearTimeout(this.#flushTimeout) - this.#flushTimeout = null - } - } - - /** - * Async close method that saves all recordings and performs cleanup - * @returns {Promise<void>} - */ - async close () { - // Save any pending recordings if we have a snapshot path - if (this.#snapshotPath && this.#snapshots.size !== 0) { - await this.saveSnapshots() - } - - // Perform cleanup - this.destroy() - } -} - -module.exports = { SnapshotRecorder, formatRequestKey, createRequestHash, filterHeadersForMatching, filterHeadersForStorage, createHeaderFilters } diff --git a/vanilla/node_modules/undici/lib/mock/snapshot-utils.js b/vanilla/node_modules/undici/lib/mock/snapshot-utils.js deleted file mode 100644 index a14b69c..0000000 --- a/vanilla/node_modules/undici/lib/mock/snapshot-utils.js +++ /dev/null @@ -1,158 +0,0 @@ -'use strict' - -const { InvalidArgumentError } = require('../core/errors') -const { runtimeFeatures } = require('../util/runtime-features.js') - -/** - * @typedef {Object} HeaderFilters - * @property {Set<string>} ignore - Set of headers to ignore for matching - * @property {Set<string>} exclude - Set of headers to exclude from matching - * @property {Set<string>} match - Set of headers to match (empty means match - */ - -/** - * Creates cached header sets for performance - * - * @param {import('./snapshot-recorder').SnapshotRecorderMatchOptions} matchOptions - Matching options for headers - * @returns {HeaderFilters} - Cached sets for ignore, exclude, and match headers - */ -function createHeaderFilters (matchOptions = {}) { - const { ignoreHeaders = [], excludeHeaders = [], matchHeaders = [], caseSensitive = false } = matchOptions - - return { - ignore: new Set(ignoreHeaders.map(header => caseSensitive ? header : header.toLowerCase())), - exclude: new Set(excludeHeaders.map(header => caseSensitive ? header : header.toLowerCase())), - match: new Set(matchHeaders.map(header => caseSensitive ? header : header.toLowerCase())) - } -} - -const crypto = runtimeFeatures.has('crypto') - ? require('node:crypto') - : null - -/** - * @callback HashIdFunction - * @param {string} value - The value to hash - * @returns {string} - The base64url encoded hash of the value - */ - -/** - * Generates a hash for a given value - * @type {HashIdFunction} - */ -const hashId = crypto?.hash - ? (value) => crypto.hash('sha256', value, 'base64url') - : (value) => Buffer.from(value).toString('base64url') - -/** - * @typedef {(url: string) => boolean} IsUrlExcluded Checks if a URL matches any of the exclude patterns - */ - -/** @typedef {{[key: Lowercase<string>]: string}} NormalizedHeaders */ -/** @typedef {Array<string>} UndiciHeaders */ -/** @typedef {Record<string, string|string[]>} Headers */ - -/** - * @param {*} headers - * @returns {headers is UndiciHeaders} - */ -function isUndiciHeaders (headers) { - return Array.isArray(headers) && (headers.length & 1) === 0 -} - -/** - * Factory function to create a URL exclusion checker - * @param {Array<string| RegExp>} [excludePatterns=[]] - Array of patterns to exclude - * @returns {IsUrlExcluded} - A function that checks if a URL matches any of the exclude patterns - */ -function isUrlExcludedFactory (excludePatterns = []) { - if (excludePatterns.length === 0) { - return () => false - } - - return function isUrlExcluded (url) { - let urlLowerCased - - for (const pattern of excludePatterns) { - if (typeof pattern === 'string') { - if (!urlLowerCased) { - // Convert URL to lowercase only once - urlLowerCased = url.toLowerCase() - } - // Simple string match (case-insensitive) - if (urlLowerCased.includes(pattern.toLowerCase())) { - return true - } - } else if (pattern instanceof RegExp) { - // Regex pattern match - if (pattern.test(url)) { - return true - } - } - } - - return false - } -} - -/** - * Normalizes headers for consistent comparison - * - * @param {Object|UndiciHeaders} headers - Headers to normalize - * @returns {NormalizedHeaders} - Normalized headers as a lowercase object - */ -function normalizeHeaders (headers) { - /** @type {NormalizedHeaders} */ - const normalizedHeaders = {} - - if (!headers) return normalizedHeaders - - // Handle array format (undici internal format: [name, value, name, value, ...]) - if (isUndiciHeaders(headers)) { - for (let i = 0; i < headers.length; i += 2) { - const key = headers[i] - const value = headers[i + 1] - if (key && value !== undefined) { - // Convert Buffers to strings if needed - const keyStr = Buffer.isBuffer(key) ? key.toString() : key - const valueStr = Buffer.isBuffer(value) ? value.toString() : value - normalizedHeaders[keyStr.toLowerCase()] = valueStr - } - } - return normalizedHeaders - } - - // Handle object format - if (headers && typeof headers === 'object') { - for (const [key, value] of Object.entries(headers)) { - if (key && typeof key === 'string') { - normalizedHeaders[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value) - } - } - } - - return normalizedHeaders -} - -const validSnapshotModes = /** @type {const} */ (['record', 'playback', 'update']) - -/** @typedef {typeof validSnapshotModes[number]} SnapshotMode */ - -/** - * @param {*} mode - The snapshot mode to validate - * @returns {asserts mode is SnapshotMode} - */ -function validateSnapshotMode (mode) { - if (!validSnapshotModes.includes(mode)) { - throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be one of: ${validSnapshotModes.join(', ')}`) - } -} - -module.exports = { - createHeaderFilters, - hashId, - isUndiciHeaders, - normalizeHeaders, - isUrlExcludedFactory, - validateSnapshotMode -} |
