mock-call-history.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. 'use strict'
  2. const { kMockCallHistoryAddLog } = require('./mock-symbols')
  3. const { InvalidArgumentError } = require('../core/errors')
  4. function handleFilterCallsWithOptions (criteria, options, handler, store) {
  5. switch (options.operator) {
  6. case 'OR':
  7. store.push(...handler(criteria))
  8. return store
  9. case 'AND':
  10. return handler.call({ logs: store }, criteria)
  11. default:
  12. // guard -- should never happens because buildAndValidateFilterCallsOptions is called before
  13. throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'')
  14. }
  15. }
  16. function buildAndValidateFilterCallsOptions (options = {}) {
  17. const finalOptions = {}
  18. if ('operator' in options) {
  19. if (typeof options.operator !== 'string' || (options.operator.toUpperCase() !== 'OR' && options.operator.toUpperCase() !== 'AND')) {
  20. throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'')
  21. }
  22. return {
  23. ...finalOptions,
  24. operator: options.operator.toUpperCase()
  25. }
  26. }
  27. return finalOptions
  28. }
  29. function makeFilterCalls (parameterName) {
  30. return (parameterValue) => {
  31. if (typeof parameterValue === 'string' || parameterValue == null) {
  32. return this.logs.filter((log) => {
  33. return log[parameterName] === parameterValue
  34. })
  35. }
  36. if (parameterValue instanceof RegExp) {
  37. return this.logs.filter((log) => {
  38. return parameterValue.test(log[parameterName])
  39. })
  40. }
  41. throw new InvalidArgumentError(`${parameterName} parameter should be one of string, regexp, undefined or null`)
  42. }
  43. }
  44. function computeUrlWithMaybeSearchParameters (requestInit) {
  45. // path can contains query url parameters
  46. // or query can contains query url parameters
  47. try {
  48. const url = new URL(requestInit.path, requestInit.origin)
  49. // requestInit.path contains query url parameters
  50. // requestInit.query is then undefined
  51. if (url.search.length !== 0) {
  52. return url
  53. }
  54. // requestInit.query can be populated here
  55. url.search = new URLSearchParams(requestInit.query).toString()
  56. return url
  57. } catch (error) {
  58. throw new InvalidArgumentError('An error occurred when computing MockCallHistoryLog.url', { cause: error })
  59. }
  60. }
  61. class MockCallHistoryLog {
  62. constructor (requestInit = {}) {
  63. this.body = requestInit.body
  64. this.headers = requestInit.headers
  65. this.method = requestInit.method
  66. const url = computeUrlWithMaybeSearchParameters(requestInit)
  67. this.fullUrl = url.toString()
  68. this.origin = url.origin
  69. this.path = url.pathname
  70. this.searchParams = Object.fromEntries(url.searchParams)
  71. this.protocol = url.protocol
  72. this.host = url.host
  73. this.port = url.port
  74. this.hash = url.hash
  75. }
  76. toMap () {
  77. return new Map([
  78. ['protocol', this.protocol],
  79. ['host', this.host],
  80. ['port', this.port],
  81. ['origin', this.origin],
  82. ['path', this.path],
  83. ['hash', this.hash],
  84. ['searchParams', this.searchParams],
  85. ['fullUrl', this.fullUrl],
  86. ['method', this.method],
  87. ['body', this.body],
  88. ['headers', this.headers]]
  89. )
  90. }
  91. toString () {
  92. const options = { betweenKeyValueSeparator: '->', betweenPairSeparator: '|' }
  93. let result = ''
  94. this.toMap().forEach((value, key) => {
  95. if (typeof value === 'string' || value === undefined || value === null) {
  96. result = `${result}${key}${options.betweenKeyValueSeparator}${value}${options.betweenPairSeparator}`
  97. }
  98. if ((typeof value === 'object' && value !== null) || Array.isArray(value)) {
  99. result = `${result}${key}${options.betweenKeyValueSeparator}${JSON.stringify(value)}${options.betweenPairSeparator}`
  100. }
  101. // maybe miss something for non Record / Array headers and searchParams here
  102. })
  103. // delete last betweenPairSeparator
  104. return result.slice(0, -1)
  105. }
  106. }
  107. class MockCallHistory {
  108. logs = []
  109. calls () {
  110. return this.logs
  111. }
  112. firstCall () {
  113. return this.logs.at(0)
  114. }
  115. lastCall () {
  116. return this.logs.at(-1)
  117. }
  118. nthCall (number) {
  119. if (typeof number !== 'number') {
  120. throw new InvalidArgumentError('nthCall must be called with a number')
  121. }
  122. if (!Number.isInteger(number)) {
  123. throw new InvalidArgumentError('nthCall must be called with an integer')
  124. }
  125. if (Math.sign(number) !== 1) {
  126. throw new InvalidArgumentError('nthCall must be called with a positive value. use firstCall or lastCall instead')
  127. }
  128. // non zero based index. this is more human readable
  129. return this.logs.at(number - 1)
  130. }
  131. filterCalls (criteria, options) {
  132. // perf
  133. if (this.logs.length === 0) {
  134. return this.logs
  135. }
  136. if (typeof criteria === 'function') {
  137. return this.logs.filter(criteria)
  138. }
  139. if (criteria instanceof RegExp) {
  140. return this.logs.filter((log) => {
  141. return criteria.test(log.toString())
  142. })
  143. }
  144. if (typeof criteria === 'object' && criteria !== null) {
  145. // no criteria - returning all logs
  146. if (Object.keys(criteria).length === 0) {
  147. return this.logs
  148. }
  149. const finalOptions = { operator: 'OR', ...buildAndValidateFilterCallsOptions(options) }
  150. let maybeDuplicatedLogsFiltered = []
  151. if ('protocol' in criteria) {
  152. maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered)
  153. }
  154. if ('host' in criteria) {
  155. maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered)
  156. }
  157. if ('port' in criteria) {
  158. maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered)
  159. }
  160. if ('origin' in criteria) {
  161. maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered)
  162. }
  163. if ('path' in criteria) {
  164. maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered)
  165. }
  166. if ('hash' in criteria) {
  167. maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered)
  168. }
  169. if ('fullUrl' in criteria) {
  170. maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered)
  171. }
  172. if ('method' in criteria) {
  173. maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered)
  174. }
  175. const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)]
  176. return uniqLogsFiltered
  177. }
  178. throw new InvalidArgumentError('criteria parameter should be one of function, regexp, or object')
  179. }
  180. filterCallsByProtocol = makeFilterCalls.call(this, 'protocol')
  181. filterCallsByHost = makeFilterCalls.call(this, 'host')
  182. filterCallsByPort = makeFilterCalls.call(this, 'port')
  183. filterCallsByOrigin = makeFilterCalls.call(this, 'origin')
  184. filterCallsByPath = makeFilterCalls.call(this, 'path')
  185. filterCallsByHash = makeFilterCalls.call(this, 'hash')
  186. filterCallsByFullUrl = makeFilterCalls.call(this, 'fullUrl')
  187. filterCallsByMethod = makeFilterCalls.call(this, 'method')
  188. clear () {
  189. this.logs = []
  190. }
  191. [kMockCallHistoryAddLog] (requestInit) {
  192. const log = new MockCallHistoryLog(requestInit)
  193. this.logs.push(log)
  194. return log
  195. }
  196. * [Symbol.iterator] () {
  197. for (const log of this.calls()) {
  198. yield log
  199. }
  200. }
  201. }
  202. module.exports.MockCallHistory = MockCallHistory
  203. module.exports.MockCallHistoryLog = MockCallHistoryLog