2025-04-02 06:50:39 -04:00

232 lines
6.1 KiB
JavaScript

'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,
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);
const mockOptions = buildAndValidateMockOptions(opts);
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'
);
}
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) {
let dispatcher = this[kMockAgentGet](origin);
if (!dispatcher) {
dispatcher = this[kFactory](origin);
this[kMockAgentSet](origin, dispatcher);
}
return dispatcher;
}
dispatch(opts, handler) {
// Call MockAgent.get to perform additional setup before dispatching as normal
this.get(opts.origin);
this[kMockAgentAddCallHistoryLog](opts);
return this[kAgent].dispatch(opts, 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, 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 client = this[kClients].get(origin);
if (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;
}
// 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;
}
}
}
[kGetNetConnect]() {
return this[kNetConnect];
}
pendingInterceptors() {
const mockAgentClients = this[kClients];
return Array.from(mockAgentClients.entries())
.flatMap(([origin, scope]) =>
scope[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;