1'use strict'
2
3const { kClients } = require('../core/symbols')
4const Agent = require('../agent')
5const {
6  kAgent,
7  kMockAgentSet,
8  kMockAgentGet,
9  kDispatches,
10  kIsMockActive,
11  kNetConnect,
12  kGetNetConnect,
13  kOptions,
14  kFactory
15} = require('./mock-symbols')
16const MockClient = require('./mock-client')
17const MockPool = require('./mock-pool')
18const { matchValue, buildMockOptions } = require('./mock-utils')
19const { InvalidArgumentError, UndiciError } = require('../core/errors')
20const Dispatcher = require('../dispatcher')
21const Pluralizer = require('./pluralizer')
22const PendingInterceptorsFormatter = require('./pending-interceptors-formatter')
23
24class FakeWeakRef {
25  constructor (value) {
26    this.value = value
27  }
28
29  deref () {
30    return this.value
31  }
32}
33
34class MockAgent extends Dispatcher {
35  constructor (opts) {
36    super(opts)
37
38    this[kNetConnect] = true
39    this[kIsMockActive] = true
40
41    // Instantiate Agent and encapsulate
42    if ((opts && opts.agent && typeof opts.agent.dispatch !== 'function')) {
43      throw new InvalidArgumentError('Argument opts.agent must implement Agent')
44    }
45    const agent = opts && opts.agent ? opts.agent : new Agent(opts)
46    this[kAgent] = agent
47
48    this[kClients] = agent[kClients]
49    this[kOptions] = buildMockOptions(opts)
50  }
51
52  get (origin) {
53    let dispatcher = this[kMockAgentGet](origin)
54
55    if (!dispatcher) {
56      dispatcher = this[kFactory](origin)
57      this[kMockAgentSet](origin, dispatcher)
58    }
59    return dispatcher
60  }
61
62  dispatch (opts, handler) {
63    // Call MockAgent.get to perform additional setup before dispatching as normal
64    this.get(opts.origin)
65    return this[kAgent].dispatch(opts, handler)
66  }
67
68  async close () {
69    await this[kAgent].close()
70    this[kClients].clear()
71  }
72
73  deactivate () {
74    this[kIsMockActive] = false
75  }
76
77  activate () {
78    this[kIsMockActive] = true
79  }
80
81  enableNetConnect (matcher) {
82    if (typeof matcher === 'string' || typeof matcher === 'function' || matcher instanceof RegExp) {
83      if (Array.isArray(this[kNetConnect])) {
84        this[kNetConnect].push(matcher)
85      } else {
86        this[kNetConnect] = [matcher]
87      }
88    } else if (typeof matcher === 'undefined') {
89      this[kNetConnect] = true
90    } else {
91      throw new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.')
92    }
93  }
94
95  disableNetConnect () {
96    this[kNetConnect] = false
97  }
98
99  // This is required to bypass issues caused by using global symbols - see:
100  // https://github.com/nodejs/undici/issues/1447
101  get isMockActive () {
102    return this[kIsMockActive]
103  }
104
105  [kMockAgentSet] (origin, dispatcher) {
106    this[kClients].set(origin, new FakeWeakRef(dispatcher))
107  }
108
109  [kFactory] (origin) {
110    const mockOptions = Object.assign({ agent: this }, this[kOptions])
111    return this[kOptions] && this[kOptions].connections === 1
112      ? new MockClient(origin, mockOptions)
113      : new MockPool(origin, mockOptions)
114  }
115
116  [kMockAgentGet] (origin) {
117    // First check if we can immediately find it
118    const ref = this[kClients].get(origin)
119    if (ref) {
120      return ref.deref()
121    }
122
123    // If the origin is not a string create a dummy parent pool and return to user
124    if (typeof origin !== 'string') {
125      const dispatcher = this[kFactory]('http://localhost:9999')
126      this[kMockAgentSet](origin, dispatcher)
127      return dispatcher
128    }
129
130    // If we match, create a pool and assign the same dispatches
131    for (const [keyMatcher, nonExplicitRef] of Array.from(this[kClients])) {
132      const nonExplicitDispatcher = nonExplicitRef.deref()
133      if (nonExplicitDispatcher && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) {
134        const dispatcher = this[kFactory](origin)
135        this[kMockAgentSet](origin, dispatcher)
136        dispatcher[kDispatches] = nonExplicitDispatcher[kDispatches]
137        return dispatcher
138      }
139    }
140  }
141
142  [kGetNetConnect] () {
143    return this[kNetConnect]
144  }
145
146  pendingInterceptors () {
147    const mockAgentClients = this[kClients]
148
149    return Array.from(mockAgentClients.entries())
150      .flatMap(([origin, scope]) => scope.deref()[kDispatches].map(dispatch => ({ ...dispatch, origin })))
151      .filter(({ pending }) => pending)
152  }
153
154  assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) {
155    const pending = this.pendingInterceptors()
156
157    if (pending.length === 0) {
158      return
159    }
160
161    const pluralizer = new Pluralizer('interceptor', 'interceptors').pluralize(pending.length)
162
163    throw new UndiciError(`
164${pluralizer.count} ${pluralizer.noun} ${pluralizer.is} pending:
165
166${pendingInterceptorsFormatter.format(pending)}
167`.trim())
168  }
169}
170
171module.exports = MockAgent
172