undici 7.3.0 → 7.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/docs/docs/api/Dispatcher.md +1 -1
- package/docs/docs/api/EnvHttpProxyAgent.md +0 -2
- package/docs/docs/api/Errors.md +1 -0
- package/docs/docs/api/MockAgent.md +59 -0
- package/docs/docs/api/MockCallHistory.md +197 -0
- package/docs/docs/api/MockCallHistoryLog.md +43 -0
- package/docs/docs/best-practices/mocking-request.md +55 -1
- package/index-fetch.js +3 -0
- package/index.js +3 -0
- package/lib/cache/memory-cache-store.js +7 -1
- package/lib/cache/sqlite-cache-store.js +11 -12
- package/lib/dispatcher/client.js +1 -1
- package/lib/dispatcher/env-http-proxy-agent.js +0 -9
- package/lib/dispatcher/pool.js +15 -1
- package/lib/handler/retry-handler.js +3 -3
- package/lib/mock/mock-agent.js +59 -4
- package/lib/mock/mock-call-history.js +248 -0
- package/lib/mock/mock-symbols.js +6 -1
- package/lib/mock/mock-utils.js +11 -3
- package/lib/util/cache.js +9 -8
- package/lib/web/fetch/body.js +1 -1
- package/lib/web/fetch/request.js +12 -9
- package/package.json +1 -1
- package/types/cache-interceptor.d.ts +2 -2
- package/types/client.d.ts +1 -1
- package/types/index.d.ts +4 -1
- package/types/mock-agent.d.ts +12 -0
- package/types/mock-call-history.d.ts +111 -0
package/lib/mock/mock-agent.js
CHANGED
|
@@ -11,31 +11,44 @@ const {
|
|
|
11
11
|
kNetConnect,
|
|
12
12
|
kGetNetConnect,
|
|
13
13
|
kOptions,
|
|
14
|
-
kFactory
|
|
14
|
+
kFactory,
|
|
15
|
+
kMockAgentRegisterCallHistory,
|
|
16
|
+
kMockAgentIsCallHistoryEnabled,
|
|
17
|
+
kMockAgentAddCallHistoryLog,
|
|
18
|
+
kMockAgentMockCallHistoryInstance,
|
|
19
|
+
kMockCallHistoryAddLog
|
|
15
20
|
} = require('./mock-symbols')
|
|
16
21
|
const MockClient = require('./mock-client')
|
|
17
22
|
const MockPool = require('./mock-pool')
|
|
18
|
-
const { matchValue,
|
|
23
|
+
const { matchValue, buildAndValidateMockOptions } = require('./mock-utils')
|
|
19
24
|
const { InvalidArgumentError, UndiciError } = require('../core/errors')
|
|
20
25
|
const Dispatcher = require('../dispatcher/dispatcher')
|
|
21
26
|
const PendingInterceptorsFormatter = require('./pending-interceptors-formatter')
|
|
27
|
+
const { MockCallHistory } = require('./mock-call-history')
|
|
22
28
|
|
|
23
29
|
class MockAgent extends Dispatcher {
|
|
24
30
|
constructor (opts) {
|
|
25
31
|
super(opts)
|
|
26
32
|
|
|
33
|
+
const mockOptions = buildAndValidateMockOptions(opts)
|
|
34
|
+
|
|
27
35
|
this[kNetConnect] = true
|
|
28
36
|
this[kIsMockActive] = true
|
|
37
|
+
this[kMockAgentIsCallHistoryEnabled] = mockOptions?.enableCallHistory ?? false
|
|
29
38
|
|
|
30
39
|
// Instantiate Agent and encapsulate
|
|
31
|
-
if (
|
|
40
|
+
if (opts?.agent && typeof opts.agent.dispatch !== 'function') {
|
|
32
41
|
throw new InvalidArgumentError('Argument opts.agent must implement Agent')
|
|
33
42
|
}
|
|
34
43
|
const agent = opts?.agent ? opts.agent : new Agent(opts)
|
|
35
44
|
this[kAgent] = agent
|
|
36
45
|
|
|
37
46
|
this[kClients] = agent[kClients]
|
|
38
|
-
this[kOptions] =
|
|
47
|
+
this[kOptions] = mockOptions
|
|
48
|
+
|
|
49
|
+
if (this[kMockAgentIsCallHistoryEnabled]) {
|
|
50
|
+
this[kMockAgentRegisterCallHistory]()
|
|
51
|
+
}
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
get (origin) {
|
|
@@ -51,10 +64,14 @@ class MockAgent extends Dispatcher {
|
|
|
51
64
|
dispatch (opts, handler) {
|
|
52
65
|
// Call MockAgent.get to perform additional setup before dispatching as normal
|
|
53
66
|
this.get(opts.origin)
|
|
67
|
+
|
|
68
|
+
this[kMockAgentAddCallHistoryLog](opts)
|
|
69
|
+
|
|
54
70
|
return this[kAgent].dispatch(opts, handler)
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
async close () {
|
|
74
|
+
this.clearCallHistory()
|
|
58
75
|
await this[kAgent].close()
|
|
59
76
|
this[kClients].clear()
|
|
60
77
|
}
|
|
@@ -85,12 +102,50 @@ class MockAgent extends Dispatcher {
|
|
|
85
102
|
this[kNetConnect] = false
|
|
86
103
|
}
|
|
87
104
|
|
|
105
|
+
enableCallHistory () {
|
|
106
|
+
this[kMockAgentIsCallHistoryEnabled] = true
|
|
107
|
+
|
|
108
|
+
return this
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
disableCallHistory () {
|
|
112
|
+
this[kMockAgentIsCallHistoryEnabled] = false
|
|
113
|
+
|
|
114
|
+
return this
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getCallHistory () {
|
|
118
|
+
return this[kMockAgentMockCallHistoryInstance]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
clearCallHistory () {
|
|
122
|
+
if (this[kMockAgentMockCallHistoryInstance] !== undefined) {
|
|
123
|
+
this[kMockAgentMockCallHistoryInstance].clear()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
88
127
|
// This is required to bypass issues caused by using global symbols - see:
|
|
89
128
|
// https://github.com/nodejs/undici/issues/1447
|
|
90
129
|
get isMockActive () {
|
|
91
130
|
return this[kIsMockActive]
|
|
92
131
|
}
|
|
93
132
|
|
|
133
|
+
[kMockAgentRegisterCallHistory] () {
|
|
134
|
+
if (this[kMockAgentMockCallHistoryInstance] === undefined) {
|
|
135
|
+
this[kMockAgentMockCallHistoryInstance] = new MockCallHistory()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
[kMockAgentAddCallHistoryLog] (opts) {
|
|
140
|
+
if (this[kMockAgentIsCallHistoryEnabled]) {
|
|
141
|
+
// additional setup when enableCallHistory class method is used after mockAgent instantiation
|
|
142
|
+
this[kMockAgentRegisterCallHistory]()
|
|
143
|
+
|
|
144
|
+
// add call history log on every call (intercepted or not)
|
|
145
|
+
this[kMockAgentMockCallHistoryInstance][kMockCallHistoryAddLog](opts)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
94
149
|
[kMockAgentSet] (origin, dispatcher) {
|
|
95
150
|
this[kClients].set(origin, dispatcher)
|
|
96
151
|
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { kMockCallHistoryAddLog } = require('./mock-symbols')
|
|
4
|
+
const { InvalidArgumentError } = require('../core/errors')
|
|
5
|
+
|
|
6
|
+
function handleFilterCallsWithOptions (criteria, options, handler, store) {
|
|
7
|
+
switch (options.operator) {
|
|
8
|
+
case 'OR':
|
|
9
|
+
store.push(...handler(criteria))
|
|
10
|
+
|
|
11
|
+
return store
|
|
12
|
+
case 'AND':
|
|
13
|
+
return handler.call({ logs: store }, criteria)
|
|
14
|
+
default:
|
|
15
|
+
// guard -- should never happens because buildAndValidateFilterCallsOptions is called before
|
|
16
|
+
throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'')
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildAndValidateFilterCallsOptions (options = {}) {
|
|
21
|
+
const finalOptions = {}
|
|
22
|
+
|
|
23
|
+
if ('operator' in options) {
|
|
24
|
+
if (typeof options.operator !== 'string' || (options.operator.toUpperCase() !== 'OR' && options.operator.toUpperCase() !== 'AND')) {
|
|
25
|
+
throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
...finalOptions,
|
|
30
|
+
operator: options.operator.toUpperCase()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return finalOptions
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeFilterCalls (parameterName) {
|
|
38
|
+
return (parameterValue) => {
|
|
39
|
+
if (typeof parameterValue === 'string' || parameterValue == null) {
|
|
40
|
+
return this.logs.filter((log) => {
|
|
41
|
+
return log[parameterName] === parameterValue
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
if (parameterValue instanceof RegExp) {
|
|
45
|
+
return this.logs.filter((log) => {
|
|
46
|
+
return parameterValue.test(log[parameterName])
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new InvalidArgumentError(`${parameterName} parameter should be one of string, regexp, undefined or null`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function computeUrlWithMaybeSearchParameters (requestInit) {
|
|
54
|
+
// path can contains query url parameters
|
|
55
|
+
// or query can contains query url parameters
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(requestInit.path, requestInit.origin)
|
|
58
|
+
|
|
59
|
+
// requestInit.path contains query url parameters
|
|
60
|
+
// requestInit.query is then undefined
|
|
61
|
+
if (url.search.length !== 0) {
|
|
62
|
+
return url
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// requestInit.query can be populated here
|
|
66
|
+
url.search = new URLSearchParams(requestInit.query).toString()
|
|
67
|
+
|
|
68
|
+
return url
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new InvalidArgumentError('An error occurred when computing MockCallHistoryLog.url', { cause: error })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class MockCallHistoryLog {
|
|
75
|
+
constructor (requestInit = {}) {
|
|
76
|
+
this.body = requestInit.body
|
|
77
|
+
this.headers = requestInit.headers
|
|
78
|
+
this.method = requestInit.method
|
|
79
|
+
|
|
80
|
+
const url = computeUrlWithMaybeSearchParameters(requestInit)
|
|
81
|
+
|
|
82
|
+
this.fullUrl = url.toString()
|
|
83
|
+
this.origin = url.origin
|
|
84
|
+
this.path = url.pathname
|
|
85
|
+
this.searchParams = Object.fromEntries(url.searchParams)
|
|
86
|
+
this.protocol = url.protocol
|
|
87
|
+
this.host = url.host
|
|
88
|
+
this.port = url.port
|
|
89
|
+
this.hash = url.hash
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
toMap () {
|
|
93
|
+
return new Map([
|
|
94
|
+
['protocol', this.protocol],
|
|
95
|
+
['host', this.host],
|
|
96
|
+
['port', this.port],
|
|
97
|
+
['origin', this.origin],
|
|
98
|
+
['path', this.path],
|
|
99
|
+
['hash', this.hash],
|
|
100
|
+
['searchParams', this.searchParams],
|
|
101
|
+
['fullUrl', this.fullUrl],
|
|
102
|
+
['method', this.method],
|
|
103
|
+
['body', this.body],
|
|
104
|
+
['headers', this.headers]]
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
toString () {
|
|
109
|
+
const options = { betweenKeyValueSeparator: '->', betweenPairSeparator: '|' }
|
|
110
|
+
let result = ''
|
|
111
|
+
|
|
112
|
+
this.toMap().forEach((value, key) => {
|
|
113
|
+
if (typeof value === 'string' || value === undefined || value === null) {
|
|
114
|
+
result = `${result}${key}${options.betweenKeyValueSeparator}${value}${options.betweenPairSeparator}`
|
|
115
|
+
}
|
|
116
|
+
if ((typeof value === 'object' && value !== null) || Array.isArray(value)) {
|
|
117
|
+
result = `${result}${key}${options.betweenKeyValueSeparator}${JSON.stringify(value)}${options.betweenPairSeparator}`
|
|
118
|
+
}
|
|
119
|
+
// maybe miss something for non Record / Array headers and searchParams here
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// delete last betweenPairSeparator
|
|
123
|
+
return result.slice(0, -1)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class MockCallHistory {
|
|
128
|
+
logs = []
|
|
129
|
+
|
|
130
|
+
calls () {
|
|
131
|
+
return this.logs
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
firstCall () {
|
|
135
|
+
return this.logs.at(0)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
lastCall () {
|
|
139
|
+
return this.logs.at(-1)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
nthCall (number) {
|
|
143
|
+
if (typeof number !== 'number') {
|
|
144
|
+
throw new InvalidArgumentError('nthCall must be called with a number')
|
|
145
|
+
}
|
|
146
|
+
if (!Number.isInteger(number)) {
|
|
147
|
+
throw new InvalidArgumentError('nthCall must be called with an integer')
|
|
148
|
+
}
|
|
149
|
+
if (Math.sign(number) !== 1) {
|
|
150
|
+
throw new InvalidArgumentError('nthCall must be called with a positive value. use firstCall or lastCall instead')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// non zero based index. this is more human readable
|
|
154
|
+
return this.logs.at(number - 1)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
filterCalls (criteria, options) {
|
|
158
|
+
// perf
|
|
159
|
+
if (this.logs.length === 0) {
|
|
160
|
+
return this.logs
|
|
161
|
+
}
|
|
162
|
+
if (typeof criteria === 'function') {
|
|
163
|
+
return this.logs.filter(criteria)
|
|
164
|
+
}
|
|
165
|
+
if (criteria instanceof RegExp) {
|
|
166
|
+
return this.logs.filter((log) => {
|
|
167
|
+
return criteria.test(log.toString())
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
if (typeof criteria === 'object' && criteria !== null) {
|
|
171
|
+
// no criteria - returning all logs
|
|
172
|
+
if (Object.keys(criteria).length === 0) {
|
|
173
|
+
return this.logs
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const finalOptions = { operator: 'OR', ...buildAndValidateFilterCallsOptions(options) }
|
|
177
|
+
|
|
178
|
+
let maybeDuplicatedLogsFiltered = []
|
|
179
|
+
if ('protocol' in criteria) {
|
|
180
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered)
|
|
181
|
+
}
|
|
182
|
+
if ('host' in criteria) {
|
|
183
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered)
|
|
184
|
+
}
|
|
185
|
+
if ('port' in criteria) {
|
|
186
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered)
|
|
187
|
+
}
|
|
188
|
+
if ('origin' in criteria) {
|
|
189
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered)
|
|
190
|
+
}
|
|
191
|
+
if ('path' in criteria) {
|
|
192
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered)
|
|
193
|
+
}
|
|
194
|
+
if ('hash' in criteria) {
|
|
195
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered)
|
|
196
|
+
}
|
|
197
|
+
if ('fullUrl' in criteria) {
|
|
198
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered)
|
|
199
|
+
}
|
|
200
|
+
if ('method' in criteria) {
|
|
201
|
+
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)]
|
|
205
|
+
|
|
206
|
+
return uniqLogsFiltered
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
throw new InvalidArgumentError('criteria parameter should be one of function, regexp, or object')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
filterCallsByProtocol = makeFilterCalls.call(this, 'protocol')
|
|
213
|
+
|
|
214
|
+
filterCallsByHost = makeFilterCalls.call(this, 'host')
|
|
215
|
+
|
|
216
|
+
filterCallsByPort = makeFilterCalls.call(this, 'port')
|
|
217
|
+
|
|
218
|
+
filterCallsByOrigin = makeFilterCalls.call(this, 'origin')
|
|
219
|
+
|
|
220
|
+
filterCallsByPath = makeFilterCalls.call(this, 'path')
|
|
221
|
+
|
|
222
|
+
filterCallsByHash = makeFilterCalls.call(this, 'hash')
|
|
223
|
+
|
|
224
|
+
filterCallsByFullUrl = makeFilterCalls.call(this, 'fullUrl')
|
|
225
|
+
|
|
226
|
+
filterCallsByMethod = makeFilterCalls.call(this, 'method')
|
|
227
|
+
|
|
228
|
+
clear () {
|
|
229
|
+
this.logs = []
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
[kMockCallHistoryAddLog] (requestInit) {
|
|
233
|
+
const log = new MockCallHistoryLog(requestInit)
|
|
234
|
+
|
|
235
|
+
this.logs.push(log)
|
|
236
|
+
|
|
237
|
+
return log
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
* [Symbol.iterator] () {
|
|
241
|
+
for (const log of this.calls()) {
|
|
242
|
+
yield log
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports.MockCallHistory = MockCallHistory
|
|
248
|
+
module.exports.MockCallHistoryLog = MockCallHistoryLog
|
package/lib/mock/mock-symbols.js
CHANGED
|
@@ -21,5 +21,10 @@ module.exports = {
|
|
|
21
21
|
kNetConnect: Symbol('net connect'),
|
|
22
22
|
kGetNetConnect: Symbol('get net connect'),
|
|
23
23
|
kConnected: Symbol('connected'),
|
|
24
|
-
kIgnoreTrailingSlash: Symbol('ignore trailing slash')
|
|
24
|
+
kIgnoreTrailingSlash: Symbol('ignore trailing slash'),
|
|
25
|
+
kMockAgentMockCallHistoryInstance: Symbol('mock agent mock call history name'),
|
|
26
|
+
kMockAgentRegisterCallHistory: Symbol('mock agent register mock call history'),
|
|
27
|
+
kMockAgentAddCallHistoryLog: Symbol('mock agent add call history log'),
|
|
28
|
+
kMockAgentIsCallHistoryEnabled: Symbol('mock agent is call history enabled'),
|
|
29
|
+
kMockCallHistoryAddLog: Symbol('mock call history add log')
|
|
25
30
|
}
|
package/lib/mock/mock-utils.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
isPromise
|
|
16
16
|
}
|
|
17
17
|
} = require('node:util')
|
|
18
|
+
const { InvalidArgumentError } = require('../core/errors')
|
|
18
19
|
|
|
19
20
|
function matchValue (match, value) {
|
|
20
21
|
if (typeof match === 'string') {
|
|
@@ -124,8 +125,10 @@ function getResponseData (data) {
|
|
|
124
125
|
return data
|
|
125
126
|
} else if (typeof data === 'object') {
|
|
126
127
|
return JSON.stringify(data)
|
|
127
|
-
} else {
|
|
128
|
+
} else if (data) {
|
|
128
129
|
return data.toString()
|
|
130
|
+
} else {
|
|
131
|
+
return ''
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
134
|
|
|
@@ -365,9 +368,14 @@ function checkNetConnect (netConnect, origin) {
|
|
|
365
368
|
return false
|
|
366
369
|
}
|
|
367
370
|
|
|
368
|
-
function
|
|
371
|
+
function buildAndValidateMockOptions (opts) {
|
|
369
372
|
if (opts) {
|
|
370
373
|
const { agent, ...mockOptions } = opts
|
|
374
|
+
|
|
375
|
+
if ('enableCallHistory' in mockOptions && typeof mockOptions.enableCallHistory !== 'boolean') {
|
|
376
|
+
throw new InvalidArgumentError('options.enableCallHistory must to be a boolean')
|
|
377
|
+
}
|
|
378
|
+
|
|
371
379
|
return mockOptions
|
|
372
380
|
}
|
|
373
381
|
}
|
|
@@ -385,7 +393,7 @@ module.exports = {
|
|
|
385
393
|
mockDispatch,
|
|
386
394
|
buildMockDispatch,
|
|
387
395
|
checkNetConnect,
|
|
388
|
-
|
|
396
|
+
buildAndValidateMockOptions,
|
|
389
397
|
getHeaderByName,
|
|
390
398
|
buildHeadersFromArray
|
|
391
399
|
}
|
package/lib/util/cache.js
CHANGED
|
@@ -26,10 +26,14 @@ function makeCacheKey (opts) {
|
|
|
26
26
|
if (typeof key !== 'string' || typeof val !== 'string') {
|
|
27
27
|
throw new Error('opts.headers is not a valid header map')
|
|
28
28
|
}
|
|
29
|
-
headers[key] = val
|
|
29
|
+
headers[key.toLowerCase()] = val
|
|
30
30
|
}
|
|
31
31
|
} else if (typeof opts.headers === 'object') {
|
|
32
|
-
headers =
|
|
32
|
+
headers = {}
|
|
33
|
+
|
|
34
|
+
for (const key of Object.keys(opts.headers)) {
|
|
35
|
+
headers[key.toLowerCase()] = opts.headers[key]
|
|
36
|
+
}
|
|
33
37
|
} else {
|
|
34
38
|
throw new Error('opts.headers is not an object')
|
|
35
39
|
}
|
|
@@ -260,19 +264,16 @@ function parseVaryHeader (varyHeader, headers) {
|
|
|
260
264
|
return headers
|
|
261
265
|
}
|
|
262
266
|
|
|
263
|
-
const output = /** @type {Record<string, string | string[]>} */ ({})
|
|
267
|
+
const output = /** @type {Record<string, string | string[] | null>} */ ({})
|
|
264
268
|
|
|
265
269
|
const varyingHeaders = typeof varyHeader === 'string'
|
|
266
270
|
? varyHeader.split(',')
|
|
267
271
|
: varyHeader
|
|
272
|
+
|
|
268
273
|
for (const header of varyingHeaders) {
|
|
269
274
|
const trimmedHeader = header.trim().toLowerCase()
|
|
270
275
|
|
|
271
|
-
|
|
272
|
-
output[trimmedHeader] = headers[trimmedHeader]
|
|
273
|
-
} else {
|
|
274
|
-
return undefined
|
|
275
|
-
}
|
|
276
|
+
output[trimmedHeader] = headers[trimmedHeader] ?? null
|
|
276
277
|
}
|
|
277
278
|
|
|
278
279
|
return output
|
package/lib/web/fetch/body.js
CHANGED
package/lib/web/fetch/request.js
CHANGED
|
@@ -37,6 +37,14 @@ const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
|
|
|
37
37
|
|
|
38
38
|
const dependentControllerMap = new WeakMap()
|
|
39
39
|
|
|
40
|
+
let abortSignalHasEventHandlerLeakWarning
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
abortSignalHasEventHandlerLeakWarning = getMaxListeners(new AbortController().signal) > 0
|
|
44
|
+
} catch {
|
|
45
|
+
abortSignalHasEventHandlerLeakWarning = false
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
function buildAbort (acRef) {
|
|
41
49
|
return abort
|
|
42
50
|
|
|
@@ -424,15 +432,10 @@ class Request {
|
|
|
424
432
|
const acRef = new WeakRef(ac)
|
|
425
433
|
const abort = buildAbort(acRef)
|
|
426
434
|
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
// This is only available in node >= v19.9.0
|
|
432
|
-
if (typeof getMaxListeners === 'function' && getMaxListeners(signal) === defaultMaxListeners) {
|
|
433
|
-
setMaxListeners(1500, signal)
|
|
434
|
-
}
|
|
435
|
-
} catch {}
|
|
435
|
+
// If the max amount of listeners is equal to the default, increase it
|
|
436
|
+
if (abortSignalHasEventHandlerLeakWarning && getMaxListeners(signal) === defaultMaxListeners) {
|
|
437
|
+
setMaxListeners(1500, signal)
|
|
438
|
+
}
|
|
436
439
|
|
|
437
440
|
util.addAbortListener(signal, abort)
|
|
438
441
|
// The third argument must be a registry key to be unregistered.
|
package/package.json
CHANGED
|
@@ -70,7 +70,7 @@ declare namespace CacheHandler {
|
|
|
70
70
|
statusCode: number
|
|
71
71
|
statusMessage: string
|
|
72
72
|
headers: Record<string, string | string[]>
|
|
73
|
-
vary?: Record<string, string | string[]>
|
|
73
|
+
vary?: Record<string, string | string[] | null>
|
|
74
74
|
etag?: string
|
|
75
75
|
cacheControlDirectives?: CacheControlDirectives
|
|
76
76
|
cachedAt: number
|
|
@@ -88,7 +88,7 @@ declare namespace CacheHandler {
|
|
|
88
88
|
statusCode: number
|
|
89
89
|
statusMessage: string
|
|
90
90
|
headers: Record<string, string | string[]>
|
|
91
|
-
vary?: Record<string, string | string[]>
|
|
91
|
+
vary?: Record<string, string | string[] | null>
|
|
92
92
|
etag?: string
|
|
93
93
|
body?: Readable | Iterable<Buffer> | AsyncIterable<Buffer> | Buffer | Iterable<string> | AsyncIterable<string> | string
|
|
94
94
|
cacheControlDirectives: CacheControlDirectives,
|
package/types/client.d.ts
CHANGED
|
@@ -70,7 +70,7 @@ export declare namespace Client {
|
|
|
70
70
|
/** TODO */
|
|
71
71
|
maxRedirections?: number;
|
|
72
72
|
/** TODO */
|
|
73
|
-
connect?: buildConnector.BuildOptions | buildConnector.connector;
|
|
73
|
+
connect?: Partial<buildConnector.BuildOptions> | buildConnector.connector;
|
|
74
74
|
/** TODO */
|
|
75
75
|
maxRequestsPerClient?: number;
|
|
76
76
|
/** TODO */
|
package/types/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import Agent from './agent'
|
|
|
12
12
|
import MockClient from './mock-client'
|
|
13
13
|
import MockPool from './mock-pool'
|
|
14
14
|
import MockAgent from './mock-agent'
|
|
15
|
+
import { MockCallHistory, MockCallHistoryLog } from './mock-call-history'
|
|
15
16
|
import mockErrors from './mock-errors'
|
|
16
17
|
import ProxyAgent from './proxy-agent'
|
|
17
18
|
import EnvHttpProxyAgent from './env-http-proxy-agent'
|
|
@@ -31,7 +32,7 @@ export * from './content-type'
|
|
|
31
32
|
export * from './cache'
|
|
32
33
|
export { Interceptable } from './mock-interceptor'
|
|
33
34
|
|
|
34
|
-
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, interceptors, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent }
|
|
35
|
+
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, interceptors, MockClient, MockPool, MockAgent, MockCallHistory, MockCallHistoryLog, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent }
|
|
35
36
|
export default Undici
|
|
36
37
|
|
|
37
38
|
declare namespace Undici {
|
|
@@ -55,6 +56,8 @@ declare namespace Undici {
|
|
|
55
56
|
const MockClient: typeof import('./mock-client').default
|
|
56
57
|
const MockPool: typeof import('./mock-pool').default
|
|
57
58
|
const MockAgent: typeof import('./mock-agent').default
|
|
59
|
+
const MockCallHistory: typeof import('./mock-call-history').MockCallHistory
|
|
60
|
+
const MockCallHistoryLog: typeof import('./mock-call-history').MockCallHistoryLog
|
|
58
61
|
const mockErrors: typeof import('./mock-errors').default
|
|
59
62
|
const fetch: typeof import('./fetch').fetch
|
|
60
63
|
const Headers: typeof import('./fetch').Headers
|
package/types/mock-agent.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import Agent from './agent'
|
|
|
2
2
|
import Dispatcher from './dispatcher'
|
|
3
3
|
import { Interceptable, MockInterceptor } from './mock-interceptor'
|
|
4
4
|
import MockDispatch = MockInterceptor.MockDispatch
|
|
5
|
+
import { MockCallHistory } from './mock-call-history'
|
|
5
6
|
|
|
6
7
|
export default MockAgent
|
|
7
8
|
|
|
@@ -31,6 +32,14 @@ declare class MockAgent<TMockAgentOptions extends MockAgent.Options = MockAgent.
|
|
|
31
32
|
enableNetConnect (host: ((host: string) => boolean)): void
|
|
32
33
|
/** Causes all requests to throw when requests are not matched in a MockAgent intercept. */
|
|
33
34
|
disableNetConnect (): void
|
|
35
|
+
/** get call history. returns the MockAgent call history or undefined if the option is not enabled. */
|
|
36
|
+
getCallHistory (): MockCallHistory | undefined
|
|
37
|
+
/** clear every call history. Any MockCallHistoryLog will be deleted on the MockCallHistory instance */
|
|
38
|
+
clearCallHistory (): void
|
|
39
|
+
/** Enable call history. Any subsequence calls will then be registered. */
|
|
40
|
+
enableCallHistory (): this
|
|
41
|
+
/** Disable call history. Any subsequence calls will then not be registered. */
|
|
42
|
+
disableCallHistory (): this
|
|
34
43
|
pendingInterceptors (): PendingInterceptor[]
|
|
35
44
|
assertNoPendingInterceptors (options?: {
|
|
36
45
|
pendingInterceptorsFormatter?: PendingInterceptorsFormatter;
|
|
@@ -49,5 +58,8 @@ declare namespace MockAgent {
|
|
|
49
58
|
|
|
50
59
|
/** Ignore trailing slashes in the path */
|
|
51
60
|
ignoreTrailingSlash?: boolean;
|
|
61
|
+
|
|
62
|
+
/** Enable call history. you can either call MockAgent.enableCallHistory(). default false */
|
|
63
|
+
enableCallHistory?: boolean
|
|
52
64
|
}
|
|
53
65
|
}
|