format: prettify entire project

This commit is contained in:
Rim
2025-04-02 06:50:39 -04:00
parent 86f0782a98
commit 7ccc0be712
1711 changed files with 755867 additions and 235931 deletions

View File

@ -1,7 +1,7 @@
'use strict'
'use strict';
const { kClients } = require('../core/symbols')
const Agent = require('../dispatcher/agent')
const { kClients } = require('../core/symbols');
const Agent = require('../dispatcher/agent');
const {
kAgent,
kMockAgentSet,
@ -16,197 +16,216 @@ const {
kMockAgentIsCallHistoryEnabled,
kMockAgentAddCallHistoryLog,
kMockAgentMockCallHistoryInstance,
kMockCallHistoryAddLog
} = require('./mock-symbols')
const MockClient = require('./mock-client')
const MockPool = require('./mock-pool')
const { matchValue, buildAndValidateMockOptions } = 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')
kMockCallHistoryAddLog,
} = require('./mock-symbols');
const MockClient = require('./mock-client');
const MockPool = require('./mock-pool');
const { matchValue, buildAndValidateMockOptions } = 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)
constructor(opts) {
super(opts);
const mockOptions = buildAndValidateMockOptions(opts)
const mockOptions = buildAndValidateMockOptions(opts);
this[kNetConnect] = true
this[kIsMockActive] = true
this[kMockAgentIsCallHistoryEnabled] = mockOptions?.enableCallHistory ?? false
this[kNetConnect] = true;
this[kIsMockActive] = true;
this[kMockAgentIsCallHistoryEnabled] =
mockOptions?.enableCallHistory ?? false;
// Instantiate Agent and encapsulate
if (opts?.agent && typeof opts.agent.dispatch !== 'function') {
throw new InvalidArgumentError('Argument opts.agent must implement Agent')
throw new InvalidArgumentError(
'Argument opts.agent must implement Agent'
);
}
const agent = opts?.agent ? opts.agent : new Agent(opts)
this[kAgent] = agent
const agent = opts?.agent ? opts.agent : new Agent(opts);
this[kAgent] = agent;
this[kClients] = agent[kClients]
this[kOptions] = mockOptions
this[kClients] = agent[kClients];
this[kOptions] = mockOptions;
if (this[kMockAgentIsCallHistoryEnabled]) {
this[kMockAgentRegisterCallHistory]()
this[kMockAgentRegisterCallHistory]();
}
}
get (origin) {
let dispatcher = this[kMockAgentGet](origin)
get(origin) {
let dispatcher = this[kMockAgentGet](origin);
if (!dispatcher) {
dispatcher = this[kFactory](origin)
this[kMockAgentSet](origin, dispatcher)
dispatcher = this[kFactory](origin);
this[kMockAgentSet](origin, dispatcher);
}
return dispatcher
return dispatcher;
}
dispatch (opts, handler) {
dispatch(opts, handler) {
// Call MockAgent.get to perform additional setup before dispatching as normal
this.get(opts.origin)
this.get(opts.origin);
this[kMockAgentAddCallHistoryLog](opts)
this[kMockAgentAddCallHistoryLog](opts);
return this[kAgent].dispatch(opts, handler)
return this[kAgent].dispatch(opts, handler);
}
async close () {
this.clearCallHistory()
await this[kAgent].close()
this[kClients].clear()
async close() {
this.clearCallHistory();
await this[kAgent].close();
this[kClients].clear();
}
deactivate () {
this[kIsMockActive] = false
deactivate() {
this[kIsMockActive] = false;
}
activate () {
this[kIsMockActive] = true
activate() {
this[kIsMockActive] = true;
}
enableNetConnect (matcher) {
if (typeof matcher === 'string' || typeof matcher === 'function' || matcher instanceof RegExp) {
enableNetConnect(matcher) {
if (
typeof matcher === 'string' ||
typeof matcher === 'function' ||
matcher instanceof RegExp
) {
if (Array.isArray(this[kNetConnect])) {
this[kNetConnect].push(matcher)
this[kNetConnect].push(matcher);
} else {
this[kNetConnect] = [matcher]
this[kNetConnect] = [matcher];
}
} else if (typeof matcher === 'undefined') {
this[kNetConnect] = true
this[kNetConnect] = true;
} else {
throw new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.')
throw new InvalidArgumentError(
'Unsupported matcher. Must be one of String|Function|RegExp.'
);
}
}
disableNetConnect () {
this[kNetConnect] = false
disableNetConnect() {
this[kNetConnect] = false;
}
enableCallHistory () {
this[kMockAgentIsCallHistoryEnabled] = true
enableCallHistory() {
this[kMockAgentIsCallHistoryEnabled] = true;
return this
return this;
}
disableCallHistory () {
this[kMockAgentIsCallHistoryEnabled] = false
disableCallHistory() {
this[kMockAgentIsCallHistoryEnabled] = false;
return this
return this;
}
getCallHistory () {
return this[kMockAgentMockCallHistoryInstance]
getCallHistory() {
return this[kMockAgentMockCallHistoryInstance];
}
clearCallHistory () {
clearCallHistory() {
if (this[kMockAgentMockCallHistoryInstance] !== undefined) {
this[kMockAgentMockCallHistoryInstance].clear()
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]
get isMockActive() {
return this[kIsMockActive];
}
[kMockAgentRegisterCallHistory] () {
[kMockAgentRegisterCallHistory]() {
if (this[kMockAgentMockCallHistoryInstance] === undefined) {
this[kMockAgentMockCallHistoryInstance] = new MockCallHistory()
this[kMockAgentMockCallHistoryInstance] = new MockCallHistory();
}
}
[kMockAgentAddCallHistoryLog] (opts) {
[kMockAgentAddCallHistoryLog](opts) {
if (this[kMockAgentIsCallHistoryEnabled]) {
// additional setup when enableCallHistory class method is used after mockAgent instantiation
this[kMockAgentRegisterCallHistory]()
this[kMockAgentRegisterCallHistory]();
// add call history log on every call (intercepted or not)
this[kMockAgentMockCallHistoryInstance][kMockCallHistoryAddLog](opts)
this[kMockAgentMockCallHistoryInstance][kMockCallHistoryAddLog](opts);
}
}
[kMockAgentSet] (origin, dispatcher) {
this[kClients].set(origin, dispatcher)
[kMockAgentSet](origin, dispatcher) {
this[kClients].set(origin, 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)
[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) {
[kMockAgentGet](origin) {
// First check if we can immediately find it
const client = this[kClients].get(origin)
const client = this[kClients].get(origin);
if (client) {
return client
return client;
}
// 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
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, nonExplicitDispatcher] of Array.from(this[kClients])) {
if (nonExplicitDispatcher && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) {
const dispatcher = this[kFactory](origin)
this[kMockAgentSet](origin, dispatcher)
dispatcher[kDispatches] = nonExplicitDispatcher[kDispatches]
return dispatcher
for (const [keyMatcher, nonExplicitDispatcher] of Array.from(
this[kClients]
)) {
if (
nonExplicitDispatcher &&
typeof keyMatcher !== 'string' &&
matchValue(keyMatcher, origin)
) {
const dispatcher = this[kFactory](origin);
this[kMockAgentSet](origin, dispatcher);
dispatcher[kDispatches] = nonExplicitDispatcher[kDispatches];
return dispatcher;
}
}
}
[kGetNetConnect] () {
return this[kNetConnect]
[kGetNetConnect]() {
return this[kNetConnect];
}
pendingInterceptors () {
const mockAgentClients = this[kClients]
pendingInterceptors() {
const mockAgentClients = this[kClients];
return Array.from(mockAgentClients.entries())
.flatMap(([origin, scope]) => scope[kDispatches].map(dispatch => ({ ...dispatch, origin })))
.filter(({ pending }) => pending)
.flatMap(([origin, scope]) =>
scope[kDispatches].map((dispatch) => ({ ...dispatch, origin }))
)
.filter(({ pending }) => pending);
}
assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) {
const pending = this.pendingInterceptors()
assertNoPendingInterceptors({
pendingInterceptorsFormatter = new PendingInterceptorsFormatter(),
} = {}) {
const pending = this.pendingInterceptors();
if (pending.length === 0) {
return
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()
)
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
module.exports = MockAgent;

View File

@ -1,95 +1,108 @@
'use strict'
'use strict';
const { kMockCallHistoryAddLog } = require('./mock-symbols')
const { InvalidArgumentError } = require('../core/errors')
const { kMockCallHistoryAddLog } = require('./mock-symbols');
const { InvalidArgumentError } = require('../core/errors');
function handleFilterCallsWithOptions (criteria, options, handler, store) {
function handleFilterCallsWithOptions(criteria, options, handler, store) {
switch (options.operator) {
case 'OR':
store.push(...handler(criteria))
store.push(...handler(criteria));
return store
return store;
case 'AND':
return handler.call({ logs: store }, criteria)
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\'')
throw new InvalidArgumentError(
"options.operator must to be a case insensitive string equal to 'OR' or 'AND'"
);
}
}
function buildAndValidateFilterCallsOptions (options = {}) {
const finalOptions = {}
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\'')
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()
}
operator: options.operator.toUpperCase(),
};
}
return finalOptions
return finalOptions;
}
function makeFilterCalls (parameterName) {
function makeFilterCalls(parameterName) {
return (parameterValue) => {
if (typeof parameterValue === 'string' || parameterValue == null) {
return this.logs.filter((log) => {
return log[parameterName] === parameterValue
})
return log[parameterName] === parameterValue;
});
}
if (parameterValue instanceof RegExp) {
return this.logs.filter((log) => {
return parameterValue.test(log[parameterName])
})
return parameterValue.test(log[parameterName]);
});
}
throw new InvalidArgumentError(`${parameterName} parameter should be one of string, regexp, undefined or null`)
}
throw new InvalidArgumentError(
`${parameterName} parameter should be one of string, regexp, undefined or null`
);
};
}
function computeUrlWithMaybeSearchParameters (requestInit) {
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)
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
return url;
}
// requestInit.query can be populated here
url.search = new URLSearchParams(requestInit.query).toString()
url.search = new URLSearchParams(requestInit.query).toString();
return url
return url;
} catch (error) {
throw new InvalidArgumentError('An error occurred when computing MockCallHistoryLog.url', { cause: 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
constructor(requestInit = {}) {
this.body = requestInit.body;
this.headers = requestInit.headers;
this.method = requestInit.method;
const url = computeUrlWithMaybeSearchParameters(requestInit)
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
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 () {
toMap() {
return new Map([
['protocol', this.protocol],
['host', this.host],
@ -101,148 +114,201 @@ class MockCallHistoryLog {
['fullUrl', this.fullUrl],
['method', this.method],
['body', this.body],
['headers', this.headers]]
)
['headers', this.headers],
]);
}
toString () {
const options = { betweenKeyValueSeparator: '->', betweenPairSeparator: '|' }
let result = ''
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}`
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}`
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)
return result.slice(0, -1);
}
}
class MockCallHistory {
logs = []
logs = [];
calls () {
return this.logs
calls() {
return this.logs;
}
firstCall () {
return this.logs.at(0)
firstCall() {
return this.logs.at(0);
}
lastCall () {
return this.logs.at(-1)
lastCall() {
return this.logs.at(-1);
}
nthCall (number) {
nthCall(number) {
if (typeof number !== 'number') {
throw new InvalidArgumentError('nthCall must be called with a 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')
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')
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)
return this.logs.at(number - 1);
}
filterCalls (criteria, options) {
filterCalls(criteria, options) {
// perf
if (this.logs.length === 0) {
return this.logs
return this.logs;
}
if (typeof criteria === 'function') {
return this.logs.filter(criteria)
return this.logs.filter(criteria);
}
if (criteria instanceof RegExp) {
return this.logs.filter((log) => {
return criteria.test(log.toString())
})
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
return this.logs;
}
const finalOptions = { operator: 'OR', ...buildAndValidateFilterCallsOptions(options) }
const finalOptions = {
operator: 'OR',
...buildAndValidateFilterCallsOptions(options),
};
let maybeDuplicatedLogsFiltered = []
let maybeDuplicatedLogsFiltered = [];
if ('protocol' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(
criteria.protocol,
finalOptions,
this.filterCallsByProtocol,
maybeDuplicatedLogsFiltered
);
}
if ('host' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(
criteria.host,
finalOptions,
this.filterCallsByHost,
maybeDuplicatedLogsFiltered
);
}
if ('port' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(
criteria.port,
finalOptions,
this.filterCallsByPort,
maybeDuplicatedLogsFiltered
);
}
if ('origin' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(
criteria.origin,
finalOptions,
this.filterCallsByOrigin,
maybeDuplicatedLogsFiltered
);
}
if ('path' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(
criteria.path,
finalOptions,
this.filterCallsByPath,
maybeDuplicatedLogsFiltered
);
}
if ('hash' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(
criteria.hash,
finalOptions,
this.filterCallsByHash,
maybeDuplicatedLogsFiltered
);
}
if ('fullUrl' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(
criteria.fullUrl,
finalOptions,
this.filterCallsByFullUrl,
maybeDuplicatedLogsFiltered
);
}
if ('method' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(
criteria.method,
finalOptions,
this.filterCallsByMethod,
maybeDuplicatedLogsFiltered
);
}
const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)]
const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)];
return uniqLogsFiltered
return uniqLogsFiltered;
}
throw new InvalidArgumentError('criteria parameter should be one of function, regexp, or object')
throw new InvalidArgumentError(
'criteria parameter should be one of function, regexp, or object'
);
}
filterCallsByProtocol = makeFilterCalls.call(this, 'protocol')
filterCallsByProtocol = makeFilterCalls.call(this, 'protocol');
filterCallsByHost = makeFilterCalls.call(this, 'host')
filterCallsByHost = makeFilterCalls.call(this, 'host');
filterCallsByPort = makeFilterCalls.call(this, 'port')
filterCallsByPort = makeFilterCalls.call(this, 'port');
filterCallsByOrigin = makeFilterCalls.call(this, 'origin')
filterCallsByOrigin = makeFilterCalls.call(this, 'origin');
filterCallsByPath = makeFilterCalls.call(this, 'path')
filterCallsByPath = makeFilterCalls.call(this, 'path');
filterCallsByHash = makeFilterCalls.call(this, 'hash')
filterCallsByHash = makeFilterCalls.call(this, 'hash');
filterCallsByFullUrl = makeFilterCalls.call(this, 'fullUrl')
filterCallsByFullUrl = makeFilterCalls.call(this, 'fullUrl');
filterCallsByMethod = makeFilterCalls.call(this, 'method')
filterCallsByMethod = makeFilterCalls.call(this, 'method');
clear () {
this.logs = []
clear() {
this.logs = [];
}
[kMockCallHistoryAddLog] (requestInit) {
const log = new MockCallHistoryLog(requestInit)
[kMockCallHistoryAddLog](requestInit) {
const log = new MockCallHistoryLog(requestInit);
this.logs.push(log)
this.logs.push(log);
return log
return log;
}
* [Symbol.iterator] () {
*[Symbol.iterator]() {
for (const log of this.calls()) {
yield log
yield log;
}
}
}
module.exports.MockCallHistory = MockCallHistory
module.exports.MockCallHistoryLog = MockCallHistoryLog
module.exports.MockCallHistory = MockCallHistory;
module.exports.MockCallHistoryLog = MockCallHistoryLog;

View File

@ -1,8 +1,8 @@
'use strict'
'use strict';
const { promisify } = require('node:util')
const Client = require('../dispatcher/client')
const { buildMockDispatch } = require('./mock-utils')
const { promisify } = require('node:util');
const Client = require('../dispatcher/client');
const { buildMockDispatch } = require('./mock-utils');
const {
kDispatches,
kMockAgent,
@ -11,54 +11,56 @@ const {
kOrigin,
kOriginalDispatch,
kConnected,
kIgnoreTrailingSlash
} = require('./mock-symbols')
const { MockInterceptor } = require('./mock-interceptor')
const Symbols = require('../core/symbols')
const { InvalidArgumentError } = require('../core/errors')
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) {
constructor(origin, opts) {
if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') {
throw new InvalidArgumentError('Argument opts.agent must implement Agent')
throw new InvalidArgumentError(
'Argument opts.agent must implement Agent'
);
}
super(origin, opts)
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[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]
this.dispatch = buildMockDispatch.call(this);
this.close = this[kClose];
}
get [Symbols.kConnected] () {
return this[kConnected]
get [Symbols.kConnected]() {
return this[kConnected];
}
/**
* Sets up the base interceptor for mocking replies from undici.
*/
intercept (opts) {
intercept(opts) {
return new MockInterceptor(
opts && { ignoreTrailingSlash: this[kIgnoreTrailingSlash], ...opts },
this[kDispatches]
)
);
}
async [kClose] () {
await promisify(this[kOriginalClose])()
this[kConnected] = 0
this[kMockAgent][Symbols.kClients].delete(this[kOrigin])
async [kClose]() {
await promisify(this[kOriginalClose])();
this[kConnected] = 0;
this[kMockAgent][Symbols.kClients].delete(this[kOrigin]);
}
}
module.exports = MockClient
module.exports = MockClient;

View File

@ -1,19 +1,20 @@
'use strict'
'use strict';
const { UndiciError } = require('../core/errors')
const { UndiciError } = require('../core/errors');
/**
* 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'
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';
}
}
module.exports = {
MockNotMatchedError
}
MockNotMatchedError,
};

View File

@ -1,6 +1,6 @@
'use strict'
'use strict';
const { getResponseData, buildKey, addMockDispatch } = require('./mock-utils')
const { getResponseData, buildKey, addMockDispatch } = require('./mock-utils');
const {
kDispatches,
kDispatchKey,
@ -8,49 +8,57 @@ const {
kDefaultTrailers,
kContentLength,
kMockDispatch,
kIgnoreTrailingSlash
} = require('./mock-symbols')
const { InvalidArgumentError } = require('../core/errors')
const { serializePathWithQuery } = require('../core/util')
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
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')
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
this[kMockDispatch].delay = waitInMs;
return this;
}
/**
* For a defined reply, never mark as consumed.
*/
persist () {
this[kMockDispatch].persist = true
return this
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')
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
this[kMockDispatch].times = repeatTimes;
return this;
}
}
@ -58,62 +66,70 @@ class MockScope {
* Defines an interceptor for a Mock
*/
class MockInterceptor {
constructor (opts, mockDispatches) {
constructor(opts, mockDispatches) {
if (typeof opts !== 'object') {
throw new InvalidArgumentError('opts must be an object')
throw new InvalidArgumentError('opts must be an object');
}
if (typeof opts.path === 'undefined') {
throw new InvalidArgumentError('opts.path must be defined')
throw new InvalidArgumentError('opts.path must be defined');
}
if (typeof opts.method === 'undefined') {
opts.method = 'GET'
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)
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
const parsedURL = new URL(opts.path, 'data://');
opts.path = parsedURL.pathname + parsedURL.search;
}
}
if (typeof opts.method === 'string') {
opts.method = opts.method.toUpperCase()
opts.method = opts.method.toUpperCase();
}
this[kDispatchKey] = buildKey(opts)
this[kDispatches] = mockDispatches
this[kIgnoreTrailingSlash] = opts.ignoreTrailingSlash ?? false
this[kDefaultHeaders] = {}
this[kDefaultTrailers] = {}
this[kContentLength] = false
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 }
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 }
return { statusCode, data, headers, trailers };
}
validateReplyParameters (replyParameters) {
validateReplyParameters(replyParameters) {
if (typeof replyParameters.statusCode === 'undefined') {
throw new InvalidArgumentError('statusCode must be defined')
throw new InvalidArgumentError('statusCode must be defined');
}
if (typeof replyParameters.responseOptions !== 'object' || replyParameters.responseOptions === null) {
throw new InvalidArgumentError('responseOptions must be an object')
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) {
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') {
@ -122,25 +138,36 @@ class MockInterceptor {
// when invoked.
const wrappedDefaultsCallback = (opts) => {
// Our reply options callback contains the parameter for statusCode, data and options.
const resolvedData = replyOptionsCallbackOrStatusCode(opts)
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')
throw new InvalidArgumentError(
'reply options callback must return an object'
);
}
const replyParameters = { data: '', responseOptions: {}, ...resolvedData }
this.validateReplyParameters(replyParameters)
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)
}
}
...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)
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,
@ -150,60 +177,70 @@ class MockInterceptor {
const replyParameters = {
statusCode: replyOptionsCallbackOrStatusCode,
data: arguments[1] === undefined ? '' : arguments[1],
responseOptions: arguments[2] === undefined ? {} : arguments[2]
}
this.validateReplyParameters(replyParameters)
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)
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) {
replyWithError(error) {
if (typeof error === 'undefined') {
throw new InvalidArgumentError('error must be defined')
throw new InvalidArgumentError('error must be defined');
}
const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error }, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] })
return new MockScope(newMockDispatch)
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) {
defaultReplyHeaders(headers) {
if (typeof headers === 'undefined') {
throw new InvalidArgumentError('headers must be defined')
throw new InvalidArgumentError('headers must be defined');
}
this[kDefaultHeaders] = headers
return this
this[kDefaultHeaders] = headers;
return this;
}
/**
* Set default reply trailers on the interceptor for subsequent replies
*/
defaultReplyTrailers (trailers) {
defaultReplyTrailers(trailers) {
if (typeof trailers === 'undefined') {
throw new InvalidArgumentError('trailers must be defined')
throw new InvalidArgumentError('trailers must be defined');
}
this[kDefaultTrailers] = trailers
return this
this[kDefaultTrailers] = trailers;
return this;
}
/**
* Set reply content length header for replies on the interceptor
*/
replyContentLength () {
this[kContentLength] = true
return this
replyContentLength() {
this[kContentLength] = true;
return this;
}
}
module.exports.MockInterceptor = MockInterceptor
module.exports.MockScope = MockScope
module.exports.MockInterceptor = MockInterceptor;
module.exports.MockScope = MockScope;

View File

@ -1,8 +1,8 @@
'use strict'
'use strict';
const { promisify } = require('node:util')
const Pool = require('../dispatcher/pool')
const { buildMockDispatch } = require('./mock-utils')
const { promisify } = require('node:util');
const Pool = require('../dispatcher/pool');
const { buildMockDispatch } = require('./mock-utils');
const {
kDispatches,
kMockAgent,
@ -11,54 +11,56 @@ const {
kOrigin,
kOriginalDispatch,
kConnected,
kIgnoreTrailingSlash
} = require('./mock-symbols')
const { MockInterceptor } = require('./mock-interceptor')
const Symbols = require('../core/symbols')
const { InvalidArgumentError } = require('../core/errors')
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) {
constructor(origin, opts) {
if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') {
throw new InvalidArgumentError('Argument opts.agent must implement Agent')
throw new InvalidArgumentError(
'Argument opts.agent must implement Agent'
);
}
super(origin, opts)
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[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]
this.dispatch = buildMockDispatch.call(this);
this.close = this[kClose];
}
get [Symbols.kConnected] () {
return this[kConnected]
get [Symbols.kConnected]() {
return this[kConnected];
}
/**
* Sets up the base interceptor for mocking replies from undici.
*/
intercept (opts) {
intercept(opts) {
return new MockInterceptor(
opts && { ignoreTrailingSlash: this[kIgnoreTrailingSlash], ...opts },
this[kDispatches]
)
);
}
async [kClose] () {
await promisify(this[kOriginalClose])()
this[kConnected] = 0
this[kMockAgent][Symbols.kClients].delete(this[kOrigin])
async [kClose]() {
await promisify(this[kOriginalClose])();
this[kConnected] = 0;
this[kMockAgent][Symbols.kClients].delete(this[kOrigin]);
}
}
module.exports = MockPool
module.exports = MockPool;

View File

@ -1,4 +1,4 @@
'use strict'
'use strict';
module.exports = {
kAgent: Symbol('agent'),
@ -22,9 +22,13 @@ module.exports = {
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'),
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'),
kMockCallHistoryAddLog: Symbol('mock call history add log')
}
kMockCallHistoryAddLog: Symbol('mock call history add log'),
};

View File

@ -1,305 +1,353 @@
'use strict'
'use strict';
const { MockNotMatchedError } = require('./mock-errors')
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')
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')
types: { isPromise },
} = require('node:util');
const { InvalidArgumentError } = require('../core/errors');
function matchValue (match, value) {
function matchValue(match, value) {
if (typeof match === 'string') {
return match === value
return match === value;
}
if (match instanceof RegExp) {
return match.test(value)
return match.test(value);
}
if (typeof match === 'function') {
return match(value) === true
return match(value) === true;
}
return false
return false;
}
function lowerCaseEntries (headers) {
function lowerCaseEntries(headers) {
return Object.fromEntries(
Object.entries(headers).map(([headerName, headerValue]) => {
return [headerName.toLocaleLowerCase(), headerValue]
return [headerName.toLocaleLowerCase(), headerValue];
})
)
);
}
/**
* @param {import('../../index').Headers|string[]|Record<string, string>} headers
* @param {string} key
*/
function getHeaderByName (headers, 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 headers[i + 1];
}
}
return undefined
return undefined;
} else if (typeof headers.get === 'function') {
return headers.get(key)
return headers.get(key);
} else {
return lowerCaseEntries(headers)[key.toLocaleLowerCase()]
return lowerCaseEntries(headers)[key.toLocaleLowerCase()];
}
}
/** @param {string[]} headers */
function buildHeadersFromArray (headers) { // fetch HeadersList
const clone = headers.slice()
const entries = []
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]])
entries.push([clone[index], clone[index + 1]]);
}
return Object.fromEntries(entries)
return Object.fromEntries(entries);
}
function matchHeaders (mockDispatch, headers) {
function matchHeaders(mockDispatch, headers) {
if (typeof mockDispatch.headers === 'function') {
if (Array.isArray(headers)) { // fetch HeadersList
headers = buildHeadersFromArray(headers)
if (Array.isArray(headers)) {
// fetch HeadersList
headers = buildHeadersFromArray(headers);
}
return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {})
return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {});
}
if (typeof mockDispatch.headers === 'undefined') {
return true
return true;
}
if (typeof headers !== 'object' || typeof mockDispatch.headers !== 'object') {
return false
return false;
}
for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) {
const headerValue = getHeaderByName(headers, matchHeaderName)
for (const [matchHeaderName, matchHeaderValue] of Object.entries(
mockDispatch.headers
)) {
const headerValue = getHeaderByName(headers, matchHeaderName);
if (!matchValue(matchHeaderValue, headerValue)) {
return false
return false;
}
}
return true
return true;
}
function safeUrl (path) {
function safeUrl(path) {
if (typeof path !== 'string') {
return path
return path;
}
const pathSegments = path.split('?', 3)
const pathSegments = path.split('?', 3);
if (pathSegments.length !== 2) {
return path
return path;
}
const qp = new URLSearchParams(pathSegments.pop())
qp.sort()
return [...pathSegments, qp.toString()].join('?')
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 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) {
function getResponseData(data) {
if (Buffer.isBuffer(data)) {
return data
return data;
} else if (data instanceof Uint8Array) {
return data
return data;
} else if (data instanceof ArrayBuffer) {
return data
return data;
} else if (typeof data === 'object') {
return JSON.stringify(data)
return JSON.stringify(data);
} else if (data) {
return data.toString()
return data.toString();
} else {
return ''
return '';
}
}
function getMockDispatch (mockDispatches, key) {
const basePath = key.query ? serializePathWithQuery(key.path, key.query) : key.path
const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath
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)
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)
})
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}'`)
throw new MockNotMatchedError(
`Mock dispatch not matched for path '${resolvedPath}'`
);
}
// Match method
matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.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}'`)
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)
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}'`)
throw new MockNotMatchedError(
`Mock dispatch not matched for body '${key.body}' on path '${resolvedPath}'`
);
}
// Match headers
matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.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}'`)
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]
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 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 => {
function deleteMockDispatch(mockDispatches, key) {
const index = mockDispatches.findIndex((dispatch) => {
if (!dispatch.consumed) {
return false
return false;
}
return matchKey(dispatch, key)
})
return matchKey(dispatch, key);
});
if (index !== -1) {
mockDispatches.splice(index, 1)
mockDispatches.splice(index, 1);
}
}
/**
* @param {string} path Path to remove trailing slash from
*/
function removeTrailingSlash (path) {
function removeTrailingSlash(path) {
while (path.endsWith('/')) {
path = path.slice(0, -1)
path = path.slice(0, -1);
}
if (path.length === 0) {
path = '/'
path = '/';
}
return path
return path;
}
function buildKey (opts) {
const { path, method, body, headers, query } = opts
function buildKey(opts) {
const { path, method, body, headers, query } = opts;
return {
path,
method,
body,
headers,
query
}
query,
};
}
function generateKeyValues (data) {
const keys = Object.keys(data)
const result = []
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}`)
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]}`))
result.push(name, Buffer.from(`${value[j]}`));
}
} else {
result.push(name, Buffer.from(`${value}`))
result.push(name, Buffer.from(`${value}`));
}
}
return result
return result;
}
/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
* @param {number} statusCode
*/
function getStatusText (statusCode) {
return STATUS_CODES[statusCode] || 'unknown'
function getStatusText(statusCode) {
return STATUS_CODES[statusCode] || 'unknown';
}
async function getResponse (body) {
const buffers = []
async function getResponse(body) {
const buffers = [];
for await (const data of body) {
buffers.push(data)
buffers.push(data);
}
return Buffer.concat(buffers).toString('utf8')
return Buffer.concat(buffers).toString('utf8');
}
/**
* Mock dispatch function used to simulate undici dispatches
*/
function mockDispatch (opts, handler) {
function mockDispatch(opts, handler) {
// Get mock dispatch from built key
const key = buildKey(opts)
const mockDispatch = getMockDispatch(this[kDispatches], key)
const key = buildKey(opts);
const mockDispatch = getMockDispatch(this[kDispatches], key);
mockDispatch.timesInvoked++
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) }
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
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
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
deleteMockDispatch(this[kDispatches], key);
handler.onError(error);
return true;
}
// Handle the request with a delay if necessary
if (typeof delay === 'number' && delay > 0) {
setTimeout(() => {
handleReply(this[kDispatches])
}, delay)
handleReply(this[kDispatches]);
}, delay);
} else {
handleReply(this[kDispatches])
handleReply(this[kDispatches]);
}
function handleReply (mockDispatches, _data = data) {
function handleReply(mockDispatches, _data = data) {
// fetch's HeadersList is a 1D string array
const optsHeaders = Array.isArray(opts.headers)
? buildHeadersFromArray(opts.headers)
: opts.headers
const body = typeof _data === 'function'
? _data({ ...opts, headers: optsHeaders })
: _data
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)) {
@ -308,75 +356,92 @@ function mockDispatch (opts, handler) {
// synchronously throw the error, which breaks some tests.
// Rather, we wait for the callback to resolve if it is a
// promise, and then re-run handleReply with the new body.
body.then((newData) => handleReply(mockDispatches, newData))
return
body.then((newData) => handleReply(mockDispatches, newData));
return;
}
const responseData = getResponseData(body)
const responseHeaders = generateKeyValues(headers)
const responseTrailers = generateKeyValues(trailers)
const responseData = getResponseData(body);
const responseHeaders = generateKeyValues(headers);
const responseTrailers = generateKeyValues(trailers);
handler.onConnect?.(err => handler.onError(err), null)
handler.onHeaders?.(statusCode, responseHeaders, resume, getStatusText(statusCode))
handler.onData?.(Buffer.from(responseData))
handler.onComplete?.(responseTrailers)
deleteMockDispatch(mockDispatches, key)
handler.onConnect?.((err) => handler.onError(err), null);
handler.onHeaders?.(
statusCode,
responseHeaders,
resume,
getStatusText(statusCode)
);
handler.onData?.(Buffer.from(responseData));
handler.onComplete?.(responseTrailers);
deleteMockDispatch(mockDispatches, key);
}
function resume () {}
function resume() {}
return true
return true;
}
function buildMockDispatch () {
const agent = this[kMockAgent]
const origin = this[kOrigin]
const originalDispatch = this[kOriginalDispatch]
function buildMockDispatch() {
const agent = this[kMockAgent];
const origin = this[kOrigin];
const originalDispatch = this[kOriginalDispatch];
return function dispatch (opts, handler) {
return function dispatch(opts, handler) {
if (agent.isMockActive) {
try {
mockDispatch.call(this, opts, handler)
mockDispatch.call(this, opts, handler);
} catch (error) {
if (error instanceof MockNotMatchedError) {
const netConnect = agent[kGetNetConnect]()
const netConnect = agent[kGetNetConnect]();
if (netConnect === false) {
throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`)
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)
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)`)
throw new MockNotMatchedError(
`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`
);
}
} else {
throw error
throw error;
}
}
} else {
originalDispatch.call(this, opts, handler)
originalDispatch.call(this, opts, handler);
}
}
};
}
function checkNetConnect (netConnect, origin) {
const url = new URL(origin)
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 true;
} else if (
Array.isArray(netConnect) &&
netConnect.some((matcher) => matchValue(matcher, url.host))
) {
return true;
}
return false
return false;
}
function buildAndValidateMockOptions (opts) {
function buildAndValidateMockOptions(opts) {
if (opts) {
const { agent, ...mockOptions } = opts
const { agent, ...mockOptions } = opts;
if ('enableCallHistory' in mockOptions && typeof mockOptions.enableCallHistory !== 'boolean') {
throw new InvalidArgumentError('options.enableCallHistory must to be a boolean')
if (
'enableCallHistory' in mockOptions &&
typeof mockOptions.enableCallHistory !== 'boolean'
) {
throw new InvalidArgumentError(
'options.enableCallHistory must to be a boolean'
);
}
return mockOptions
return mockOptions;
}
}
@ -395,5 +460,5 @@ module.exports = {
checkNetConnect,
buildAndValidateMockOptions,
getHeaderByName,
buildHeadersFromArray
}
buildHeadersFromArray,
};

View File

@ -1,43 +1,52 @@
'use strict'
'use strict';
const { Transform } = require('node:stream')
const { Console } = require('node:console')
const { Transform } = require('node:stream');
const { Console } = require('node:console');
const PERSISTENT = process.versions.icu ? '✅' : 'Y '
const NOT_PERSISTENT = process.versions.icu ? '❌' : 'N '
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 } = {}) {
constructor({ disableColors } = {}) {
this.transform = new Transform({
transform (chunk, _enc, cb) {
cb(null, chunk)
}
})
transform(chunk, _enc, cb) {
cb(null, chunk);
},
});
this.logger = new Console({
stdout: this.transform,
inspectOptions: {
colors: !disableColors && !process.env.CI
}
})
colors: !disableColors && !process.env.CI,
},
});
}
format (pendingInterceptors) {
format(pendingInterceptors) {
const withPrettyHeaders = pendingInterceptors.map(
({ method, path, data: { statusCode }, persist, times, timesInvoked, origin }) => ({
({
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
}))
Remaining: persist ? Infinity : times - timesInvoked,
})
);
this.logger.table(withPrettyHeaders)
return this.transform.read().toString()
this.logger.table(withPrettyHeaders);
return this.transform.read().toString();
}
}
};