undici 7.11.0 → 7.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/docs/docs/api/DiagnosticsChannel.md +7 -4
- package/docs/docs/api/Dispatcher.md +2 -2
- package/docs/docs/api/ProxyAgent.md +1 -1
- package/docs/docs/api/SnapshotAgent.md +616 -0
- package/docs/docs/api/WebSocket.md +27 -0
- package/index.js +5 -1
- package/lib/api/readable.js +49 -29
- package/lib/core/request.js +6 -1
- package/lib/core/tree.js +1 -1
- package/lib/core/util.js +0 -1
- package/lib/dispatcher/client-h1.js +8 -17
- package/lib/dispatcher/proxy-agent.js +67 -71
- package/lib/handler/cache-handler.js +4 -1
- package/lib/handler/redirect-handler.js +12 -2
- package/lib/interceptor/cache.js +2 -2
- package/lib/interceptor/dump.js +2 -1
- package/lib/interceptor/redirect.js +1 -1
- package/lib/mock/mock-agent.js +10 -4
- package/lib/mock/snapshot-agent.js +333 -0
- package/lib/mock/snapshot-recorder.js +517 -0
- package/lib/util/cache.js +1 -1
- package/lib/util/promise.js +28 -0
- package/lib/web/cache/cache.js +10 -8
- package/lib/web/fetch/body.js +35 -24
- package/lib/web/fetch/formdata-parser.js +0 -3
- package/lib/web/fetch/formdata.js +0 -4
- package/lib/web/fetch/index.js +221 -225
- package/lib/web/fetch/request.js +15 -7
- package/lib/web/fetch/response.js +5 -3
- package/lib/web/fetch/util.js +21 -23
- package/lib/web/webidl/index.js +1 -1
- package/lib/web/websocket/connection.js +0 -9
- package/lib/web/websocket/receiver.js +2 -12
- package/lib/web/websocket/stream/websocketstream.js +7 -4
- package/lib/web/websocket/websocket.js +57 -1
- package/package.json +2 -2
- package/types/agent.d.ts +0 -4
- package/types/client.d.ts +0 -2
- package/types/dispatcher.d.ts +0 -6
- package/types/h2c-client.d.ts +0 -2
- package/types/index.d.ts +3 -1
- package/types/mock-interceptor.d.ts +0 -1
- package/types/snapshot-agent.d.ts +107 -0
- package/types/webidl.d.ts +10 -0
- package/types/websocket.d.ts +2 -0
- package/lib/web/fetch/dispatcher-weakref.js +0 -5
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
# SnapshotAgent
|
|
2
|
+
|
|
3
|
+
The `SnapshotAgent` provides a powerful way to record and replay HTTP requests for testing purposes. It extends `MockAgent` to enable automatic snapshot testing, eliminating the need to manually define mock responses.
|
|
4
|
+
|
|
5
|
+
## Use Cases
|
|
6
|
+
|
|
7
|
+
- **Integration Testing**: Record real API interactions and replay them in tests
|
|
8
|
+
- **Offline Development**: Work with APIs without network connectivity
|
|
9
|
+
- **Consistent Test Data**: Ensure tests use the same responses across runs
|
|
10
|
+
- **API Contract Testing**: Capture and validate API behavior over time
|
|
11
|
+
|
|
12
|
+
## Constructor
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
new SnapshotAgent([options])
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Parameters
|
|
19
|
+
|
|
20
|
+
- **options** `Object` (optional)
|
|
21
|
+
- **mode** `String` - The snapshot mode: `'record'`, `'playback'`, or `'update'`. Default: `'record'`
|
|
22
|
+
- **snapshotPath** `String` - Path to the snapshot file for loading/saving
|
|
23
|
+
- **maxSnapshots** `Number` - Maximum number of snapshots to keep in memory. Default: `Infinity`
|
|
24
|
+
- **autoFlush** `Boolean` - Whether to automatically save snapshots to disk. Default: `false`
|
|
25
|
+
- **flushInterval** `Number` - Interval in milliseconds for auto-flush. Default: `30000`
|
|
26
|
+
- **matchHeaders** `Array<String>` - Specific headers to include in request matching. Default: all headers
|
|
27
|
+
- **ignoreHeaders** `Array<String>` - Headers to ignore during request matching
|
|
28
|
+
- **excludeHeaders** `Array<String>` - Headers to exclude from snapshots (for security)
|
|
29
|
+
- **matchBody** `Boolean` - Whether to include request body in matching. Default: `true`
|
|
30
|
+
- **matchQuery** `Boolean` - Whether to include query parameters in matching. Default: `true`
|
|
31
|
+
- **caseSensitive** `Boolean` - Whether header matching is case-sensitive. Default: `false`
|
|
32
|
+
- **shouldRecord** `Function` - Callback to determine if a request should be recorded
|
|
33
|
+
- **shouldPlayback** `Function` - Callback to determine if a request should be played back
|
|
34
|
+
- **excludeUrls** `Array` - URL patterns (strings or RegExp) to exclude from recording/playback
|
|
35
|
+
- All other options from `MockAgent` are supported
|
|
36
|
+
|
|
37
|
+
### Modes
|
|
38
|
+
|
|
39
|
+
#### Record Mode (`'record'`)
|
|
40
|
+
Makes real HTTP requests and saves the responses to snapshots.
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
|
|
44
|
+
|
|
45
|
+
const agent = new SnapshotAgent({
|
|
46
|
+
mode: 'record',
|
|
47
|
+
snapshotPath: './test/snapshots/api-calls.json'
|
|
48
|
+
})
|
|
49
|
+
setGlobalDispatcher(agent)
|
|
50
|
+
|
|
51
|
+
// Makes real requests and records them
|
|
52
|
+
const response = await fetch('https://api.example.com/users')
|
|
53
|
+
const users = await response.json()
|
|
54
|
+
|
|
55
|
+
// Save recorded snapshots
|
|
56
|
+
await agent.saveSnapshots()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### Playback Mode (`'playback'`)
|
|
60
|
+
Replays recorded responses without making real HTTP requests.
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
|
|
64
|
+
|
|
65
|
+
const agent = new SnapshotAgent({
|
|
66
|
+
mode: 'playback',
|
|
67
|
+
snapshotPath: './test/snapshots/api-calls.json'
|
|
68
|
+
})
|
|
69
|
+
setGlobalDispatcher(agent)
|
|
70
|
+
|
|
71
|
+
// Uses recorded response instead of real request
|
|
72
|
+
const response = await fetch('https://api.example.com/users')
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Update Mode (`'update'`)
|
|
76
|
+
Uses existing snapshots when available, but records new ones for missing requests.
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
|
|
80
|
+
|
|
81
|
+
const agent = new SnapshotAgent({
|
|
82
|
+
mode: 'update',
|
|
83
|
+
snapshotPath: './test/snapshots/api-calls.json'
|
|
84
|
+
})
|
|
85
|
+
setGlobalDispatcher(agent)
|
|
86
|
+
|
|
87
|
+
// Uses snapshot if exists, otherwise makes real request and records it
|
|
88
|
+
const response = await fetch('https://api.example.com/new-endpoint')
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Instance Methods
|
|
92
|
+
|
|
93
|
+
### `agent.saveSnapshots([filePath])`
|
|
94
|
+
|
|
95
|
+
Saves all recorded snapshots to a file.
|
|
96
|
+
|
|
97
|
+
#### Parameters
|
|
98
|
+
|
|
99
|
+
- **filePath** `String` (optional) - Path to save snapshots. Uses constructor `snapshotPath` if not provided.
|
|
100
|
+
|
|
101
|
+
#### Returns
|
|
102
|
+
|
|
103
|
+
`Promise<void>`
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
await agent.saveSnapshots('./custom-snapshots.json')
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Advanced Configuration
|
|
110
|
+
|
|
111
|
+
### Header Filtering
|
|
112
|
+
|
|
113
|
+
Control which headers are used for request matching and what gets stored in snapshots:
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
const agent = new SnapshotAgent({
|
|
117
|
+
mode: 'record',
|
|
118
|
+
snapshotPath: './snapshots.json',
|
|
119
|
+
|
|
120
|
+
// Only match these specific headers
|
|
121
|
+
matchHeaders: ['content-type', 'accept'],
|
|
122
|
+
|
|
123
|
+
// Ignore these headers during matching (but still store them)
|
|
124
|
+
ignoreHeaders: ['user-agent', 'date'],
|
|
125
|
+
|
|
126
|
+
// Exclude sensitive headers from snapshots entirely
|
|
127
|
+
excludeHeaders: ['authorization', 'x-api-key', 'cookie']
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Custom Request/Response Filtering
|
|
132
|
+
|
|
133
|
+
Use callback functions to determine what gets recorded or played back:
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
const agent = new SnapshotAgent({
|
|
137
|
+
mode: 'record',
|
|
138
|
+
snapshotPath: './snapshots.json',
|
|
139
|
+
|
|
140
|
+
// Only record GET requests to specific endpoints
|
|
141
|
+
shouldRecord: (requestOpts) => {
|
|
142
|
+
const url = new URL(requestOpts.path, requestOpts.origin)
|
|
143
|
+
return requestOpts.method === 'GET' && url.pathname.startsWith('/api/v1/')
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// Skip authentication endpoints during playback
|
|
147
|
+
shouldPlayback: (requestOpts) => {
|
|
148
|
+
const url = new URL(requestOpts.path, requestOpts.origin)
|
|
149
|
+
return !url.pathname.includes('/auth/')
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### URL Pattern Exclusion
|
|
155
|
+
|
|
156
|
+
Exclude specific URLs from recording/playback using patterns:
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
const agent = new SnapshotAgent({
|
|
160
|
+
mode: 'record',
|
|
161
|
+
snapshotPath: './snapshots.json',
|
|
162
|
+
|
|
163
|
+
excludeUrls: [
|
|
164
|
+
'https://analytics.example.com', // String match
|
|
165
|
+
/\/api\/v\d+\/health/, // Regex pattern
|
|
166
|
+
'telemetry' // Substring match
|
|
167
|
+
]
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Memory Management
|
|
172
|
+
|
|
173
|
+
Configure automatic memory and disk management:
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
const agent = new SnapshotAgent({
|
|
177
|
+
mode: 'record',
|
|
178
|
+
snapshotPath: './snapshots.json',
|
|
179
|
+
|
|
180
|
+
// Keep only 1000 snapshots in memory
|
|
181
|
+
maxSnapshots: 1000,
|
|
182
|
+
|
|
183
|
+
// Automatically save to disk every 30 seconds
|
|
184
|
+
autoFlush: true,
|
|
185
|
+
flushInterval: 30000
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Sequential Response Handling
|
|
190
|
+
|
|
191
|
+
Handle multiple responses for the same request (similar to nock):
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
// In record mode, multiple identical requests get recorded as separate responses
|
|
195
|
+
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './sequential.json' })
|
|
196
|
+
|
|
197
|
+
// First call returns response A
|
|
198
|
+
await fetch('https://api.example.com/random')
|
|
199
|
+
|
|
200
|
+
// Second call returns response B
|
|
201
|
+
await fetch('https://api.example.com/random')
|
|
202
|
+
|
|
203
|
+
await agent.saveSnapshots()
|
|
204
|
+
|
|
205
|
+
// In playback mode, calls return responses in sequence
|
|
206
|
+
const playbackAgent = new SnapshotAgent({ mode: 'playback', snapshotPath: './sequential.json' })
|
|
207
|
+
|
|
208
|
+
// Returns response A
|
|
209
|
+
const first = await fetch('https://api.example.com/random')
|
|
210
|
+
|
|
211
|
+
// Returns response B
|
|
212
|
+
const second = await fetch('https://api.example.com/random')
|
|
213
|
+
|
|
214
|
+
// Third call repeats the last response (B)
|
|
215
|
+
const third = await fetch('https://api.example.com/random')
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Managing Snapshots
|
|
219
|
+
|
|
220
|
+
### Replacing Existing Snapshots
|
|
221
|
+
|
|
222
|
+
```javascript
|
|
223
|
+
// Load existing snapshots
|
|
224
|
+
await agent.loadSnapshots('./old-snapshots.json')
|
|
225
|
+
|
|
226
|
+
// Get snapshot data
|
|
227
|
+
const recorder = agent.getRecorder()
|
|
228
|
+
const snapshots = recorder.getSnapshots()
|
|
229
|
+
|
|
230
|
+
// Modify or filter snapshots
|
|
231
|
+
const filteredSnapshots = snapshots.filter(s =>
|
|
232
|
+
!s.request.url.includes('deprecated')
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
// Replace all snapshots
|
|
236
|
+
agent.replaceSnapshots(filteredSnapshots.map((snapshot, index) => ({
|
|
237
|
+
hash: `new-hash-${index}`,
|
|
238
|
+
snapshot
|
|
239
|
+
})))
|
|
240
|
+
|
|
241
|
+
// Save updated snapshots
|
|
242
|
+
await agent.saveSnapshots('./updated-snapshots.json')
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### `agent.loadSnapshots([filePath])`
|
|
246
|
+
|
|
247
|
+
Loads snapshots from a file.
|
|
248
|
+
|
|
249
|
+
#### Parameters
|
|
250
|
+
|
|
251
|
+
- **filePath** `String` (optional) - Path to load snapshots from. Uses constructor `snapshotPath` if not provided.
|
|
252
|
+
|
|
253
|
+
#### Returns
|
|
254
|
+
|
|
255
|
+
`Promise<void>`
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
await agent.loadSnapshots('./existing-snapshots.json')
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### `agent.getRecorder()`
|
|
262
|
+
|
|
263
|
+
Gets the underlying `SnapshotRecorder` instance.
|
|
264
|
+
|
|
265
|
+
#### Returns
|
|
266
|
+
|
|
267
|
+
`SnapshotRecorder`
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
const recorder = agent.getRecorder()
|
|
271
|
+
console.log(`Recorded ${recorder.size()} interactions`)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### `agent.getMode()`
|
|
275
|
+
|
|
276
|
+
Gets the current snapshot mode.
|
|
277
|
+
|
|
278
|
+
#### Returns
|
|
279
|
+
|
|
280
|
+
`String` - The current mode (`'record'`, `'playback'`, or `'update'`)
|
|
281
|
+
|
|
282
|
+
### `agent.clearSnapshots()`
|
|
283
|
+
|
|
284
|
+
Clears all recorded snapshots from memory.
|
|
285
|
+
|
|
286
|
+
```javascript
|
|
287
|
+
agent.clearSnapshots()
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Working with Different Request Types
|
|
291
|
+
|
|
292
|
+
### GET Requests
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
// Record mode
|
|
296
|
+
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './get-snapshots.json' })
|
|
297
|
+
setGlobalDispatcher(agent)
|
|
298
|
+
|
|
299
|
+
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1')
|
|
300
|
+
const post = await response.json()
|
|
301
|
+
|
|
302
|
+
await agent.saveSnapshots()
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### POST Requests with Body
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
// Record mode
|
|
309
|
+
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './post-snapshots.json' })
|
|
310
|
+
setGlobalDispatcher(agent)
|
|
311
|
+
|
|
312
|
+
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
headers: { 'Content-Type': 'application/json' },
|
|
315
|
+
body: JSON.stringify({ title: 'Test Post', body: 'Content' })
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
await agent.saveSnapshots()
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Using with `undici.request`
|
|
322
|
+
|
|
323
|
+
SnapshotAgent works with all undici APIs, not just fetch:
|
|
324
|
+
|
|
325
|
+
```javascript
|
|
326
|
+
import { SnapshotAgent, request, setGlobalDispatcher } from 'undici'
|
|
327
|
+
|
|
328
|
+
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './request-snapshots.json' })
|
|
329
|
+
setGlobalDispatcher(agent)
|
|
330
|
+
|
|
331
|
+
const { statusCode, headers, body } = await request('https://api.example.com/data')
|
|
332
|
+
const data = await body.json()
|
|
333
|
+
|
|
334
|
+
await agent.saveSnapshots()
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Test Integration
|
|
338
|
+
|
|
339
|
+
### Basic Test Setup
|
|
340
|
+
|
|
341
|
+
```javascript
|
|
342
|
+
import { test } from 'node:test'
|
|
343
|
+
import { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher } from 'undici'
|
|
344
|
+
|
|
345
|
+
test('API integration test', async (t) => {
|
|
346
|
+
const originalDispatcher = getGlobalDispatcher()
|
|
347
|
+
|
|
348
|
+
const agent = new SnapshotAgent({
|
|
349
|
+
mode: 'playback',
|
|
350
|
+
snapshotPath: './test/snapshots/api-test.json'
|
|
351
|
+
})
|
|
352
|
+
setGlobalDispatcher(agent)
|
|
353
|
+
|
|
354
|
+
t.after(() => setGlobalDispatcher(originalDispatcher))
|
|
355
|
+
|
|
356
|
+
// This will use recorded data
|
|
357
|
+
const response = await fetch('https://api.example.com/users')
|
|
358
|
+
const users = await response.json()
|
|
359
|
+
|
|
360
|
+
assert(Array.isArray(users))
|
|
361
|
+
assert(users.length > 0)
|
|
362
|
+
})
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Environment-Based Mode Selection
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
const mode = process.env.SNAPSHOT_MODE || 'playback'
|
|
369
|
+
|
|
370
|
+
const agent = new SnapshotAgent({
|
|
371
|
+
mode,
|
|
372
|
+
snapshotPath: './test/snapshots/integration.json'
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
// Run with: SNAPSHOT_MODE=record npm test (to record)
|
|
376
|
+
// Run with: npm test (to playback)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Test Helper Function
|
|
380
|
+
|
|
381
|
+
```javascript
|
|
382
|
+
function createSnapshotAgent(testName, mode = 'playback') {
|
|
383
|
+
return new SnapshotAgent({
|
|
384
|
+
mode,
|
|
385
|
+
snapshotPath: `./test/snapshots/${testName}.json`
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
test('user API test', async (t) => {
|
|
390
|
+
const agent = createSnapshotAgent('user-api')
|
|
391
|
+
setGlobalDispatcher(agent)
|
|
392
|
+
|
|
393
|
+
// Test implementation...
|
|
394
|
+
})
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Snapshot File Format
|
|
398
|
+
|
|
399
|
+
Snapshots are stored as JSON with the following structure:
|
|
400
|
+
|
|
401
|
+
```json
|
|
402
|
+
[
|
|
403
|
+
{
|
|
404
|
+
"hash": "dGVzdC1oYXNo...",
|
|
405
|
+
"snapshot": {
|
|
406
|
+
"request": {
|
|
407
|
+
"method": "GET",
|
|
408
|
+
"url": "https://api.example.com/users",
|
|
409
|
+
"headers": {
|
|
410
|
+
"authorization": "Bearer token"
|
|
411
|
+
},
|
|
412
|
+
"body": undefined
|
|
413
|
+
},
|
|
414
|
+
"response": {
|
|
415
|
+
"statusCode": 200,
|
|
416
|
+
"headers": {
|
|
417
|
+
"content-type": "application/json"
|
|
418
|
+
},
|
|
419
|
+
"body": "eyJkYXRhIjoidGVzdCJ9", // base64 encoded
|
|
420
|
+
"trailers": {}
|
|
421
|
+
},
|
|
422
|
+
"timestamp": "2024-01-01T00:00:00.000Z"
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Security Considerations
|
|
429
|
+
|
|
430
|
+
### Sensitive Data in Snapshots
|
|
431
|
+
|
|
432
|
+
By default, SnapshotAgent records all headers and request/response data. For production use, always exclude sensitive information:
|
|
433
|
+
|
|
434
|
+
```javascript
|
|
435
|
+
const agent = new SnapshotAgent({
|
|
436
|
+
mode: 'record',
|
|
437
|
+
snapshotPath: './snapshots.json',
|
|
438
|
+
|
|
439
|
+
// Exclude sensitive headers from snapshots
|
|
440
|
+
excludeHeaders: [
|
|
441
|
+
'authorization',
|
|
442
|
+
'x-api-key',
|
|
443
|
+
'cookie',
|
|
444
|
+
'set-cookie',
|
|
445
|
+
'x-auth-token',
|
|
446
|
+
'x-csrf-token'
|
|
447
|
+
],
|
|
448
|
+
|
|
449
|
+
// Filter out requests with sensitive data
|
|
450
|
+
shouldRecord: (requestOpts) => {
|
|
451
|
+
const url = new URL(requestOpts.path, requestOpts.origin)
|
|
452
|
+
|
|
453
|
+
// Don't record authentication endpoints
|
|
454
|
+
if (url.pathname.includes('/auth/') || url.pathname.includes('/login')) {
|
|
455
|
+
return false
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Don't record if request contains sensitive body data
|
|
459
|
+
if (requestOpts.body && typeof requestOpts.body === 'string') {
|
|
460
|
+
const body = requestOpts.body.toLowerCase()
|
|
461
|
+
if (body.includes('password') || body.includes('secret')) {
|
|
462
|
+
return false
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return true
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Snapshot File Security
|
|
472
|
+
|
|
473
|
+
**Important**: Snapshot files may contain sensitive data. Handle them securely:
|
|
474
|
+
|
|
475
|
+
- ✅ Add snapshot files to `.gitignore` if they contain real API data
|
|
476
|
+
- ✅ Use environment-specific snapshots (dev/staging/prod)
|
|
477
|
+
- ✅ Regularly review snapshot contents for sensitive information
|
|
478
|
+
- ✅ Use the `excludeHeaders` option for production snapshots
|
|
479
|
+
- ❌ Never commit snapshots with real authentication tokens
|
|
480
|
+
- ❌ Don't share snapshot files containing personal data
|
|
481
|
+
|
|
482
|
+
```gitignore
|
|
483
|
+
# Exclude snapshots with real data
|
|
484
|
+
/test/snapshots/production-*.json
|
|
485
|
+
/test/snapshots/*-real-data.json
|
|
486
|
+
|
|
487
|
+
# Include sanitized test snapshots
|
|
488
|
+
!/test/snapshots/mock-*.json
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## Error Handling
|
|
492
|
+
|
|
493
|
+
### Missing Snapshots in Playback Mode
|
|
494
|
+
|
|
495
|
+
```javascript
|
|
496
|
+
try {
|
|
497
|
+
const response = await fetch('https://api.example.com/nonexistent')
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if (error.message.includes('No snapshot found')) {
|
|
500
|
+
// Handle missing snapshot
|
|
501
|
+
console.log('Snapshot not found for this request')
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Handling Network Errors in Record Mode
|
|
507
|
+
|
|
508
|
+
```javascript
|
|
509
|
+
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './snapshots.json' })
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const response = await fetch('https://nonexistent-api.example.com/data')
|
|
513
|
+
} catch (error) {
|
|
514
|
+
// Network errors are not recorded as snapshots
|
|
515
|
+
console.log('Network error:', error.message)
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
## Best Practices
|
|
520
|
+
|
|
521
|
+
### 1. Organize Snapshots by Test Suite
|
|
522
|
+
|
|
523
|
+
```javascript
|
|
524
|
+
// Use descriptive snapshot file names
|
|
525
|
+
const agent = new SnapshotAgent({
|
|
526
|
+
mode: 'playback',
|
|
527
|
+
snapshotPath: `./test/snapshots/${testSuiteName}-${testName}.json`
|
|
528
|
+
})
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### 2. Version Control Snapshots
|
|
532
|
+
|
|
533
|
+
Add snapshot files to version control to ensure consistent test behavior across environments:
|
|
534
|
+
|
|
535
|
+
```gitignore
|
|
536
|
+
# Include snapshots in version control
|
|
537
|
+
!/test/snapshots/*.json
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### 3. Clean Up Test Data
|
|
541
|
+
|
|
542
|
+
```javascript
|
|
543
|
+
test('API test', async (t) => {
|
|
544
|
+
const agent = new SnapshotAgent({
|
|
545
|
+
mode: 'playback',
|
|
546
|
+
snapshotPath: './test/snapshots/temp-test.json'
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
// Clean up after test
|
|
550
|
+
t.after(() => {
|
|
551
|
+
agent.clearSnapshots()
|
|
552
|
+
})
|
|
553
|
+
})
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### 4. Snapshot Validation
|
|
557
|
+
|
|
558
|
+
```javascript
|
|
559
|
+
test('validate snapshot contents', async (t) => {
|
|
560
|
+
const agent = new SnapshotAgent({
|
|
561
|
+
mode: 'playback',
|
|
562
|
+
snapshotPath: './test/snapshots/validation.json'
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
const recorder = agent.getRecorder()
|
|
566
|
+
const snapshots = recorder.getSnapshots()
|
|
567
|
+
|
|
568
|
+
// Validate snapshot structure
|
|
569
|
+
assert(snapshots.length > 0, 'Should have recorded snapshots')
|
|
570
|
+
assert(snapshots[0].request.url.startsWith('https://'), 'Should use HTTPS')
|
|
571
|
+
})
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
## Comparison with Other Tools
|
|
575
|
+
|
|
576
|
+
### vs Manual MockAgent Setup
|
|
577
|
+
|
|
578
|
+
**Manual MockAgent:**
|
|
579
|
+
```javascript
|
|
580
|
+
const mockAgent = new MockAgent()
|
|
581
|
+
const mockPool = mockAgent.get('https://api.example.com')
|
|
582
|
+
|
|
583
|
+
mockPool.intercept({
|
|
584
|
+
path: '/users',
|
|
585
|
+
method: 'GET'
|
|
586
|
+
}).reply(200, [
|
|
587
|
+
{ id: 1, name: 'User 1' },
|
|
588
|
+
{ id: 2, name: 'User 2' }
|
|
589
|
+
])
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**SnapshotAgent:**
|
|
593
|
+
```javascript
|
|
594
|
+
// Record once
|
|
595
|
+
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './snapshots.json' })
|
|
596
|
+
// Real API call gets recorded automatically
|
|
597
|
+
|
|
598
|
+
// Use in tests
|
|
599
|
+
const agent = new SnapshotAgent({ mode: 'playback', snapshotPath: './snapshots.json' })
|
|
600
|
+
// Automatically replays recorded response
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### vs nock
|
|
604
|
+
|
|
605
|
+
SnapshotAgent provides similar functionality to nock but is specifically designed for undici:
|
|
606
|
+
|
|
607
|
+
- ✅ Works with all undici APIs (`request`, `stream`, `pipeline`, etc.)
|
|
608
|
+
- ✅ Supports undici-specific features (RetryAgent, connection pooling)
|
|
609
|
+
- ✅ Better TypeScript integration
|
|
610
|
+
- ✅ More efficient for high-performance scenarios
|
|
611
|
+
|
|
612
|
+
## See Also
|
|
613
|
+
|
|
614
|
+
- [MockAgent](./MockAgent.md) - Manual mocking for more control
|
|
615
|
+
- [MockCallHistory](./MockCallHistory.md) - Inspecting request history
|
|
616
|
+
- [Testing Best Practices](../best-practices/writing-tests.md) - General testing guidance
|
|
@@ -78,6 +78,33 @@ setInterval(() => write(), 5000)
|
|
|
78
78
|
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
## ping(websocket, payload)
|
|
82
|
+
Arguments:
|
|
83
|
+
|
|
84
|
+
* **websocket** `WebSocket` - The WebSocket instance to send the ping frame on
|
|
85
|
+
* **payload** `Buffer|undefined` (optional) - Optional payload data to include with the ping frame. Must not exceed 125 bytes.
|
|
86
|
+
|
|
87
|
+
Sends a ping frame to the WebSocket server. The server must respond with a pong frame containing the same payload data. This can be used for keepalive purposes or to verify that the connection is still active.
|
|
88
|
+
|
|
89
|
+
### Example:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import { WebSocket, ping } from 'undici'
|
|
93
|
+
|
|
94
|
+
const ws = new WebSocket('wss://echo.websocket.events')
|
|
95
|
+
|
|
96
|
+
ws.addEventListener('open', () => {
|
|
97
|
+
// Send ping with no payload
|
|
98
|
+
ping(ws)
|
|
99
|
+
|
|
100
|
+
// Send ping with payload
|
|
101
|
+
const payload = Buffer.from('hello')
|
|
102
|
+
ping(ws, payload)
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Note**: A ping frame cannot have a payload larger than 125 bytes. The ping will only be sent if the WebSocket connection is in the OPEN state.
|
|
107
|
+
|
|
81
108
|
## Read More
|
|
82
109
|
|
|
83
110
|
- [MDN - WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
package/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const MockClient = require('./lib/mock/mock-client')
|
|
|
18
18
|
const { MockCallHistory, MockCallHistoryLog } = require('./lib/mock/mock-call-history')
|
|
19
19
|
const MockAgent = require('./lib/mock/mock-agent')
|
|
20
20
|
const MockPool = require('./lib/mock/mock-pool')
|
|
21
|
+
const SnapshotAgent = require('./lib/mock/snapshot-agent')
|
|
21
22
|
const mockErrors = require('./lib/mock/mock-errors')
|
|
22
23
|
const RetryHandler = require('./lib/handler/retry-handler')
|
|
23
24
|
const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
|
|
@@ -157,10 +158,12 @@ module.exports.parseMIMEType = parseMIMEType
|
|
|
157
158
|
module.exports.serializeAMimeType = serializeAMimeType
|
|
158
159
|
|
|
159
160
|
const { CloseEvent, ErrorEvent, MessageEvent } = require('./lib/web/websocket/events')
|
|
160
|
-
|
|
161
|
+
const { WebSocket, ping } = require('./lib/web/websocket/websocket')
|
|
162
|
+
module.exports.WebSocket = WebSocket
|
|
161
163
|
module.exports.CloseEvent = CloseEvent
|
|
162
164
|
module.exports.ErrorEvent = ErrorEvent
|
|
163
165
|
module.exports.MessageEvent = MessageEvent
|
|
166
|
+
module.exports.ping = ping
|
|
164
167
|
|
|
165
168
|
module.exports.WebSocketStream = require('./lib/web/websocket/stream/websocketstream').WebSocketStream
|
|
166
169
|
module.exports.WebSocketError = require('./lib/web/websocket/stream/websocketerror').WebSocketError
|
|
@@ -176,6 +179,7 @@ module.exports.MockCallHistory = MockCallHistory
|
|
|
176
179
|
module.exports.MockCallHistoryLog = MockCallHistoryLog
|
|
177
180
|
module.exports.MockPool = MockPool
|
|
178
181
|
module.exports.MockAgent = MockAgent
|
|
182
|
+
module.exports.SnapshotAgent = SnapshotAgent
|
|
179
183
|
module.exports.mockErrors = mockErrors
|
|
180
184
|
|
|
181
185
|
const { EventSource } = require('./lib/web/eventsource/eventsource')
|