'use strict'
const { describe, it } = require('node:test')
const assert = require('node:assert')
const { createServer } = require('node:http')
const { promisify } = require('node:util')
const { unlink, writeFile, readFile } = require('node:fs/promises')
const { tmpdir } = require('node:os')
const { join } = require('node:path')
const { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher, request } = require('..')
// Test constants
const TEST_CONSTANTS = {
KEEP_ALIVE_TIMEOUT: 10,
KEEP_ALIVE_MAX_TIMEOUT: 10,
AUTO_FLUSH_INTERVAL: 100,
SEQUENTIAL_RESPONSE_DELAY: 200,
TEST_TIMESTAMP: '2024-01-01T00:00:00Z',
TEST_MESSAGE: 'Hello World',
MAX_SNAPSHOTS_FOR_LRU: 2,
TEST_ORIGINS: {
LOCALHOST_3000: 'http://localhost:3000'
},
ERROR_MESSAGES: {
INVALID_MODE: 'Invalid snapshot mode: invalid. Must be one of: record, playback, update',
MISSING_SNAPSHOT_PATH_PLAYBACK: "snapshotPath is required when mode is 'playback'",
MISSING_SNAPSHOT_PATH_UPDATE: "snapshotPath is required when mode is 'update'",
NO_SNAPSHOT_FOUND: 'No snapshot found for GET /nonexistent'
}
}
// Test helper functions
function createSnapshotPath (prefix = 'test-snapshots') {
return join(tmpdir(), `${prefix}-${Date.now()}.json`)
}
function createTestServer (handler) {
return createServer(handler)
}
async function setupServer (server) {
await promisify(server.listen.bind(server))(0)
const { port } = server.address()
const origin = `http://localhost:${port}`
return { port, origin }
}
function setupCleanup (t, resources) {
if (resources.server) {
t.after(() => {
resources.server.closeAllConnections?.()
resources.server.close()
})
}
if (resources.snapshotPath) {
t.after(() => unlink(resources.snapshotPath).catch(() => {}))
}
if (resources.agent) {
t.after(async () => await resources.agent.close())
}
if (resources.originalDispatcher) {
t.after(() => setGlobalDispatcher(resources.originalDispatcher))
}
}
function createJsonResponse (data) {
return JSON.stringify(data)
}
function createDefaultHandler () {
return (req, res) => {
if (req.url === '/test') {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(createJsonResponse({
message: TEST_CONSTANTS.TEST_MESSAGE,
timestamp: TEST_CONSTANTS.TEST_TIMESTAMP
}))
} else {
res.writeHead(404)
res.end('Not Found')
}
}
}
function createEchoHandler () {
return (req, res) => {
let body = ''
req.on('data', chunk => { body += chunk })
req.on('end', async (t) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(createJsonResponse({
received: body,
method: req.method,
headers: req.headers
}))
})
}
}
function createSequentialHandler (responses) {
let callCount = 0
return (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(responses[callCount++] || responses[responses.length - 1])
}
}
async function createLargeSnapshotFile (path, size = 1000) {
const { createRequestHash, formatRequestKey, createHeaderFilters } = require('../lib/mock/snapshot-recorder')
const snapshots = []
for (let i = 0; i < size; i++) {
const requestOpts = {
origin: 'http://localhost:3000',
path: `/api/test-${i}`,
method: 'GET'
}
const cachedSets = createHeaderFilters({})
const requestKey = formatRequestKey(requestOpts, cachedSets)
const hash = createRequestHash(requestKey)
snapshots.push({
hash,
snapshot: {
request: requestKey,
responses: [{
statusCode: 200,
headers: { 'content-type': 'application/json' },
body: Buffer.from(`{"data": "test-${i}"}`).toString('base64'),
trailers: {}
}],
callCount: 0,
timestamp: new Date().toISOString()
}
})
}
await writeFile(path, JSON.stringify(snapshots, null, 2))
}
// Organize tests with describe blocks
describe('SnapshotAgent - Basic Operations', () => {
it('record mode', async (t) => {
const server = createTestServer(createDefaultHandler())
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('record-mode')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
keepAliveTimeout: TEST_CONSTANTS.KEEP_ALIVE_TIMEOUT,
keepAliveMaxTimeout: TEST_CONSTANTS.KEEP_ALIVE_MAX_TIMEOUT,
mode: 'record',
snapshotPath
})
// Make a request that should be recorded
const response = await request(`${origin}/test`, {
dispatcher: agent
})
const body = await response.body.json()
assert.strictEqual(response.statusCode, 200, 'Response should have status 200')
assert.deepStrictEqual(body, {
message: TEST_CONSTANTS.TEST_MESSAGE,
timestamp: TEST_CONSTANTS.TEST_TIMESTAMP
}, 'Response body should match expected data')
// Save snapshots
await agent.saveSnapshots()
// Verify snapshot was recorded
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should have recorded exactly one snapshot')
const snapshots = recorder.getSnapshots()
assert.strictEqual(snapshots.length, 1, 'Snapshots array should contain one item')
assert.strictEqual(snapshots[0].request.method, 'GET', 'Recorded request method should be GET')
assert.strictEqual(snapshots[0].request.url, `${origin}/test`, 'Recorded request URL should match')
assert.strictEqual(snapshots[0].responses[0].statusCode, 200, 'Recorded response status should be 200')
})
it('playback mode', async (t) => {
const snapshotPath = createSnapshotPath('playback-mode')
setupCleanup(t, { snapshotPath })
// First, create a recording
const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
// Create a simple server for recording
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('Recorded response')
})
const { origin } = await setupServer(server)
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { server, originalDispatcher })
setGlobalDispatcher(recordingAgent)
// Record the request
await request(`${origin}/api/test`)
await recordingAgent.saveSnapshots()
// Now test playback mode
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath
})
setGlobalDispatcher(playbackAgent)
// This should use the recorded response, not make a real request
const response = await request(`${origin}/api/test`)
const body = await response.body.text()
assert.strictEqual(response.statusCode, 200, 'Playback response should have status 200')
assert.strictEqual(body, 'Recorded response', 'Playback should return recorded response')
})
it('update mode', async (t) => {
const snapshotPath = createSnapshotPath('update-mode')
setupCleanup(t, { snapshotPath })
// Create agent in update mode
const agent = new SnapshotAgent({
mode: 'update',
snapshotPath
})
// Create a simple server
const server = createTestServer((req, res) => {
if (req.url === '/existing') {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('Existing endpoint')
} else if (req.url === '/new') {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('New endpoint')
} else {
res.writeHead(404)
res.end('Not Found')
}
})
const { origin } = await setupServer(server)
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { server, originalDispatcher })
setGlobalDispatcher(agent)
// First request - should be recorded as new
const response1 = await request(`${origin}/existing`)
const body1 = await response1.body.text()
assert.strictEqual(body1, 'Existing endpoint', 'First request should get live response')
// Save and reload to simulate existing snapshots
await agent.saveSnapshots()
// Second request to same endpoint - should use existing snapshot
const response2 = await request(`${origin}/existing`)
const body2 = await response2.body.text()
assert.strictEqual(body2, 'Existing endpoint', 'Second request should use cached response')
// Request to new endpoint - should be recorded
const response3 = await request(`${origin}/new`)
const body3 = await response3.body.text()
assert.strictEqual(body3, 'New endpoint', 'New endpoint should get live response')
// Verify we have 2 different snapshots
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 2, 'Should have exactly two snapshots recorded')
})
})
describe('SnapshotAgent - Request Handling', () => {
it('handles POST requests with body', async (t) => {
const snapshotPath = createSnapshotPath('post-requests')
setupCleanup(t, { snapshotPath })
const server = createTestServer(createEchoHandler())
const { origin } = await setupServer(server)
setupCleanup(t, { server })
// Record mode
const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { originalDispatcher })
setGlobalDispatcher(recordingAgent)
const requestBody = createJsonResponse({ test: 'data' })
const response = await request(`${origin}/api/submit`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: requestBody
})
const responseBody = await response.body.json()
assert.strictEqual(responseBody.received, requestBody, 'Server should receive the request body')
assert.strictEqual(responseBody.method, 'POST', 'Server should receive POST method')
await recordingAgent.saveSnapshots()
// Playback mode
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath
})
setGlobalDispatcher(playbackAgent)
// Make the same request - should get recorded response
const playbackResponse = await request(`${origin}/api/submit`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: requestBody
})
const playbackBody = await playbackResponse.body.json()
assert.strictEqual(playbackBody.received, requestBody, 'Playback should return recorded request body')
assert.strictEqual(playbackBody.method, 'POST', 'Playback should return recorded method')
})
it('sequential response support', async (t) => {
const responses = ['First response', 'Second response', 'Third response']
const server = createTestServer(createSequentialHandler(responses))
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('sequential')
setupCleanup(t, { server, snapshotPath })
// Record multiple responses to the same endpoint
const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { originalDispatcher })
setGlobalDispatcher(recordingAgent)
// Make multiple requests to record sequential responses
{
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()
// Verify recording worked correctly before switching to playback
const recordingRecorder = recordingAgent.getRecorder()
assert.strictEqual(recordingRecorder.size(), 1, 'Should have recorded exactly one snapshot')
const recordedSnapshots = recordingRecorder.getSnapshots()
assert.strictEqual(recordedSnapshots[0].responses.length, 3, 'Should have recorded three responses')
// Close recording agent cleanly before starting playback
await recordingAgent.close()
// Switch to playback mode and test sequential responses
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath
})
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
// Ensure snapshots are loaded and call counts are reset before setting dispatcher
await playbackAgent.loadSnapshots()
// Reset call counts after loading to ensure clean state
playbackAgent.resetCallCounts()
// Verify we have the expected snapshots before proceeding
const recorder = playbackAgent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should have exactly one snapshot loaded')
const snapshots = recorder.getSnapshots()
assert.strictEqual(snapshots.length, 1, 'Should have exactly one snapshot')
assert.strictEqual(snapshots[0].responses.length, 3, 'Should have three sequential responses')
// Test sequential responses
const response1 = await request(`${origin}/api/test`)
const body1 = await response1.body.text()
assert.strictEqual(body1, 'First response', 'First call should return first response')
const response2 = await request(`${origin}/api/test`)
const body2 = await response2.body.text()
assert.strictEqual(body2, 'Second response', 'Second call should return second response')
const response3 = await request(`${origin}/api/test`)
const body3 = await response3.body.text()
assert.strictEqual(body3, 'Third response', 'Third call should return third response')
// Fourth call should repeat the last response
const response4 = await request(`${origin}/api/test`)
const body4 = await response4.body.text()
assert.strictEqual(body4, 'Third response', 'Fourth call should repeat the last response')
})
})
describe('SnapshotAgent - Error Handling', () => {
it('error handling in playback mode', async (t) => {
const snapshotPath = createSnapshotPath('error-handling')
setupCleanup(t, { snapshotPath })
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath // File doesn't exist
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// This should throw because no snapshot exists for this request
let errorThrown = false
try {
await request('http://localhost:9999/nonexistent')
} catch (error) {
errorThrown = true
assert.strictEqual(error.name, 'UndiciError', 'Error should be UndiciError')
assert(error.message.includes(TEST_CONSTANTS.ERROR_MESSAGES.NO_SNAPSHOT_FOUND),
'Error message should indicate no snapshot found')
assert.strictEqual(error.code, 'UND_ERR', 'Error code should be UND_ERR')
}
assert(errorThrown, 'Expected an error to be thrown for missing snapshot')
})
it('constructor options validation', async (t) => {
// Test invalid mode
assert.throws(() => {
return new SnapshotAgent({ mode: 'invalid' })
}, {
name: 'InvalidArgumentError',
message: new RegExp(TEST_CONSTANTS.ERROR_MESSAGES.INVALID_MODE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
}, 'Should throw for invalid mode')
// Test missing snapshotPath for playback mode
assert.throws(() => {
return new SnapshotAgent({ mode: 'playback' })
}, {
name: 'InvalidArgumentError',
message: new RegExp(TEST_CONSTANTS.ERROR_MESSAGES.MISSING_SNAPSHOT_PATH_PLAYBACK)
}, 'Should throw for missing snapshotPath in playback mode')
// Test missing snapshotPath for update mode
assert.throws(() => {
return new SnapshotAgent({ mode: 'update' })
}, {
name: 'InvalidArgumentError',
message: new RegExp(TEST_CONSTANTS.ERROR_MESSAGES.MISSING_SNAPSHOT_PATH_UPDATE)
}, 'Should throw for missing snapshotPath in update mode')
// Test valid configurations should not throw
await assert.doesNotReject(async () => {
const agent1 = new SnapshotAgent({ mode: 'record' })
await agent1.close()
}, 'Should not throw for valid record mode')
await assert.doesNotReject(async () => {
const snapshotPath = createSnapshotPath('valid-playback')
const agent2 = new SnapshotAgent({ mode: 'playback', snapshotPath })
await agent2.close()
}, 'Should not throw for valid playback mode')
await assert.doesNotReject(async () => {
const snapshotPath = createSnapshotPath('valid-update')
const agent3 = new SnapshotAgent({ mode: 'update', snapshotPath })
await agent3.close()
}, 'Should not throw for valid update mode')
})
})
describe('SnapshotAgent - Edge Cases', () => {
it('handles large snapshot files', async (t) => {
const snapshotPath = createSnapshotPath('large')
setupCleanup(t, { snapshotPath })
await createLargeSnapshotFile(snapshotPath, 100)
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
// Should load large files without issues
await agent.loadSnapshots()
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 100, 'Should load all 100 snapshots from large file')
setGlobalDispatcher(agent)
// Should be able to find and use snapshots from large file
const response = await request('http://localhost:3000/api/test-0')
const body = await response.body.json()
assert.deepStrictEqual(body, { data: 'test-0' }, 'Should return correct data from large snapshot file')
})
it('concurrent access scenarios', async (t) => {
const snapshotPath = createSnapshotPath('concurrent')
setupCleanup(t, { snapshotPath })
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(createJsonResponse({ path: req.url }))
})
const { origin } = await setupServer(server)
setupCleanup(t, { server })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make multiple concurrent requests
const promises = []
for (let i = 0; i < 10; i++) {
promises.push(request(`${origin}/api/test-${i}`))
}
const responses = await Promise.all(promises)
// Verify all responses were handled correctly
for (let i = 0; i < responses.length; i++) {
const body = await responses[i].body.json()
assert.deepStrictEqual(body, { path: `/api/test-${i}` },
`Concurrent request ${i} should return correct response`)
}
await agent.saveSnapshots()
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 10, 'Should record all 10 concurrent requests')
})
})
describe('SnapshotAgent - Advanced Features', () => {
it('snapshot file format validation', async (t) => {
const snapshotPath = createSnapshotPath('format-validation')
setupCleanup(t, { snapshotPath })
const server = createTestServer((req, res) => {
res.writeHead(200, { 'x-custom-header': 'test-value' })
res.end('Test response')
})
const { origin } = await setupServer(server)
setupCleanup(t, { server })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
await request(`${origin}/test-endpoint`)
await agent.saveSnapshots()
// Read and verify the snapshot file format
const snapshotData = JSON.parse(await readFile(snapshotPath, 'utf8'))
assert(Array.isArray(snapshotData), 'Snapshot data should be an array')
assert.strictEqual(snapshotData.length, 1, 'Should contain exactly one snapshot')
const snapshot = snapshotData[0]
assert(typeof snapshot.hash === 'string', 'Snapshot should have string hash')
assert(typeof snapshot.snapshot === 'object', 'Snapshot should have snapshot object')
const { request: req, responses, timestamp } = snapshot.snapshot
assert.strictEqual(req.method, 'GET', 'Request method should be GET')
assert.strictEqual(req.url, `${origin}/test-endpoint`, 'Request URL should match')
assert.strictEqual(responses[0].statusCode, 200, 'Response status should be 200')
// Headers should be normalized to lowercase
assert(responses[0].headers['x-custom-header'], 'Custom header should be present')
assert.strictEqual(responses[0].headers['x-custom-header'], 'test-value', 'Custom header value should match')
assert(typeof timestamp === 'string', 'Timestamp should be a string')
})
it('maxSnapshots and LRU eviction', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('lru-eviction')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
maxSnapshots: TEST_CONSTANTS.MAX_SNAPSHOTS_FOR_LRU
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make 3 requests to trigger LRU eviction
await request(`${origin}/first`)
await request(`${origin}/second`)
await request(`${origin}/third`)
const recorder = agent.getRecorder()
// Should only have 2 snapshots due to LRU eviction
assert.strictEqual(recorder.size(), TEST_CONSTANTS.MAX_SNAPSHOTS_FOR_LRU,
`Should only keep ${TEST_CONSTANTS.MAX_SNAPSHOTS_FOR_LRU} snapshots due to LRU eviction`)
const snapshots = recorder.getSnapshots()
const urls = snapshots.map(s => s.request.url)
// First snapshot should be evicted, should have second and third
assert(urls.includes(`${origin}/second`), 'Should contain second request')
assert(urls.includes(`${origin}/third`), 'Should contain third request')
assert(!urls.includes(`${origin}/first`), 'Should not contain first request (evicted)')
})
it('auto-flush functionality', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('Auto-flush test')
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('auto-flush')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
autoFlush: true,
flushInterval: TEST_CONSTANTS.AUTO_FLUSH_INTERVAL
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make a request
await request(`${origin}/autoflush-test`)
// Wait for auto-flush to trigger and ensure it completes
await new Promise(resolve => setTimeout(resolve, TEST_CONSTANTS.SEQUENTIAL_RESPONSE_DELAY))
// Force a final flush to ensure all data is written
await agent.saveSnapshots()
// Verify file was written automatically
const fileData = await readFile(snapshotPath, 'utf8')
const snapshots = JSON.parse(fileData)
assert(Array.isArray(snapshots), 'Auto-flushed data should be an array')
assert.strictEqual(snapshots.length, 1, 'Should contain exactly one auto-flushed snapshot')
assert.strictEqual(snapshots[0].snapshot.request.url, `${origin}/autoflush-test`,
'Auto-flushed snapshot should have correct URL')
})
})
describe('SnapshotAgent - Header Management', () => {
it('custom header matching with matchHeaders', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, {
'content-type': 'application/json',
'x-request-id': '12345',
authorization: 'Bearer secret-token'
})
res.end('{"message": "test"}')
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('match-headers')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
matchHeaders: ['content-type'] // Only match on content-type header
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make first request with authorization header
await request(`${origin}/test`, {
headers: {
authorization: 'Bearer secret-token',
'content-type': 'application/json'
}
})
// Save snapshots before switching to playback
await agent.saveSnapshots()
// Make second request with different authorization but same content-type
// This should match the first request due to matchHeaders config
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath,
matchHeaders: ['content-type']
})
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
const response = await request(`${origin}/test`, {
headers: {
authorization: 'Bearer different-token', // Different auth token
'content-type': 'application/json' // Same content-type
}
})
assert.strictEqual(response.statusCode, 200, 'Should match despite different auth token')
const body = await response.body.text()
assert.strictEqual(body, '{"message": "test"}', 'Should return recorded response')
})
it('ignore headers functionality', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('ignore headers test')
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('ignore-headers')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
ignoreHeaders: ['authorization', 'x-request-id'] // Ignore these for matching
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make first request
await request(`${origin}/test`, {
headers: {
authorization: 'Bearer token1',
'x-request-id': 'req-123',
'content-type': 'application/json'
}
})
// Save snapshots before switching to playback
await agent.saveSnapshots()
// Switch to playback mode and make request with different ignored headers
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath,
ignoreHeaders: ['authorization', 'x-request-id']
})
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
const response = await request(`${origin}/test`, {
headers: {
authorization: 'Bearer different-token', // Different (ignored)
'x-request-id': 'req-456', // Different (ignored)
'content-type': 'application/json' // Same (not ignored)
}
})
assert.strictEqual(response.statusCode, 200, 'Should match despite different ignored headers')
const body = await response.body.text()
assert.strictEqual(body, 'ignore headers test', 'Should return recorded response')
})
it('exclude headers for security', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, {
'content-type': 'application/json',
'set-cookie': 'session=secret123; HttpOnly',
authorization: 'Bearer server-token'
})
res.end('{"data": "sensitive"}')
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('exclude-headers')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
excludeHeaders: ['authorization', 'set-cookie'] // Don't store these sensitive headers
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
await request(`${origin}/test`)
await agent.saveSnapshots()
// Read snapshot file and verify sensitive headers are not stored
const fileData = await readFile(snapshotPath, 'utf8')
const snapshots = JSON.parse(fileData)
assert.strictEqual(snapshots.length, 1, 'Should contain exactly one snapshot')
const snapshot = snapshots[0].snapshot
// Verify excluded headers are not in stored response
assert(!snapshot.responses[0].headers.authorization, 'Authorization header should be excluded from storage')
assert(!snapshot.responses[0].headers['set-cookie'], 'Set-Cookie header should be excluded from storage')
assert(snapshot.responses[0].headers['content-type'], 'Content-Type header should be preserved')
})
})
describe('SnapshotAgent - Request Matching', () => {
it('query parameter matching control', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('query-matching')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
matchQuery: false // Ignore query parameters in matching
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Record request with query parameters
await request(`${origin}/api/data?timestamp=123&session=abc`)
// Save snapshots before switching to playback
await agent.saveSnapshots()
// Switch to playback with different query parameters
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath,
matchQuery: false
})
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
// This should match the recorded request despite different query params
const response = await request(`${origin}/api/data?timestamp=456&session=xyz`)
assert.strictEqual(response.statusCode, 200, 'Should match despite different query parameters')
const body = await response.body.text()
assert.strictEqual(body, 'Response for /api/data?timestamp=123&session=abc',
'Should return original recorded response with original query params')
})
it('normalizeQuery function for partial query matching', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify({ url: req.url }))
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('normalize-query')
setupCleanup(t, { server, snapshotPath })
// Strip the volatile '_cb' cache-buster param before matching
const normalizeQuery = (params) => {
const copy = new URLSearchParams(params)
copy.delete('_cb')
return copy.toString()
}
const agent = new SnapshotAgent({ mode: 'record', snapshotPath, normalizeQuery })
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
await request(`${origin}/api/data?filter=active&_cb=111`)
await agent.saveSnapshots()
// Playback with a different cache-buster — should still match
const playbackAgent = new SnapshotAgent({ mode: 'playback', snapshotPath, normalizeQuery })
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
const response = await request(`${origin}/api/data?filter=active&_cb=999`)
assert.strictEqual(response.statusCode, 200, 'Should match snapshot despite different cache-buster')
// Different filter value — should NOT match
await assert.rejects(
() => request(`${origin}/api/data?filter=inactive&_cb=111`),
/No snapshot found/,
'Should not match snapshot with different filter value'
)
})
it('body matching control', async (t) => {
const server = createTestServer((req, res) => {
let body = ''
req.on('data', chunk => { body += chunk })
req.on('end', async (t) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(`{"received": "${body}"}`)
})
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('body-matching')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
matchBody: false // Ignore request body in matching
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Record request with specific body
await request(`${origin}/api/submit`, {
method: 'POST',
body: 'original-data',
headers: { 'content-type': 'text/plain' }
})
// Save snapshots before switching to playback
await agent.saveSnapshots()
// Switch to playback with different body
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath,
matchBody: false
})
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
// This should match despite different body content
const response = await request(`${origin}/api/submit`, {
method: 'POST',
body: 'different-data',
headers: { 'content-type': 'text/plain' }
})
assert.strictEqual(response.statusCode, 200, 'Should match despite different request body')
const responseBody = await response.body.json()
assert.strictEqual(responseBody.received, 'original-data',
'Should return recorded response with original body')
})
it('normalizeBody function for partial body matching', async (t) => {
const server = createTestServer((req, res) => {
let body = ''
req.on('data', chunk => { body += chunk })
req.on('end', () => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify({ received: body }))
})
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('body-matching-fn')
setupCleanup(t, { server, snapshotPath })
// Normalize body by stripping the timestamp before hashing
const normalizeBody = (body) => {
if (!body) return ''
const parsed = JSON.parse(String(body))
delete parsed.timestamp
return JSON.stringify(parsed)
}
const agent = new SnapshotAgent({ mode: 'record', snapshotPath, normalizeBody })
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
await request(`${origin}/api/submit`, {
method: 'POST',
body: JSON.stringify({ action: 'create', timestamp: '2024-01-01T00:00:00Z' }),
headers: { 'content-type': 'application/json' }
})
await agent.saveSnapshots()
// Playback with a different timestamp — should still match
const playbackAgent = new SnapshotAgent({ mode: 'playback', snapshotPath, normalizeBody })
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
const response = await request(`${origin}/api/submit`, {
method: 'POST',
body: JSON.stringify({ action: 'create', timestamp: '2025-06-15T12:00:00Z' }),
headers: { 'content-type': 'application/json' }
})
assert.strictEqual(response.statusCode, 200, 'Should match snapshot despite different timestamp')
// Different action — should NOT match
await assert.rejects(
() => request(`${origin}/api/submit`, {
method: 'POST',
body: JSON.stringify({ action: 'delete', timestamp: '2024-01-01T00:00:00Z' }),
headers: { 'content-type': 'application/json' }
}),
/No snapshot found/,
'Should not match snapshot with different action'
)
})
})
describe('SnapshotAgent - Management Features', () => {
it('call count reset functionality', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('Test response')
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('reset-functionality')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Record a snapshot
await request(`${origin}/api/test`)
await agent.saveSnapshots()
// Check initial call count
const info1 = agent.getSnapshotInfo({
origin,
path: '/api/test',
method: 'GET'
})
assert(info1, 'Should find snapshot info')
assert.strictEqual(info1.callCount, 0, 'Call count should be 0 initially (only incremented during findSnapshot)')
// Reset call counts
agent.resetCallCounts()
const info2 = agent.getSnapshotInfo({
origin,
path: '/api/test',
method: 'GET'
})
assert(info2, 'Should still find snapshot info after reset')
assert.strictEqual(info2.callCount, 0, 'Call count should remain 0 after reset')
})
it('snapshot management methods', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('management-methods')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Record multiple snapshots
await request(`${origin}/api/users`)
await request(`${origin}/api/posts`)
// Test getSnapshotInfo
const userInfo = agent.getSnapshotInfo({
origin,
path: '/api/users',
method: 'GET'
})
assert(userInfo, 'Should find user snapshot info')
assert.strictEqual(userInfo.request.method, 'GET', 'User snapshot method should be GET')
assert.strictEqual(userInfo.request.url, `${origin}/api/users`, 'User snapshot URL should match')
assert.strictEqual(userInfo.responseCount, 1, 'User snapshot should have one response')
// Test deleteSnapshot
const deleted = agent.deleteSnapshot({
origin,
path: '/api/users',
method: 'GET'
})
assert.strictEqual(deleted, true, 'Should successfully delete user snapshot')
// Verify deletion
const deletedInfo = agent.getSnapshotInfo({
origin,
path: '/api/users',
method: 'GET'
})
assert.strictEqual(deletedInfo, null, 'Deleted snapshot should not be found')
// Post snapshot should still exist
const postInfo = agent.getSnapshotInfo({
origin,
path: '/api/posts',
method: 'GET'
})
assert(postInfo, 'Post snapshot should still exist after deleting user snapshot')
// Test replaceSnapshots - create a snapshot with proper hash
const { createRequestHash, formatRequestKey, createHeaderFilters } = require('../lib/mock/snapshot-recorder')
const mockRequestOpts = {
origin,
path: '/api/mock',
method: 'GET'
}
const cachedSets = createHeaderFilters({})
const mockRequest = formatRequestKey(mockRequestOpts, cachedSets)
const mockHash = createRequestHash(mockRequest)
const mockData = [
{
hash: mockHash,
snapshot: {
request: mockRequest,
responses: [{ statusCode: 200, headers: {}, body: 'bW9jaw==', trailers: {} }],
callCount: 0,
timestamp: new Date().toISOString()
}
}
]
agent.replaceSnapshots(mockData)
// Should only have the mock snapshot now
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should have only one snapshot after replacement')
const mockInfo = agent.getSnapshotInfo(mockRequestOpts)
assert(mockInfo, 'Should find mock snapshot after replacement')
assert.strictEqual(mockInfo.request.url, `${origin}/api/mock`, 'Mock snapshot URL should match')
})
})
describe('SnapshotAgent - Filtering', () => {
it('shouldRecord filtering', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('should-record-filter')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
shouldRecord: (requestOpts) => {
// Only record requests to /api/allowed
return requestOpts.path === '/api/allowed'
}
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make requests - only one should be recorded
await request(`${origin}/api/allowed`)
await request(`${origin}/api/filtered`)
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should record only the allowed request')
const snapshots = recorder.getSnapshots()
assert.strictEqual(snapshots[0].request.url, `${origin}/api/allowed`,
'Recorded snapshot should be the allowed request')
})
it('shouldPlayback filtering', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Live response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('should-playback-filter')
setupCleanup(t, { server, snapshotPath })
// First, record some snapshots without filtering
const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent: recordingAgent, originalDispatcher })
setGlobalDispatcher(recordingAgent)
await request(`${origin}/api/cached`)
await request(`${origin}/api/live`)
await recordingAgent.saveSnapshots()
// Now test playback with filtering
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath,
shouldPlayback: (requestOpts) => {
// Only playback requests to /api/cached
return requestOpts.path === '/api/cached'
}
})
setupCleanup(t, { agent: playbackAgent })
setGlobalDispatcher(playbackAgent)
// This should use cached response
const cachedResponse = await request(`${origin}/api/cached`)
const cachedBody = await cachedResponse.body.text()
assert.strictEqual(cachedBody, 'Live response for /api/cached',
'Should return cached response for allowed path')
// This should fail because playback is filtered and no live server
// Need to close the recording server to ensure no fallback
server.close()
let errorThrown = false
try {
await request(`${origin}/api/live`)
} catch (error) {
errorThrown = true
assert.strictEqual(error.name, 'UndiciError', 'Should throw UndiciError for filtered request')
}
assert(errorThrown, 'Expected an error for filtered playback request')
})
it('URL exclusion patterns (string)', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('url-exclusion-string')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
excludeUrls: ['/private', 'secret']
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make requests - some should be excluded
await request(`${origin}/api/public`)
await request(`${origin}/private/data`)
await request(`${origin}/api/secret-endpoint`)
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should record only non-excluded requests')
const snapshots = recorder.getSnapshots()
assert.strictEqual(snapshots[0].request.url, `${origin}/api/public`,
'Should record only the public API request')
})
it('URL exclusion patterns (regex)', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('url-exclusion-regex')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
excludeUrls: [/\/admin\/.*/, /.*\?token=.*/]
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make requests - some should be excluded by regex patterns
await request(`${origin}/api/data`)
await request(`${origin}/admin/users`)
await request(`${origin}/api/auth?token=secret`)
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should record only requests not matching exclusion patterns')
const snapshots = recorder.getSnapshots()
assert.strictEqual(snapshots[0].request.url, `${origin}/api/data`,
'Should record only the non-excluded API request')
})
it('complex filtering scenarios', async (t) => {
const server = createTestServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end(`Response for ${req.url}`)
})
const { origin } = await setupServer(server)
const snapshotPath = createSnapshotPath('complex-filtering')
setupCleanup(t, { server, snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
shouldRecord: (requestOpts) => {
// Only record GET requests
return (requestOpts.method || 'GET') === 'GET'
},
excludeUrls: ['/health']
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { agent, originalDispatcher })
setGlobalDispatcher(agent)
// Make various requests with multiple filters
await request(`${origin}/api/users`) // Should record (GET, not excluded)
await request(`${origin}/health`) // Should not record (excluded URL)
await request(`${origin}/api/data`, { method: 'POST' }) // Should not record (POST method)
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should record only requests passing all filters')
const snapshots = recorder.getSnapshots()
assert.strictEqual(snapshots[0].request.url, `${origin}/api/users`,
'Should record only the allowed GET request')
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', () => {
it('close() saves recordings before cleanup', async (t) => {
const snapshotPath = createSnapshotPath('close-saves')
setupCleanup(t, { snapshotPath })
const server = createTestServer(createDefaultHandler())
const { origin } = await setupServer(server)
setupCleanup(t, { server })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath,
autoFlush: false // Disable auto-flush to test manual save on close
})
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { originalDispatcher })
setGlobalDispatcher(agent)
// Make a request that should be recorded
await request(`${origin}/test`)
// Verify snapshot is in memory but not yet saved to file
const recorder = agent.getRecorder()
assert.strictEqual(recorder.size(), 1, 'Should have recorded one snapshot in memory')
// Check that file doesn't exist yet (since autoFlush is false)
let fileExists = false
try {
await readFile(snapshotPath)
fileExists = true
} catch {
// File doesn't exist, which is expected
}
assert.strictEqual(fileExists, false, 'File should not exist before close()')
// Close the agent - this should save the snapshots
await agent.close()
// Verify the snapshots were saved to file
let savedData
try {
const fileContent = await readFile(snapshotPath, 'utf8')
savedData = JSON.parse(fileContent)
} catch (error) {
assert.fail(`Failed to read saved snapshot file: ${error.message}`)
}
assert(Array.isArray(savedData), 'Saved data should be an array')
assert.strictEqual(savedData.length, 1, 'Should have saved one snapshot')
assert.strictEqual(savedData[0].snapshot.request.method, 'GET', 'Saved snapshot should have correct method')
assert.strictEqual(savedData[0].snapshot.request.url, `${origin}/test`, 'Saved snapshot should have correct URL')
})
it('close() works when no recordings exist', async (t) => {
const snapshotPath = createSnapshotPath('close-no-recordings')
setupCleanup(t, { snapshotPath })
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath
})
// Close agent immediately without making any requests or setting as dispatcher
await assert.doesNotReject(async () => {
await agent.close()
}, 'Should not throw when closing agent with no recordings')
// Verify no file was created
let fileExists = false
try {
await readFile(snapshotPath)
fileExists = true
} catch {
// File doesn't exist, which is expected
}
assert.strictEqual(fileExists, false, 'No file should be created when no recordings exist')
})
it('close() works when no snapshot path is configured', async (t) => {
const agent = new SnapshotAgent({
mode: 'record'
// No snapshotPath provided
})
const server = createTestServer(createDefaultHandler())
const { origin } = await setupServer(server)
setupCleanup(t, { server })
const originalDispatcher = getGlobalDispatcher()
t.after(() => setGlobalDispatcher(originalDispatcher))
setGlobalDispatcher(agent)
// Make a request
await request(`${origin}/test`)
// Close should not throw even without snapshot path
await assert.doesNotReject(async () => {
await agent.close()
}, 'Should not throw when closing agent without snapshot path')
})
it('close() does not rewrite the snapshot file in playback mode', async (t) => {
const snapshotPath = createSnapshotPath('close-playback-no-rewrite')
setupCleanup(t, { snapshotPath })
const server = createTestServer(createDefaultHandler())
const { origin } = await setupServer(server)
setupCleanup(t, { server })
const originalDispatcher = getGlobalDispatcher()
setupCleanup(t, { originalDispatcher })
// Record a snapshot so there is a file on disk to play back.
const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath,
autoFlush: false
})
setGlobalDispatcher(recordingAgent)
await request(`${origin}/test`)
await recordingAgent.close()
// Capture the exact bytes written by the recording session.
const recordedContent = await readFile(snapshotPath, 'utf8')
// Play the snapshot back. Each matched request increments the snapshot's
// callCount in memory, so a save on close would change the file on disk.
const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath,
autoFlush: false
})
setGlobalDispatcher(playbackAgent)
await request(`${origin}/test`)
await request(`${origin}/test`)
await request(`${origin}/test`)
await playbackAgent.close()
// The file must be byte-for-byte identical: playback is a read-only path
// and must never rewrite the fixture (and thus never churn its callCount).
const contentAfterPlayback = await readFile(snapshotPath, 'utf8')
assert.strictEqual(
contentAfterPlayback,
recordedContent,
'Playback close() should not modify the snapshot file on disk'
)
})
it('recorder close() method works independently', async (t) => {
const { SnapshotRecorder } = require('../lib/mock/snapshot-recorder')
const snapshotPath = createSnapshotPath('recorder-close')
setupCleanup(t, { snapshotPath })
const recorder = new SnapshotRecorder({
snapshotPath,
mode: 'record'
})
// Manually add a snapshot to test saving
await recorder.record(
{ origin: 'http://test.com', path: '/api', method: 'GET' },
{
statusCode: 200,
headers: { 'content-type': 'application/json' },
body: Buffer.from('{"test": true}'),
trailers: {}
}
)
assert.strictEqual(recorder.size(), 1, 'Should have one recorded snapshot')
// Close the recorder
await recorder.close()
// Verify the snapshot was saved
let savedData
try {
const fileContent = await readFile(snapshotPath, 'utf8')
savedData = JSON.parse(fileContent)
} catch (error) {
assert.fail(`Failed to read saved snapshot file: ${error.message}`)
}
assert(Array.isArray(savedData), 'Saved data should be an array')
assert.strictEqual(savedData.length, 1, 'Should have saved one snapshot')
assert.strictEqual(savedData[0].snapshot.request.method, 'GET', 'Should have correct method')
})
})