mock-interceptor.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. 'use strict'
  2. const { getResponseData, buildKey, addMockDispatch } = require('./mock-utils')
  3. const {
  4. kDispatches,
  5. kDispatchKey,
  6. kDefaultHeaders,
  7. kDefaultTrailers,
  8. kContentLength,
  9. kMockDispatch
  10. } = require('./mock-symbols')
  11. const { InvalidArgumentError } = require('../core/errors')
  12. const { buildURL } = require('../core/util')
  13. /**
  14. * Defines the scope API for an interceptor reply
  15. */
  16. class MockScope {
  17. constructor (mockDispatch) {
  18. this[kMockDispatch] = mockDispatch
  19. }
  20. /**
  21. * Delay a reply by a set amount in ms.
  22. */
  23. delay (waitInMs) {
  24. if (typeof waitInMs !== 'number' || !Number.isInteger(waitInMs) || waitInMs <= 0) {
  25. throw new InvalidArgumentError('waitInMs must be a valid integer > 0')
  26. }
  27. this[kMockDispatch].delay = waitInMs
  28. return this
  29. }
  30. /**
  31. * For a defined reply, never mark as consumed.
  32. */
  33. persist () {
  34. this[kMockDispatch].persist = true
  35. return this
  36. }
  37. /**
  38. * Allow one to define a reply for a set amount of matching requests.
  39. */
  40. times (repeatTimes) {
  41. if (typeof repeatTimes !== 'number' || !Number.isInteger(repeatTimes) || repeatTimes <= 0) {
  42. throw new InvalidArgumentError('repeatTimes must be a valid integer > 0')
  43. }
  44. this[kMockDispatch].times = repeatTimes
  45. return this
  46. }
  47. }
  48. /**
  49. * Defines an interceptor for a Mock
  50. */
  51. class MockInterceptor {
  52. constructor (opts, mockDispatches) {
  53. if (typeof opts !== 'object') {
  54. throw new InvalidArgumentError('opts must be an object')
  55. }
  56. if (typeof opts.path === 'undefined') {
  57. throw new InvalidArgumentError('opts.path must be defined')
  58. }
  59. if (typeof opts.method === 'undefined') {
  60. opts.method = 'GET'
  61. }
  62. // See https://github.com/nodejs/undici/issues/1245
  63. // As per RFC 3986, clients are not supposed to send URI
  64. // fragments to servers when they retrieve a document,
  65. if (typeof opts.path === 'string') {
  66. if (opts.query) {
  67. opts.path = buildURL(opts.path, opts.query)
  68. } else {
  69. // Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811
  70. const parsedURL = new URL(opts.path, 'data://')
  71. opts.path = parsedURL.pathname + parsedURL.search
  72. }
  73. }
  74. if (typeof opts.method === 'string') {
  75. opts.method = opts.method.toUpperCase()
  76. }
  77. this[kDispatchKey] = buildKey(opts)
  78. this[kDispatches] = mockDispatches
  79. this[kDefaultHeaders] = {}
  80. this[kDefaultTrailers] = {}
  81. this[kContentLength] = false
  82. }
  83. createMockScopeDispatchData (statusCode, data, responseOptions = {}) {
  84. const responseData = getResponseData(data)
  85. const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {}
  86. const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers }
  87. const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers }
  88. return { statusCode, data, headers, trailers }
  89. }
  90. validateReplyParameters (statusCode, data, responseOptions) {
  91. if (typeof statusCode === 'undefined') {
  92. throw new InvalidArgumentError('statusCode must be defined')
  93. }
  94. if (typeof data === 'undefined') {
  95. throw new InvalidArgumentError('data must be defined')
  96. }
  97. if (typeof responseOptions !== 'object') {
  98. throw new InvalidArgumentError('responseOptions must be an object')
  99. }
  100. }
  101. /**
  102. * Mock an undici request with a defined reply.
  103. */
  104. reply (replyData) {
  105. // Values of reply aren't available right now as they
  106. // can only be available when the reply callback is invoked.
  107. if (typeof replyData === 'function') {
  108. // We'll first wrap the provided callback in another function,
  109. // this function will properly resolve the data from the callback
  110. // when invoked.
  111. const wrappedDefaultsCallback = (opts) => {
  112. // Our reply options callback contains the parameter for statusCode, data and options.
  113. const resolvedData = replyData(opts)
  114. // Check if it is in the right format
  115. if (typeof resolvedData !== 'object') {
  116. throw new InvalidArgumentError('reply options callback must return an object')
  117. }
  118. const { statusCode, data = '', responseOptions = {} } = resolvedData
  119. this.validateReplyParameters(statusCode, data, responseOptions)
  120. // Since the values can be obtained immediately we return them
  121. // from this higher order function that will be resolved later.
  122. return {
  123. ...this.createMockScopeDispatchData(statusCode, data, responseOptions)
  124. }
  125. }
  126. // Add usual dispatch data, but this time set the data parameter to function that will eventually provide data.
  127. const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback)
  128. return new MockScope(newMockDispatch)
  129. }
  130. // We can have either one or three parameters, if we get here,
  131. // we should have 1-3 parameters. So we spread the arguments of
  132. // this function to obtain the parameters, since replyData will always
  133. // just be the statusCode.
  134. const [statusCode, data = '', responseOptions = {}] = [...arguments]
  135. this.validateReplyParameters(statusCode, data, responseOptions)
  136. // Send in-already provided data like usual
  137. const dispatchData = this.createMockScopeDispatchData(statusCode, data, responseOptions)
  138. const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData)
  139. return new MockScope(newMockDispatch)
  140. }
  141. /**
  142. * Mock an undici request with a defined error.
  143. */
  144. replyWithError (error) {
  145. if (typeof error === 'undefined') {
  146. throw new InvalidArgumentError('error must be defined')
  147. }
  148. const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error })
  149. return new MockScope(newMockDispatch)
  150. }
  151. /**
  152. * Set default reply headers on the interceptor for subsequent replies
  153. */
  154. defaultReplyHeaders (headers) {
  155. if (typeof headers === 'undefined') {
  156. throw new InvalidArgumentError('headers must be defined')
  157. }
  158. this[kDefaultHeaders] = headers
  159. return this
  160. }
  161. /**
  162. * Set default reply trailers on the interceptor for subsequent replies
  163. */
  164. defaultReplyTrailers (trailers) {
  165. if (typeof trailers === 'undefined') {
  166. throw new InvalidArgumentError('trailers must be defined')
  167. }
  168. this[kDefaultTrailers] = trailers
  169. return this
  170. }
  171. /**
  172. * Set reply content length header for replies on the interceptor
  173. */
  174. replyContentLength () {
  175. this[kContentLength] = true
  176. return this
  177. }
  178. }
  179. module.exports.MockInterceptor = MockInterceptor
  180. module.exports.MockScope = MockScope