source: node_modules/undici/lib/mock/mock-utils.js@ d24f17c

main
Last change on this file since d24f17c was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 10.0 KB
Line 
1'use strict'
2
3const { MockNotMatchedError } = require('./mock-errors')
4const {
5 kDispatches,
6 kMockAgent,
7 kOriginalDispatch,
8 kOrigin,
9 kGetNetConnect
10} = require('./mock-symbols')
11const { buildURL, nop } = require('../core/util')
12const { STATUS_CODES } = require('http')
13const {
14 types: {
15 isPromise
16 }
17} = require('util')
18
19function matchValue (match, value) {
20 if (typeof match === 'string') {
21 return match === value
22 }
23 if (match instanceof RegExp) {
24 return match.test(value)
25 }
26 if (typeof match === 'function') {
27 return match(value) === true
28 }
29 return false
30}
31
32function lowerCaseEntries (headers) {
33 return Object.fromEntries(
34 Object.entries(headers).map(([headerName, headerValue]) => {
35 return [headerName.toLocaleLowerCase(), headerValue]
36 })
37 )
38}
39
40/**
41 * @param {import('../../index').Headers|string[]|Record<string, string>} headers
42 * @param {string} key
43 */
44function getHeaderByName (headers, key) {
45 if (Array.isArray(headers)) {
46 for (let i = 0; i < headers.length; i += 2) {
47 if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) {
48 return headers[i + 1]
49 }
50 }
51
52 return undefined
53 } else if (typeof headers.get === 'function') {
54 return headers.get(key)
55 } else {
56 return lowerCaseEntries(headers)[key.toLocaleLowerCase()]
57 }
58}
59
60/** @param {string[]} headers */
61function buildHeadersFromArray (headers) { // fetch HeadersList
62 const clone = headers.slice()
63 const entries = []
64 for (let index = 0; index < clone.length; index += 2) {
65 entries.push([clone[index], clone[index + 1]])
66 }
67 return Object.fromEntries(entries)
68}
69
70function matchHeaders (mockDispatch, headers) {
71 if (typeof mockDispatch.headers === 'function') {
72 if (Array.isArray(headers)) { // fetch HeadersList
73 headers = buildHeadersFromArray(headers)
74 }
75 return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {})
76 }
77 if (typeof mockDispatch.headers === 'undefined') {
78 return true
79 }
80 if (typeof headers !== 'object' || typeof mockDispatch.headers !== 'object') {
81 return false
82 }
83
84 for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) {
85 const headerValue = getHeaderByName(headers, matchHeaderName)
86
87 if (!matchValue(matchHeaderValue, headerValue)) {
88 return false
89 }
90 }
91 return true
92}
93
94function safeUrl (path) {
95 if (typeof path !== 'string') {
96 return path
97 }
98
99 const pathSegments = path.split('?')
100
101 if (pathSegments.length !== 2) {
102 return path
103 }
104
105 const qp = new URLSearchParams(pathSegments.pop())
106 qp.sort()
107 return [...pathSegments, qp.toString()].join('?')
108}
109
110function matchKey (mockDispatch, { path, method, body, headers }) {
111 const pathMatch = matchValue(mockDispatch.path, path)
112 const methodMatch = matchValue(mockDispatch.method, method)
113 const bodyMatch = typeof mockDispatch.body !== 'undefined' ? matchValue(mockDispatch.body, body) : true
114 const headersMatch = matchHeaders(mockDispatch, headers)
115 return pathMatch && methodMatch && bodyMatch && headersMatch
116}
117
118function getResponseData (data) {
119 if (Buffer.isBuffer(data)) {
120 return data
121 } else if (typeof data === 'object') {
122 return JSON.stringify(data)
123 } else {
124 return data.toString()
125 }
126}
127
128function getMockDispatch (mockDispatches, key) {
129 const basePath = key.query ? buildURL(key.path, key.query) : key.path
130 const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath
131
132 // Match path
133 let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(safeUrl(path), resolvedPath))
134 if (matchedMockDispatches.length === 0) {
135 throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`)
136 }
137
138 // Match method
139 matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method))
140 if (matchedMockDispatches.length === 0) {
141 throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}'`)
142 }
143
144 // Match body
145 matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== 'undefined' ? matchValue(body, key.body) : true)
146 if (matchedMockDispatches.length === 0) {
147 throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}'`)
148 }
149
150 // Match headers
151 matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers))
152 if (matchedMockDispatches.length === 0) {
153 throw new MockNotMatchedError(`Mock dispatch not matched for headers '${typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers}'`)
154 }
155
156 return matchedMockDispatches[0]
157}
158
159function addMockDispatch (mockDispatches, key, data) {
160 const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false }
161 const replyData = typeof data === 'function' ? { callback: data } : { ...data }
162 const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } }
163 mockDispatches.push(newMockDispatch)
164 return newMockDispatch
165}
166
167function deleteMockDispatch (mockDispatches, key) {
168 const index = mockDispatches.findIndex(dispatch => {
169 if (!dispatch.consumed) {
170 return false
171 }
172 return matchKey(dispatch, key)
173 })
174 if (index !== -1) {
175 mockDispatches.splice(index, 1)
176 }
177}
178
179function buildKey (opts) {
180 const { path, method, body, headers, query } = opts
181 return {
182 path,
183 method,
184 body,
185 headers,
186 query
187 }
188}
189
190function generateKeyValues (data) {
191 return Object.entries(data).reduce((keyValuePairs, [key, value]) => [
192 ...keyValuePairs,
193 Buffer.from(`${key}`),
194 Array.isArray(value) ? value.map(x => Buffer.from(`${x}`)) : Buffer.from(`${value}`)
195 ], [])
196}
197
198/**
199 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
200 * @param {number} statusCode
201 */
202function getStatusText (statusCode) {
203 return STATUS_CODES[statusCode] || 'unknown'
204}
205
206async function getResponse (body) {
207 const buffers = []
208 for await (const data of body) {
209 buffers.push(data)
210 }
211 return Buffer.concat(buffers).toString('utf8')
212}
213
214/**
215 * Mock dispatch function used to simulate undici dispatches
216 */
217function mockDispatch (opts, handler) {
218 // Get mock dispatch from built key
219 const key = buildKey(opts)
220 const mockDispatch = getMockDispatch(this[kDispatches], key)
221
222 mockDispatch.timesInvoked++
223
224 // Here's where we resolve a callback if a callback is present for the dispatch data.
225 if (mockDispatch.data.callback) {
226 mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) }
227 }
228
229 // Parse mockDispatch data
230 const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch
231 const { timesInvoked, times } = mockDispatch
232
233 // If it's used up and not persistent, mark as consumed
234 mockDispatch.consumed = !persist && timesInvoked >= times
235 mockDispatch.pending = timesInvoked < times
236
237 // If specified, trigger dispatch error
238 if (error !== null) {
239 deleteMockDispatch(this[kDispatches], key)
240 handler.onError(error)
241 return true
242 }
243
244 // Handle the request with a delay if necessary
245 if (typeof delay === 'number' && delay > 0) {
246 setTimeout(() => {
247 handleReply(this[kDispatches])
248 }, delay)
249 } else {
250 handleReply(this[kDispatches])
251 }
252
253 function handleReply (mockDispatches, _data = data) {
254 // fetch's HeadersList is a 1D string array
255 const optsHeaders = Array.isArray(opts.headers)
256 ? buildHeadersFromArray(opts.headers)
257 : opts.headers
258 const body = typeof _data === 'function'
259 ? _data({ ...opts, headers: optsHeaders })
260 : _data
261
262 // util.types.isPromise is likely needed for jest.
263 if (isPromise(body)) {
264 // If handleReply is asynchronous, throwing an error
265 // in the callback will reject the promise, rather than
266 // synchronously throw the error, which breaks some tests.
267 // Rather, we wait for the callback to resolve if it is a
268 // promise, and then re-run handleReply with the new body.
269 body.then((newData) => handleReply(mockDispatches, newData))
270 return
271 }
272
273 const responseData = getResponseData(body)
274 const responseHeaders = generateKeyValues(headers)
275 const responseTrailers = generateKeyValues(trailers)
276
277 handler.abort = nop
278 handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode))
279 handler.onData(Buffer.from(responseData))
280 handler.onComplete(responseTrailers)
281 deleteMockDispatch(mockDispatches, key)
282 }
283
284 function resume () {}
285
286 return true
287}
288
289function buildMockDispatch () {
290 const agent = this[kMockAgent]
291 const origin = this[kOrigin]
292 const originalDispatch = this[kOriginalDispatch]
293
294 return function dispatch (opts, handler) {
295 if (agent.isMockActive) {
296 try {
297 mockDispatch.call(this, opts, handler)
298 } catch (error) {
299 if (error instanceof MockNotMatchedError) {
300 const netConnect = agent[kGetNetConnect]()
301 if (netConnect === false) {
302 throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`)
303 }
304 if (checkNetConnect(netConnect, origin)) {
305 originalDispatch.call(this, opts, handler)
306 } else {
307 throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`)
308 }
309 } else {
310 throw error
311 }
312 }
313 } else {
314 originalDispatch.call(this, opts, handler)
315 }
316 }
317}
318
319function checkNetConnect (netConnect, origin) {
320 const url = new URL(origin)
321 if (netConnect === true) {
322 return true
323 } else if (Array.isArray(netConnect) && netConnect.some((matcher) => matchValue(matcher, url.host))) {
324 return true
325 }
326 return false
327}
328
329function buildMockOptions (opts) {
330 if (opts) {
331 const { agent, ...mockOptions } = opts
332 return mockOptions
333 }
334}
335
336module.exports = {
337 getResponseData,
338 getMockDispatch,
339 addMockDispatch,
340 deleteMockDispatch,
341 buildKey,
342 generateKeyValues,
343 matchValue,
344 getResponse,
345 getStatusText,
346 mockDispatch,
347 buildMockDispatch,
348 checkNetConnect,
349 buildMockOptions,
350 getHeaderByName
351}
Note: See TracBrowser for help on using the repository browser.