undici 7.4.0 → 7.6.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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Extends: `undici.Dispatcher`
4
4
 
5
- Agent allow dispatching requests against multiple different origins.
5
+ Agent allows dispatching requests against multiple different origins.
6
6
 
7
7
  Requests are not guaranteed to be dispatched in order of invocation.
8
8
 
@@ -179,7 +179,9 @@ for await (const data of result2.body) {
179
179
  console.log('data', data.toString('utf8')) // data hello
180
180
  }
181
181
  ```
182
+
182
183
  #### Example - Mock different requests within the same file
184
+
183
185
  ```js
184
186
  const { MockAgent, setGlobalDispatcher } = require('undici');
185
187
  const agent = new MockAgent();
@@ -540,3 +542,60 @@ agent.assertNoPendingInterceptors()
540
542
  // │ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '❌' │ 0 │ 1 │
541
543
  // └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
542
544
  ```
545
+
546
+ #### Example - access call history on MockAgent
547
+
548
+ You can register every call made within a MockAgent to be able to retrieve the body, headers and so on.
549
+
550
+ This is not enabled by default.
551
+
552
+ ```js
553
+ import { MockAgent, setGlobalDispatcher, request } from 'undici'
554
+
555
+ const mockAgent = new MockAgent({ enableCallHistory: true })
556
+ setGlobalDispatcher(mockAgent)
557
+
558
+ await request('http://example.com', { query: { item: 1 }})
559
+
560
+ mockAgent.getCallHistory()?.firstCall()
561
+ // Returns
562
+ // MockCallHistoryLog {
563
+ // body: undefined,
564
+ // headers: undefined,
565
+ // method: 'GET',
566
+ // origin: 'http://example.com',
567
+ // fullUrl: 'http://example.com/?item=1',
568
+ // path: '/',
569
+ // searchParams: { item: '1' },
570
+ // protocol: 'http:',
571
+ // host: 'example.com',
572
+ // port: ''
573
+ // }
574
+ ```
575
+
576
+ #### Example - clear call history
577
+
578
+ ```js
579
+ const mockAgent = new MockAgent()
580
+
581
+ mockAgent.clearAllCallHistory()
582
+ ```
583
+
584
+ #### Example - call history instance class method
585
+
586
+ ```js
587
+ const mockAgent = new MockAgent()
588
+
589
+ const mockAgentHistory = mockAgent.getCallHistory()
590
+
591
+ mockAgentHistory?.calls() // returns an array of MockCallHistoryLogs
592
+ mockAgentHistory?.firstCall() // returns the first MockCallHistoryLogs or undefined
593
+ mockAgentHistory?.lastCall() // returns the last MockCallHistoryLogs or undefined
594
+ mockAgentHistory?.nthCall(3) // returns the third MockCallHistoryLogs or undefined
595
+ mockAgentHistory?.filterCalls({ path: '/endpoint', hash: '#hash-value' }) // returns an Array of MockCallHistoryLogs WHERE path === /endpoint OR hash === #hash-value
596
+ mockAgentHistory?.filterCalls({ path: '/endpoint', hash: '#hash-value' }, { operator: 'AND' }) // returns an Array of MockCallHistoryLogs WHERE path === /endpoint AND hash === #hash-value
597
+ mockAgentHistory?.filterCalls(/"data": "{}"/) // returns an Array of MockCallHistoryLogs where any value match regexp
598
+ mockAgentHistory?.filterCalls('application/json') // returns an Array of MockCallHistoryLogs where any value === 'application/json'
599
+ mockAgentHistory?.filterCalls((log) => log.path === '/endpoint') // returns an Array of MockCallHistoryLogs when given function returns true
600
+ mockAgentHistory?.clear() // clear the history
601
+ ```
@@ -0,0 +1,197 @@
1
+ # Class: MockCallHistory
2
+
3
+ Access to an instance with :
4
+
5
+ ```js
6
+ const mockAgent = new MockAgent({ enableCallHistory: true })
7
+ mockAgent.getCallHistory()
8
+
9
+ // or
10
+ const mockAgent = new MockAgent()
11
+ mockAgent.enableMockHistory()
12
+ mockAgent.getCallHistory()
13
+
14
+ ```
15
+
16
+ a MockCallHistory instance implements a **Symbol.iterator** letting you iterate on registered logs :
17
+
18
+ ```ts
19
+ for (const log of mockAgent.getCallHistory()) {
20
+ //...
21
+ }
22
+
23
+ const array: Array<MockCallHistoryLog> = [...mockAgent.getCallHistory()]
24
+ const set: Set<MockCallHistoryLog> = new Set(mockAgent.getCallHistory())
25
+ ```
26
+
27
+ ## class methods
28
+
29
+ ### clear
30
+
31
+ Clear all MockCallHistoryLog registered. This is automatically done when calling `mockAgent.close()`
32
+
33
+ ```js
34
+ mockAgent.clearCallHistory()
35
+ // same as
36
+ mockAgent.getCallHistory()?.clear()
37
+ ```
38
+
39
+ ### calls
40
+
41
+ Get all MockCallHistoryLog registered as an array
42
+
43
+ ```js
44
+ mockAgent.getCallHistory()?.calls()
45
+ ```
46
+
47
+ ### firstCall
48
+
49
+ Get the first MockCallHistoryLog registered or undefined
50
+
51
+ ```js
52
+ mockAgent.getCallHistory()?.firstCall()
53
+ ```
54
+
55
+ ### lastCall
56
+
57
+ Get the last MockCallHistoryLog registered or undefined
58
+
59
+ ```js
60
+ mockAgent.getCallHistory()?.lastCall()
61
+ ```
62
+
63
+ ### nthCall
64
+
65
+ Get the nth MockCallHistoryLog registered or undefined
66
+
67
+ ```js
68
+ mockAgent.getCallHistory()?.nthCall(3) // the third MockCallHistoryLog registered
69
+ ```
70
+
71
+ ### filterCallsByProtocol
72
+
73
+ Filter MockCallHistoryLog by protocol.
74
+
75
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
76
+
77
+ ```js
78
+ mockAgent.getCallHistory()?.filterCallsByProtocol(/https/)
79
+ mockAgent.getCallHistory()?.filterCallsByProtocol('https:')
80
+ ```
81
+
82
+ ### filterCallsByHost
83
+
84
+ Filter MockCallHistoryLog by host.
85
+
86
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
87
+
88
+ ```js
89
+ mockAgent.getCallHistory()?.filterCallsByHost(/localhost/)
90
+ mockAgent.getCallHistory()?.filterCallsByHost('localhost:3000')
91
+ ```
92
+
93
+ ### filterCallsByPort
94
+
95
+ Filter MockCallHistoryLog by port.
96
+
97
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
98
+
99
+ ```js
100
+ mockAgent.getCallHistory()?.filterCallsByPort(/3000/)
101
+ mockAgent.getCallHistory()?.filterCallsByPort('3000')
102
+ mockAgent.getCallHistory()?.filterCallsByPort('')
103
+ ```
104
+
105
+ ### filterCallsByOrigin
106
+
107
+ Filter MockCallHistoryLog by origin.
108
+
109
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
110
+
111
+ ```js
112
+ mockAgent.getCallHistory()?.filterCallsByOrigin(/http:\/\/localhost:3000/)
113
+ mockAgent.getCallHistory()?.filterCallsByOrigin('http://localhost:3000')
114
+ ```
115
+
116
+ ### filterCallsByPath
117
+
118
+ Filter MockCallHistoryLog by path.
119
+
120
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
121
+
122
+ ```js
123
+ mockAgent.getCallHistory()?.filterCallsByPath(/api\/v1\/graphql/)
124
+ mockAgent.getCallHistory()?.filterCallsByPath('/api/v1/graphql')
125
+ ```
126
+
127
+ ### filterCallsByHash
128
+
129
+ Filter MockCallHistoryLog by hash.
130
+
131
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
132
+
133
+ ```js
134
+ mockAgent.getCallHistory()?.filterCallsByPath(/hash/)
135
+ mockAgent.getCallHistory()?.filterCallsByPath('#hash')
136
+ ```
137
+
138
+ ### filterCallsByFullUrl
139
+
140
+ Filter MockCallHistoryLog by fullUrl. fullUrl contains protocol, host, port, path, hash, and query params
141
+
142
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
143
+
144
+ ```js
145
+ mockAgent.getCallHistory()?.filterCallsByFullUrl(/https:\/\/localhost:3000\/\?query=value#hash/)
146
+ mockAgent.getCallHistory()?.filterCallsByFullUrl('https://localhost:3000/?query=value#hash')
147
+ ```
148
+
149
+ ### filterCallsByMethod
150
+
151
+ Filter MockCallHistoryLog by method.
152
+
153
+ > more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
154
+
155
+ ```js
156
+ mockAgent.getCallHistory()?.filterCallsByMethod(/POST/)
157
+ mockAgent.getCallHistory()?.filterCallsByMethod('POST')
158
+ ```
159
+
160
+ ### filterCalls
161
+
162
+ This class method is a meta function / alias to apply complex filtering in a single way.
163
+
164
+ Parameters :
165
+
166
+ - criteria : the first parameter. a function, regexp or object.
167
+ - function : filter MockCallHistoryLog when the function returns false
168
+ - regexp : filter MockCallHistoryLog when the regexp does not match on MockCallHistoryLog.toString() ([see](./MockCallHistoryLog.md#to-string))
169
+ - object : an object with MockCallHistoryLog properties as keys to apply multiple filters. each values are a [filter parameter](/docs/docs/api/MockCallHistory.md#filter-parameter)
170
+ - options : the second parameter. an object.
171
+ - options.operator : `'AND'` or `'OR'` (default `'OR'`). Used only if criteria is an object. see below
172
+
173
+ ```js
174
+ mockAgent.getCallHistory()?.filterCalls((log) => log.hash === value && log.headers?.['authorization'] !== undefined)
175
+ mockAgent.getCallHistory()?.filterCalls(/"data": "{ "errors": "wrong body" }"/)
176
+
177
+ // returns an Array of MockCallHistoryLog which all have
178
+ // - a hash containing my-hash
179
+ // - OR
180
+ // - a path equal to /endpoint
181
+ mockAgent.getCallHistory()?.filterCalls({ hash: /my-hash/, path: '/endpoint' })
182
+
183
+ // returns an Array of MockCallHistoryLog which all have
184
+ // - a hash containing my-hash
185
+ // - AND
186
+ // - a path equal to /endpoint
187
+ mockAgent.getCallHistory()?.filterCalls({ hash: /my-hash/, path: '/endpoint' }, { operator: 'AND' })
188
+ ```
189
+
190
+ ## filter parameter
191
+
192
+ Can be :
193
+
194
+ - string. MockCallHistoryLog filtered if `value !== parameterValue`
195
+ - null. MockCallHistoryLog filtered if `value !== parameterValue`
196
+ - undefined. MockCallHistoryLog filtered if `value !== parameterValue`
197
+ - regexp. MockCallHistoryLog filtered if `!parameterValue.test(value)`
@@ -0,0 +1,43 @@
1
+ # Class: MockCallHistoryLog
2
+
3
+ Access to an instance with :
4
+
5
+ ```js
6
+ const mockAgent = new MockAgent({ enableCallHistory: true })
7
+ mockAgent.getCallHistory()?.firstCall()
8
+ ```
9
+
10
+ ## class properties
11
+
12
+ - body `mockAgent.getCallHistory()?.firstCall()?.body`
13
+ - headers `mockAgent.getCallHistory()?.firstCall()?.headers` an object
14
+ - method `mockAgent.getCallHistory()?.firstCall()?.method` a string
15
+ - fullUrl `mockAgent.getCallHistory()?.firstCall()?.fullUrl` a string containing the protocol, origin, path, query and hash
16
+ - origin `mockAgent.getCallHistory()?.firstCall()?.origin` a string containing the protocol and the host
17
+ - headers `mockAgent.getCallHistory()?.firstCall()?.headers` an object
18
+ - path `mockAgent.getCallHistory()?.firstCall()?.path` a string always starting with `/`
19
+ - searchParams `mockAgent.getCallHistory()?.firstCall()?.searchParams` an object
20
+ - protocol `mockAgent.getCallHistory()?.firstCall()?.protocol` a string (`https:`)
21
+ - host `mockAgent.getCallHistory()?.firstCall()?.host` a string
22
+ - port `mockAgent.getCallHistory()?.firstCall()?.port` an empty string or a string containing numbers
23
+ - hash `mockAgent.getCallHistory()?.firstCall()?.hash` an empty string or a string starting with `#`
24
+
25
+ ## class methods
26
+
27
+ ### toMap
28
+
29
+ Returns a Map instance
30
+
31
+ ```js
32
+ mockAgent.getCallHistory()?.firstCall()?.toMap()?.get('hash')
33
+ // #hash
34
+ ```
35
+
36
+ ### toString
37
+
38
+ Returns a string computed with any class property name and value pair
39
+
40
+ ```js
41
+ mockAgent.getCallHistory()?.firstCall()?.toString()
42
+ // protocol->https:|host->localhost:4000|port->4000|origin->https://localhost:4000|path->/endpoint|hash->#here|searchParams->{"query":"value"}|fullUrl->https://localhost:4000/endpoint?query=value#here|method->PUT|body->"{ "data": "hello" }"|headers->{"content-type":"application/json"}
43
+ ```
@@ -29,7 +29,7 @@ And this is what the test file looks like:
29
29
 
30
30
  ```js
31
31
  // index.test.mjs
32
- import { strict as assert } from 'assert'
32
+ import { strict as assert } from 'node:assert'
33
33
  import { MockAgent, setGlobalDispatcher, } from 'undici'
34
34
  import { bankTransfer } from './bank.mjs'
35
35
 
@@ -75,6 +75,60 @@ assert.deepEqual(badRequest, { message: 'bank account not found' })
75
75
 
76
76
  Explore other MockAgent functionality [here](/docs/docs/api/MockAgent.md)
77
77
 
78
+ ## Access agent call history
79
+
80
+ Using a MockAgent also allows you to make assertions on the configuration used to make your request in your application.
81
+
82
+ Here is an example :
83
+
84
+ ```js
85
+ // index.test.mjs
86
+ import { strict as assert } from 'node:assert'
87
+ import { MockAgent, setGlobalDispatcher, fetch } from 'undici'
88
+ import { app } from './app.mjs'
89
+
90
+ // given an application server running on http://localhost:3000
91
+ await app.start()
92
+
93
+ // enable call history at instantiation
94
+ const mockAgent = new MockAgent({ enableCallHistory: true })
95
+ // or after instantiation
96
+ mockAgent.enableCallHistory()
97
+
98
+ setGlobalDispatcher(mockAgent)
99
+
100
+ // this call is made (not intercepted)
101
+ await fetch(`http://localhost:3000/endpoint?query='hello'`, {
102
+ method: 'POST',
103
+ headers: { 'content-type': 'application/json' }
104
+ body: JSON.stringify({ data: '' })
105
+ })
106
+
107
+ // access to the call history of the MockAgent (which register every call made intercepted or not)
108
+ assert.ok(mockAgent.getCallHistory()?.calls().length === 1)
109
+ assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.fullUrl, `http://localhost:3000/endpoint?query='hello'`)
110
+ assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.body, JSON.stringify({ data: '' }))
111
+ assert.deepStrictEqual(mockAgent.getCallHistory()?.firstCall()?.searchParams, { query: 'hello' })
112
+ assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.port, '3000')
113
+ assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.host, 'localhost:3000')
114
+ assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.method, 'POST')
115
+ assert.strictEqual(mockAgent.getCallHistory()?.firstCall()?.path, '/endpoint')
116
+ assert.deepStrictEqual(mockAgent.getCallHistory()?.firstCall()?.headers, { 'content-type': 'application/json' })
117
+
118
+ // clear all call history logs
119
+ mockAgent.clearCallHistory()
120
+
121
+ assert.ok(mockAgent.getCallHistory()?.calls().length === 0)
122
+ ```
123
+
124
+ Calling `mockAgent.close()` will automatically clear and delete every call history for you.
125
+
126
+ Explore other MockAgent functionality [here](/docs/docs/api/MockAgent.md)
127
+
128
+ Explore other MockCallHistory functionality [here](/docs/docs/api/MockCallHistory.md)
129
+
130
+ Explore other MockCallHistoryLog functionality [here](/docs/docs/api/MockCallHistoryLog.md)
131
+
78
132
  ## Debug Mock Value
79
133
 
80
134
  When the interceptor and the request options are not the same, undici will automatically make a real HTTP request. To prevent real requests from being made, use `mockAgent.disableNetConnect()`:
package/index.js CHANGED
@@ -14,6 +14,7 @@ const { InvalidArgumentError } = errors
14
14
  const api = require('./lib/api')
15
15
  const buildConnector = require('./lib/core/connect')
16
16
  const MockClient = require('./lib/mock/mock-client')
17
+ const { MockCallHistory, MockCallHistoryLog } = require('./lib/mock/mock-call-history')
17
18
  const MockAgent = require('./lib/mock/mock-agent')
18
19
  const MockPool = require('./lib/mock/mock-pool')
19
20
  const mockErrors = require('./lib/mock/mock-errors')
@@ -169,6 +170,8 @@ module.exports.connect = makeDispatcher(api.connect)
169
170
  module.exports.upgrade = makeDispatcher(api.upgrade)
170
171
 
171
172
  module.exports.MockClient = MockClient
173
+ module.exports.MockCallHistory = MockCallHistory
174
+ module.exports.MockCallHistoryLog = MockCallHistoryLog
172
175
  module.exports.MockPool = MockPool
173
176
  module.exports.MockAgent = MockAgent
174
177
  module.exports.mockErrors = mockErrors
package/lib/core/util.js CHANGED
@@ -13,7 +13,7 @@ const { InvalidArgumentError } = require('./errors')
13
13
  const { headerNameLowerCasedRecord } = require('./constants')
14
14
  const { tree } = require('./tree')
15
15
 
16
- const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
16
+ const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(v => Number(v))
17
17
 
18
18
  class BodyAsyncIterable {
19
19
  constructor (body) {
@@ -207,7 +207,7 @@ class Client extends DispatcherBase {
207
207
  allowH2,
208
208
  socketPath,
209
209
  timeout: connectTimeout,
210
- ...(autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
210
+ ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
211
211
  ...connect
212
212
  })
213
213
  }
@@ -58,7 +58,7 @@ class Pool extends PoolBase {
58
58
  allowH2,
59
59
  socketPath,
60
60
  timeout: connectTimeout,
61
- ...(autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
61
+ ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
62
62
  ...connect
63
63
  })
64
64
  }
@@ -70,6 +70,20 @@ class Pool extends PoolBase {
70
70
  ? { ...options.interceptors }
71
71
  : undefined
72
72
  this[kFactory] = factory
73
+
74
+ this.on('connectionError', (origin, targets, error) => {
75
+ // If a connection error occurs, we remove the client from the pool,
76
+ // and emit a connectionError event. They will not be re-used.
77
+ // Fixes https://github.com/nodejs/undici/issues/3895
78
+ for (const target of targets) {
79
+ // Do not use kRemoveClient here, as it will close the client,
80
+ // but the client cannot be closed in this state.
81
+ const idx = this[kClients].indexOf(target)
82
+ if (idx !== -1) {
83
+ this[kClients].splice(idx, 1)
84
+ }
85
+ }
86
+ })
73
87
  }
74
88
 
75
89
  [kGetDispatcher] () {
@@ -9,7 +9,7 @@ const assert = require('node:assert')
9
9
  * here, which we then just pass on to the next handler (most likely a
10
10
  * CacheHandler). Note that this assumes the proper headers were already
11
11
  * included in the request to tell the origin that we want to revalidate the
12
- * response (i.e. if-modified-since).
12
+ * response (i.e. if-modified-since or if-none-match).
13
13
  *
14
14
  * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-validation
15
15
  *
@@ -11,8 +11,8 @@ const {
11
11
  } = require('../core/util')
12
12
 
13
13
  function calculateRetryAfterHeader (retryAfter) {
14
- const current = Date.now()
15
- return new Date(retryAfter).getTime() - current
14
+ const retryTime = new Date(retryAfter).getTime()
15
+ return isNaN(retryTime) ? 0 : retryTime - Date.now()
16
16
  }
17
17
 
18
18
  class RetryHandler {
@@ -124,7 +124,7 @@ class RetryHandler {
124
124
  if (retryAfterHeader) {
125
125
  retryAfterHeader = Number(retryAfterHeader)
126
126
  retryAfterHeader = Number.isNaN(retryAfterHeader)
127
- ? calculateRetryAfterHeader(retryAfterHeader)
127
+ ? calculateRetryAfterHeader(headers['retry-after'])
128
128
  : retryAfterHeader * 1e3 // Retry-After is in seconds
129
129
  }
130
130
 
@@ -6,7 +6,7 @@ const util = require('../core/util')
6
6
  const CacheHandler = require('../handler/cache-handler')
7
7
  const MemoryCacheStore = require('../cache/memory-cache-store')
8
8
  const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
9
- const { assertCacheStore, assertCacheMethods, makeCacheKey, parseCacheControlHeader } = require('../util/cache.js')
9
+ const { assertCacheStore, assertCacheMethods, makeCacheKey, normaliseHeaders, parseCacheControlHeader } = require('../util/cache.js')
10
10
  const { AbortError } = require('../core/errors.js')
11
11
 
12
12
  /**
@@ -221,7 +221,7 @@ function handleResult (
221
221
  // Check if the response is stale
222
222
  if (needsRevalidation(result, reqCacheControl)) {
223
223
  if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
224
- // If body is is stream we can't revalidate...
224
+ // If body is a stream we can't revalidate...
225
225
  // TODO (fix): This could be less strict...
226
226
  return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
227
227
  }
@@ -233,7 +233,7 @@ function handleResult (
233
233
  }
234
234
 
235
235
  let headers = {
236
- ...opts.headers,
236
+ ...normaliseHeaders(opts),
237
237
  'if-modified-since': new Date(result.cachedAt).toUTCString()
238
238
  }
239
239
 
@@ -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, buildMockOptions } = require('./mock-utils')
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 ((opts?.agent && typeof opts.agent.dispatch !== 'function')) {
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] = buildMockOptions(opts)
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
@@ -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
  }
@@ -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') {
@@ -96,7 +97,7 @@ function safeUrl (path) {
96
97
  return path
97
98
  }
98
99
 
99
- const pathSegments = path.split('?')
100
+ const pathSegments = path.split('?', 3)
100
101
 
101
102
  if (pathSegments.length !== 2) {
102
103
  return path
@@ -367,9 +368,14 @@ function checkNetConnect (netConnect, origin) {
367
368
  return false
368
369
  }
369
370
 
370
- function buildMockOptions (opts) {
371
+ function buildAndValidateMockOptions (opts) {
371
372
  if (opts) {
372
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
+
373
379
  return mockOptions
374
380
  }
375
381
  }
@@ -387,7 +393,7 @@ module.exports = {
387
393
  mockDispatch,
388
394
  buildMockDispatch,
389
395
  checkNetConnect,
390
- buildMockOptions,
396
+ buildAndValidateMockOptions,
391
397
  getHeaderByName,
392
398
  buildHeadersFromArray
393
399
  }
package/lib/util/cache.js CHANGED
@@ -12,7 +12,21 @@ function makeCacheKey (opts) {
12
12
  throw new Error('opts.origin is undefined')
13
13
  }
14
14
 
15
- /** @type {Record<string, string[] | string>} */
15
+ const headers = normaliseHeaders(opts)
16
+
17
+ return {
18
+ origin: opts.origin.toString(),
19
+ method: opts.method,
20
+ path: opts.path,
21
+ headers
22
+ }
23
+ }
24
+
25
+ /**
26
+ * @param {Record<string, string[] | string>}
27
+ * @return {Record<string, string[] | string>}
28
+ */
29
+ function normaliseHeaders (opts) {
16
30
  let headers
17
31
  if (opts.headers == null) {
18
32
  headers = {}
@@ -38,12 +52,7 @@ function makeCacheKey (opts) {
38
52
  throw new Error('opts.headers is not an object')
39
53
  }
40
54
 
41
- return {
42
- origin: opts.origin.toString(),
43
- method: opts.method,
44
- path: opts.path,
45
- headers
46
- }
55
+ return headers
47
56
  }
48
57
 
49
58
  /**
@@ -350,6 +359,7 @@ function assertCacheMethods (methods, name = 'CacheMethods') {
350
359
 
351
360
  module.exports = {
352
361
  makeCacheKey,
362
+ normaliseHeaders,
353
363
  assertCacheKey,
354
364
  assertCacheValue,
355
365
  parseCacheControlHeader,
@@ -309,7 +309,9 @@ function finalizeAndReportTiming (response, initiatorType = 'other') {
309
309
  originalURL.href,
310
310
  initiatorType,
311
311
  globalThis,
312
- cacheState
312
+ cacheState,
313
+ '', // bodyType
314
+ response.status
313
315
  )
314
316
  }
315
317
 
@@ -994,7 +996,7 @@ function fetchFinale (fetchParams, response) {
994
996
  // 3. Set fetchParams’s controller’s report timing steps to the following steps given a global object global:
995
997
  fetchParams.controller.reportTimingSteps = () => {
996
998
  // 1. If fetchParams’s request’s URL’s scheme is not an HTTP(S) scheme, then return.
997
- if (fetchParams.request.url.protocol !== 'https:') {
999
+ if (!urlIsHttpHttpsScheme(fetchParams.request.url)) {
998
1000
  return
999
1001
  }
1000
1002
 
@@ -1036,7 +1038,6 @@ function fetchFinale (fetchParams, response) {
1036
1038
  // fetchParams’s request’s URL, fetchParams’s request’s initiator type, global, cacheState, bodyInfo,
1037
1039
  // and responseStatus.
1038
1040
  if (fetchParams.request.initiatorType != null) {
1039
- // TODO: update markresourcetiming
1040
1041
  markResourceTiming(timingInfo, fetchParams.request.url.href, fetchParams.request.initiatorType, globalThis, cacheState, bodyInfo, responseStatus)
1041
1042
  }
1042
1043
  }
@@ -206,7 +206,7 @@ function parseExtensions (extensions) {
206
206
 
207
207
  while (position.position < extensions.length) {
208
208
  const pair = collectASequenceOfCodePointsFast(';', extensions, position)
209
- const [name, value = ''] = pair.split('=')
209
+ const [name, value = ''] = pair.split('=', 2)
210
210
 
211
211
  extensionList.set(
212
212
  removeHTTPWhitespace(name, true, false),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "7.4.0",
3
+ "version": "7.6.0",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
@@ -108,7 +108,7 @@
108
108
  },
109
109
  "devDependencies": {
110
110
  "@fastify/busboy": "3.1.1",
111
- "@matteo.collina/tspl": "^0.1.1",
111
+ "@matteo.collina/tspl": "^0.2.0",
112
112
  "@sinonjs/fake-timers": "^12.0.0",
113
113
  "@types/node": "^18.19.50",
114
114
  "abort-controller": "^3.0.0",
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
@@ -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
  }
@@ -0,0 +1,111 @@
1
+ import Dispatcher from './dispatcher'
2
+
3
+ declare namespace MockCallHistoryLog {
4
+ /** request's configuration properties */
5
+ export type MockCallHistoryLogProperties = 'protocol' | 'host' | 'port' | 'origin' | 'path' | 'hash' | 'fullUrl' | 'method' | 'searchParams' | 'body' | 'headers'
6
+ }
7
+
8
+ /** a log reflecting request configuration */
9
+ declare class MockCallHistoryLog {
10
+ constructor (requestInit: Dispatcher.DispatchOptions)
11
+ /** protocol used. ie. 'https:' or 'http:' etc... */
12
+ protocol: string
13
+ /** request's host. */
14
+ host: string
15
+ /** request's port. */
16
+ port: string
17
+ /** request's origin. ie. https://localhost:3000. */
18
+ origin: string
19
+ /** path. never contains searchParams. */
20
+ path: string
21
+ /** request's hash. */
22
+ hash: string
23
+ /** the full url requested. */
24
+ fullUrl: string
25
+ /** request's method. */
26
+ method: string
27
+ /** search params. */
28
+ searchParams: Record<string, string>
29
+ /** request's body */
30
+ body: string | null | undefined
31
+ /** request's headers */
32
+ headers: Record<string, string | string[]> | null | undefined
33
+
34
+ /** returns an Map of property / value pair */
35
+ toMap (): Map<MockCallHistoryLog.MockCallHistoryLogProperties, string | Record<string, string | string[]> | null | undefined>
36
+
37
+ /** returns a string computed with all key value pair */
38
+ toString (): string
39
+ }
40
+
41
+ declare namespace MockCallHistory {
42
+ export type FilterCallsOperator = 'AND' | 'OR'
43
+
44
+ /** modify the filtering behavior */
45
+ export interface FilterCallsOptions {
46
+ /** the operator to apply when filtering. 'OR' will adds any MockCallHistoryLog matching any criteria given. 'AND' will adds only MockCallHistoryLog matching every criteria given. (default 'OR') */
47
+ operator?: FilterCallsOperator | Lowercase<FilterCallsOperator>
48
+ }
49
+ /** a function to be executed for filtering MockCallHistoryLog */
50
+ export type FilterCallsFunctionCriteria = (log: MockCallHistoryLog) => boolean
51
+
52
+ /** parameter to filter MockCallHistoryLog */
53
+ export type FilterCallsParameter = string | RegExp | undefined | null
54
+
55
+ /** an object to execute multiple filtering at once */
56
+ export interface FilterCallsObjectCriteria extends Record<string, FilterCallsParameter> {
57
+ /** filter by request protocol. ie https: */
58
+ protocol?: FilterCallsParameter;
59
+ /** filter by request host. */
60
+ host?: FilterCallsParameter;
61
+ /** filter by request port. */
62
+ port?: FilterCallsParameter;
63
+ /** filter by request origin. */
64
+ origin?: FilterCallsParameter;
65
+ /** filter by request path. */
66
+ path?: FilterCallsParameter;
67
+ /** filter by request hash. */
68
+ hash?: FilterCallsParameter;
69
+ /** filter by request fullUrl. */
70
+ fullUrl?: FilterCallsParameter;
71
+ /** filter by request method. */
72
+ method?: FilterCallsParameter;
73
+ }
74
+ }
75
+
76
+ /** a call history to track requests configuration */
77
+ declare class MockCallHistory {
78
+ constructor (name: string)
79
+ /** returns an array of MockCallHistoryLog. */
80
+ calls (): Array<MockCallHistoryLog>
81
+ /** returns the first MockCallHistoryLog */
82
+ firstCall (): MockCallHistoryLog | undefined
83
+ /** returns the last MockCallHistoryLog. */
84
+ lastCall (): MockCallHistoryLog | undefined
85
+ /** returns the nth MockCallHistoryLog. */
86
+ nthCall (position: number): MockCallHistoryLog | undefined
87
+ /** return all MockCallHistoryLog matching any of criteria given. if an object is used with multiple properties, you can change the operator to apply during filtering on options */
88
+ filterCalls (criteria: MockCallHistory.FilterCallsFunctionCriteria | MockCallHistory.FilterCallsObjectCriteria | RegExp, options?: MockCallHistory.FilterCallsOptions): Array<MockCallHistoryLog>
89
+ /** return all MockCallHistoryLog matching the given protocol. if a string is given, it is matched with includes */
90
+ filterCallsByProtocol (protocol: MockCallHistory.FilterCallsParameter): Array<MockCallHistoryLog>
91
+ /** return all MockCallHistoryLog matching the given host. if a string is given, it is matched with includes */
92
+ filterCallsByHost (host: MockCallHistory.FilterCallsParameter): Array<MockCallHistoryLog>
93
+ /** return all MockCallHistoryLog matching the given port. if a string is given, it is matched with includes */
94
+ filterCallsByPort (port: MockCallHistory.FilterCallsParameter): Array<MockCallHistoryLog>
95
+ /** return all MockCallHistoryLog matching the given origin. if a string is given, it is matched with includes */
96
+ filterCallsByOrigin (origin: MockCallHistory.FilterCallsParameter): Array<MockCallHistoryLog>
97
+ /** return all MockCallHistoryLog matching the given path. if a string is given, it is matched with includes */
98
+ filterCallsByPath (path: MockCallHistory.FilterCallsParameter): Array<MockCallHistoryLog>
99
+ /** return all MockCallHistoryLog matching the given hash. if a string is given, it is matched with includes */
100
+ filterCallsByHash (hash: MockCallHistory.FilterCallsParameter): Array<MockCallHistoryLog>
101
+ /** return all MockCallHistoryLog matching the given fullUrl. if a string is given, it is matched with includes */
102
+ filterCallsByFullUrl (fullUrl: MockCallHistory.FilterCallsParameter): Array<MockCallHistoryLog>
103
+ /** return all MockCallHistoryLog matching the given method. if a string is given, it is matched with includes */
104
+ filterCallsByMethod (method: MockCallHistory.FilterCallsParameter): Array<MockCallHistoryLog>
105
+ /** clear all MockCallHistoryLog on this MockCallHistory. */
106
+ clear (): void
107
+ /** use it with for..of loop or spread operator */
108
+ [Symbol.iterator]: () => Generator<MockCallHistoryLog>
109
+ }
110
+
111
+ export { MockCallHistoryLog, MockCallHistory }