mock-agent.js 4.6 KB

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