undici 7.4.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/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.js +3 -0
- package/lib/dispatcher/client.js +1 -1
- 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 +8 -2
- package/package.json +1 -1
- 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
|
@@ -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/dispatcher/client.js
CHANGED
|
@@ -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
|
}
|
package/lib/dispatcher/pool.js
CHANGED
|
@@ -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] () {
|
|
@@ -11,8 +11,8 @@ const {
|
|
|
11
11
|
} = require('../core/util')
|
|
12
12
|
|
|
13
13
|
function calculateRetryAfterHeader (retryAfter) {
|
|
14
|
-
const
|
|
15
|
-
return
|
|
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(
|
|
127
|
+
? calculateRetryAfterHeader(headers['retry-after'])
|
|
128
128
|
: retryAfterHeader * 1e3 // Retry-After is in seconds
|
|
129
129
|
}
|
|
130
130
|
|
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') {
|
|
@@ -367,9 +368,14 @@ function checkNetConnect (netConnect, origin) {
|
|
|
367
368
|
return false
|
|
368
369
|
}
|
|
369
370
|
|
|
370
|
-
function
|
|
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
|
-
|
|
396
|
+
buildAndValidateMockOptions,
|
|
391
397
|
getHeaderByName,
|
|
392
398
|
buildHeadersFromArray
|
|
393
399
|
}
|
package/package.json
CHANGED
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
|
}
|
|
@@ -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 }
|