aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/node_modules/undici/lib/mock
diff options
context:
space:
mode:
Diffstat (limited to 'vanilla/node_modules/undici/lib/mock')
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-agent.js232
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-call-history.js248
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-client.js68
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-errors.js29
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-interceptor.js209
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-pool.js68
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-symbols.js31
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-utils.js480
-rw-r--r--vanilla/node_modules/undici/lib/mock/pending-interceptors-formatter.js43
-rw-r--r--vanilla/node_modules/undici/lib/mock/snapshot-agent.js353
-rw-r--r--vanilla/node_modules/undici/lib/mock/snapshot-recorder.js588
-rw-r--r--vanilla/node_modules/undici/lib/mock/snapshot-utils.js158
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
-}