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