aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/node_modules/undici/lib/mock/mock-agent.js
blob: 61449e077ead351136b95f61bde5beaa9152ab21 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
'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