diff --git a/lib/mock/mock-symbols.js b/lib/mock/mock-symbols.js index 940dbe6e3f8..9b23e8e3cf0 100644 --- a/lib/mock/mock-symbols.js +++ b/lib/mock/mock-symbols.js @@ -27,5 +27,6 @@ module.exports = { kMockAgentAddCallHistoryLog: Symbol('mock agent add call history log'), kMockAgentIsCallHistoryEnabled: Symbol('mock agent is call history enabled'), kMockAgentAcceptsNonStandardSearchParameters: Symbol('mock agent accepts non standard search parameters'), - kMockCallHistoryAddLog: Symbol('mock call history add log') + kMockCallHistoryAddLog: Symbol('mock call history add log'), + kTotalDispatchCount: Symbol('total dispatch count') } diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index 291a85753be..a51816f4082 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -6,7 +6,8 @@ const { kMockAgent, kOriginalDispatch, kOrigin, - kGetNetConnect + kGetNetConnect, + kTotalDispatchCount } = require('./mock-symbols') const { serializePathWithQuery } = require('../core/util') const { STATUS_CODES } = require('node:http') @@ -206,6 +207,8 @@ function addMockDispatch (mockDispatches, key, data, opts) { const replyData = typeof data === 'function' ? { callback: data } : { ...data } const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } } mockDispatches.push(newMockDispatch) + // Track total number of intercepts ever registered for better error messages + mockDispatches[kTotalDispatchCount] = (mockDispatches[kTotalDispatchCount] || 0) + 1 return newMockDispatch } @@ -401,13 +404,16 @@ function buildMockDispatch () { } catch (error) { if (error.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') { const netConnect = agent[kGetNetConnect]() + const totalInterceptsCount = this[kDispatches][kTotalDispatchCount] || this[kDispatches].length + const pendingInterceptsCount = this[kDispatches].filter(({ consumed }) => !consumed).length + const interceptsMessage = `, ${pendingInterceptsCount} interceptor(s) remaining out of ${totalInterceptsCount} defined` if (netConnect === false) { - throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`) + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)${interceptsMessage}`) } if (checkNetConnect(netConnect, origin)) { originalDispatch.call(this, opts, handler) } else { - throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`) + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)${interceptsMessage}`) } } else { throw error diff --git a/test/mock-agent.js b/test/mock-agent.js index bb3d2afb9f7..dbc8ad1836d 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -2464,7 +2464,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for path await t.assert.rejects(request(`${baseUrl}/wrong`, { method: 'GET' - }), new MockNotMatchedError(`Mock dispatch not matched for path '/wrong': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for path '/wrong': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin), 1 interceptor(s) remaining out of 1 defined`)) }) test('MockAgent - enableNetConnect should throw if dispatch not matched for method and the origin was not allowed by net connect', async (t) => { @@ -2497,7 +2497,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for meth await t.assert.rejects(request(`${baseUrl}/foo`, { method: 'WRONG' - }), new MockNotMatchedError(`Mock dispatch not matched for method 'WRONG' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for method 'WRONG' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin), 1 interceptor(s) remaining out of 1 defined`)) }) test('MockAgent - enableNetConnect should throw if dispatch not matched for body and the origin was not allowed by net connect', async (t) => { @@ -2532,7 +2532,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for body await t.assert.rejects(request(`${baseUrl}/foo`, { method: 'GET', body: 'wrong' - }), new MockNotMatchedError(`Mock dispatch not matched for body 'wrong' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for body 'wrong' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin), 1 interceptor(s) remaining out of 1 defined`)) }) test('MockAgent - enableNetConnect should throw if dispatch not matched for headers and the origin was not allowed by net connect', async (t) => { @@ -2571,7 +2571,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for head headers: { 'User-Agent': 'wrong' } - }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"User-Agent":"wrong"}' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"User-Agent":"wrong"}' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin), 1 interceptor(s) remaining out of 1 defined`)) }) test('MockAgent - disableNetConnect should throw if dispatch not found by net connect', async (t) => { @@ -2606,12 +2606,10 @@ test('MockAgent - disableNetConnect should throw if dispatch not found by net co await t.assert.rejects(request(`${baseUrl}/foo`, { method: 'GET' - }), new MockNotMatchedError(`Mock dispatch not matched for path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled), 1 interceptor(s) remaining out of 1 defined`)) }) test('MockAgent - headers function interceptor', async (t) => { - t.plan(8) - const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { t.assert.fail('should not be called') res.end('should not be called') @@ -2647,12 +2645,12 @@ test('MockAgent - headers function interceptor', async (t) => { headers: { Authorization: 'Bearer foo' } - }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"Authorization":"Bearer foo"}' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"Authorization":"Bearer foo"}' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled), 1 interceptor(s) remaining out of 1 defined`)) await t.assert.rejects(request(`${baseUrl}/foo`, { method: 'GET', headers: ['Authorization', 'Bearer foo'] - }), new MockNotMatchedError(`Mock dispatch not matched for headers '["Authorization","Bearer foo"]' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for headers '["Authorization","Bearer foo"]' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled), 1 interceptor(s) remaining out of 1 defined`)) { const { statusCode } = await request(`${baseUrl}/foo`, { @@ -2672,6 +2670,49 @@ test('MockAgent - headers function interceptor', async (t) => { } }) +test('MockAgent - should include intercept count in error when intercepts are exhausted', async (t) => { + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + t.assert.fail('should not be called') + res.end('should not be called') + }) + after(() => { + server.closeAllConnections?.() + server.close() + }) + + await once(server.listen(0), 'listening') + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + after(() => mockAgent.close()) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'POST' + }).reply(200, 'foo') + + mockAgent.disableNetConnect() + + // First request consumes the only intercept + const { statusCode } = await request(`${baseUrl}/foo`, { + method: 'POST' + }) + t.assert.strictEqual(statusCode, 200) + + // Second request should fail with a message indicating intercept counts + try { + await request(`${baseUrl}/foo`, { + method: 'POST' + }) + t.assert.fail('should have thrown') + } catch (err) { + t.assert.ok(err.message.includes('0 interceptor(s) remaining out of 1 defined'), `Error message should include interceptor counts, got: ${err.message}`) + } +}) + test('MockAgent - clients are not garbage collected', async (t) => { const samples = 250 t.plan(2)