Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 47 additions & 26 deletions lib/mock/snapshot-recorder.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ const { writeFile, readFile, mkdir } = require('node:fs/promises')
const { dirname, resolve } = require('node:path')
const { InvalidArgumentError, UndiciError } = require('../core/errors')

let crypto
try {
crypto = require('node:crypto')
} catch {
// Fallback if crypto is not available
}

/**
* Formats a request for consistent snapshot storage
* Caches normalized headers to avoid repeated processing
Expand Down Expand Up @@ -137,15 +144,45 @@ function normalizeHeaders (headers) {

/**
* Creates a hash key for request matching
* Properly orders headers to avoid conflicts and uses crypto hashing when available
*/
function createRequestHash (request) {
const parts = [
request.method,
request.url,
JSON.stringify(request.headers, Object.keys(request.headers).sort()),
request.body || ''
request.url
]
return Buffer.from(parts.join('|')).toString('base64url')

// Process headers in a deterministic way to avoid conflicts
if (request.headers && typeof request.headers === 'object') {
const headerKeys = Object.keys(request.headers).sort()
for (const key of headerKeys) {
const lowerKey = key.toLowerCase()
const values = Array.isArray(request.headers[key])
? request.headers[key]
: [request.headers[key]]

// Add header name
parts.push(lowerKey)

// Add all values for this header, sorted for consistency
for (const value of values.sort()) {
parts.push(String(value))
}
}
}

// Add body
parts.push(request.body || '')

const content = parts.join('|')

// Use crypto hash if available for better collision resistance
if (crypto && crypto.createHash) {
return crypto.createHash('sha256').update(content, 'utf8').digest('base64url')
}

// Fallback to base64 encoding
return Buffer.from(content).toString('base64url')
}

/**
Expand Down Expand Up @@ -291,30 +328,14 @@ class SnapshotRecorder {
if (!snapshot) return undefined

// Handle sequential responses
if (snapshot.responses && Array.isArray(snapshot.responses)) {
const currentCallCount = snapshot.callCount || 0
const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1)
snapshot.callCount = currentCallCount + 1

return {
...snapshot,
response: snapshot.responses[responseIndex]
}
}
const currentCallCount = snapshot.callCount || 0
const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1)
snapshot.callCount = currentCallCount + 1

// Legacy format compatibility - convert single response to array format
if (snapshot.response && !snapshot.responses) {
snapshot.responses = [snapshot.response]
snapshot.callCount = 1
delete snapshot.response

return {
...snapshot,
response: snapshot.responses[0]
}
return {
...snapshot,
response: snapshot.responses[responseIndex]
}

return snapshot
}

/**
Expand Down
18 changes: 13 additions & 5 deletions test/snapshot-testing.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,18 @@ describe('SnapshotAgent - Request Handling', () => {
setGlobalDispatcher(recordingAgent)

// Make multiple requests to record sequential responses
await request(`${origin}/api/test`)
await request(`${origin}/api/test`)
await request(`${origin}/api/test`)
{
const res = await request(`${origin}/api/test`)
await res.body.text()
}
{
const res = await request(`${origin}/api/test`)
await res.body.text()
}
{
const res = await request(`${origin}/api/test`)
await res.body.text()
}

// Ensure all recordings are saved and verify the recording state
await recordingAgent.saveSnapshots()
Expand All @@ -370,6 +379,7 @@ describe('SnapshotAgent - Request Handling', () => {
})

setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)

// Ensure snapshots are loaded and call counts are reset before setting dispatcher
await playbackAgent.loadSnapshots()
Expand All @@ -385,8 +395,6 @@ describe('SnapshotAgent - Request Handling', () => {
assert.strictEqual(snapshots.length, 1, 'Should have exactly one snapshot')
assert.strictEqual(snapshots[0].responses.length, 3, 'Should have three sequential responses')

setGlobalDispatcher(playbackAgent)

// Test sequential responses
const response1 = await request(`${origin}/api/test`)
const body1 = await response1.body.text()
Expand Down
Loading