mock-agent.js 6.6 KB

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