Skip to content
Open
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
10 changes: 9 additions & 1 deletion lib/mock/snapshot-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ class SnapshotAgent extends MockAgent {
this[kSnapshotLoaded] = false

// For recording/update mode, we need a real agent to make actual requests
if (this[kSnapshotMode] === 'record' || this[kSnapshotMode] === 'update') {
// For playback mode, we need a real agent if there are excluded URLs
if (this[kSnapshotMode] === 'record' || this[kSnapshotMode] === 'update' ||
(this[kSnapshotMode] === 'playback' && opts.excludeUrls && opts.excludeUrls.length > 0)) {
this[kRealAgent] = new Agent(opts)
}

Expand All @@ -80,6 +82,12 @@ class SnapshotAgent extends MockAgent {
handler = WrapHandler.wrap(handler)
const mode = this[kSnapshotMode]

// Check if URL should be excluded (pass through without mocking/recording)
if (this[kSnapshotRecorder].isUrlExcluded(opts)) {
// Real agent is guaranteed by constructor when excludeUrls is configured
return this[kRealAgent].dispatch(opts, handler)
}

if (mode === 'playback' || mode === 'update') {
// Ensure snapshots are loaded
if (!this[kSnapshotLoaded]) {
Expand Down
16 changes: 12 additions & 4 deletions lib/mock/snapshot-recorder.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,7 @@ class SnapshotRecorder {
}

// Check URL exclusion patterns
const url = new URL(requestOpts.path, requestOpts.origin).toString()
if (this.#isUrlExcluded(url)) {
if (this.isUrlExcluded(requestOpts)) {
return // Skip recording
}

Expand Down Expand Up @@ -330,6 +329,16 @@ class SnapshotRecorder {
}
}

/**
* Checks if a URL should be excluded from recording/playback
* @param {SnapshotRequestOptions} requestOpts - Request options to check
* @returns {boolean} - True if URL is excluded
*/
isUrlExcluded (requestOpts) {
const url = new URL(requestOpts.path, requestOpts.origin).toString()
return this.#isUrlExcluded(url)
}

/**
* Finds a matching snapshot for the given request
* Returns the appropriate response based on call count for sequential responses
Expand All @@ -344,8 +353,7 @@ class SnapshotRecorder {
}

// Check URL exclusion patterns
const url = new URL(requestOpts.path, requestOpts.origin).toString()
if (this.#isUrlExcluded(url)) {
if (this.isUrlExcluded(requestOpts)) {
return undefined // Skip playback
}

Expand Down
57 changes: 57 additions & 0 deletions test/snapshot-testing.js
Original file line number Diff line number Diff line change
Expand Up @@ -1293,6 +1293,63 @@ describe('SnapshotAgent - Filtering', () => {
assert.strictEqual(snapshots[0].request.method, 'GET',
'Recorded request should have GET method')
})

it('excluded URLs should not error in playback mode', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response from ${req.url}`)
})

const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('exclude-playback-bug')

setupCleanup(t, { server, snapshotPath })

// Record mode: record one request, exclude another
const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath,
excludeUrls: [`${origin}/excluded`]
})

const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent: recordingAgent, originalDispatcher })
setGlobalDispatcher(recordingAgent)

// Request to included endpoint - should be recorded
const res1 = await request(`${origin}/included`)
await res1.body.text()

// Request to excluded endpoint - should NOT be recorded
const res2 = await request(`${origin}/excluded`)
await res2.body.text()

await recordingAgent.saveSnapshots()

const recorder = recordingAgent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should have recorded only the included request')

// Playback mode: should allow excluded URL to pass through without error
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath,
excludeUrls: [`${origin}/excluded`]
})

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

// This should work - replays from snapshot
const res3 = await request(`${origin}/included`)
await res3.body.text()
assert.strictEqual(res3.statusCode, 200, 'Included request should replay successfully')

// Excluded URL should pass through to real server
const res4 = await request(`${origin}/excluded`)
const body4 = await res4.body.text()
assert.strictEqual(res4.statusCode, 200, 'Excluded request should pass through to real server')
assert.strictEqual(body4, 'Response from /excluded', 'Should get live response from server')
})
})

describe('SnapshotAgent - Close Method', () => {
Expand Down
Loading