diff --git a/lib/mock/snapshot-recorder.js b/lib/mock/snapshot-recorder.js index 7482b5c1914..298fcac1d66 100644 --- a/lib/mock/snapshot-recorder.js +++ b/lib/mock/snapshot-recorder.js @@ -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 @@ -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') } /** @@ -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 } /** diff --git a/test/snapshot-testing.js b/test/snapshot-testing.js index 39c50f01fde..b44b2bb5cd6 100644 --- a/test/snapshot-testing.js +++ b/test/snapshot-testing.js @@ -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() @@ -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() @@ -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()