mock-agent.js 7.0 KB

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