From a778077d704a96e40ee912b5eb9ec42cbc2cd4af Mon Sep 17 00:00:00 2001 From: James Date: Wed, 31 Jan 2024 15:56:06 -0700 Subject: [PATCH] feat: adds a onMissedRequest hook for seeing missed mocks --- docs/api/MockAgent.md | 8 ++++++++ lib/mock/mock-agent.js | 8 +++++++- lib/mock/mock-symbols.js | 3 ++- lib/mock/mock-utils.js | 7 ++++++- test/mock-agent.js | 39 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/docs/api/MockAgent.md b/docs/api/MockAgent.md index 85ae69046e7..71677783f44 100644 --- a/docs/api/MockAgent.md +++ b/docs/api/MockAgent.md @@ -38,6 +38,14 @@ const agent = new Agent() const mockAgent = new MockAgent({ agent }) ``` +### Example - MockAgent instantiation with missed request callback + +```js +import { MockAgent } from 'undici' + +const mockAgent = new MockAgent({ onMissedRequest: (error) => console.log(`A request wasn't handled: ${error.message}`) }) +``` + ## Instance Methods ### `MockAgent.get(origin)` diff --git a/lib/mock/mock-agent.js b/lib/mock/mock-agent.js index 3d26a6a65ba..06a9d09d7d3 100644 --- a/lib/mock/mock-agent.js +++ b/lib/mock/mock-agent.js @@ -11,7 +11,8 @@ const { kNetConnect, kGetNetConnect, kOptions, - kFactory + kFactory, + kOnMissedRequest } = require('./mock-symbols') const MockClient = require('./mock-client') const MockPool = require('./mock-pool') @@ -35,6 +36,11 @@ class MockAgent extends Dispatcher { const agent = opts?.agent ? opts.agent : new Agent(opts) this[kAgent] = agent + if (opts?.onMissedRequest && typeof opts.onMissedRequest !== 'function') { + throw new InvalidArgumentError('Argument opts.onMissedRequest must be a callback') + } + + this[kOnMissedRequest] = opts?.onMissedRequest this[kClients] = agent[kClients] this[kOptions] = buildMockOptions(opts) } diff --git a/lib/mock/mock-symbols.js b/lib/mock/mock-symbols.js index 8c4cbb60e16..ae872e25440 100644 --- a/lib/mock/mock-symbols.js +++ b/lib/mock/mock-symbols.js @@ -19,5 +19,6 @@ module.exports = { kIsMockActive: Symbol('is mock active'), kNetConnect: Symbol('net connect'), kGetNetConnect: Symbol('get net connect'), - kConnected: Symbol('connected') + kConnected: Symbol('connected'), + kOnMissedRequest: Symbol('onMissedRequest') } diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index 96282f0ec9d..af05771db0c 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -6,7 +6,8 @@ const { kMockAgent, kOriginalDispatch, kOrigin, - kGetNetConnect + kGetNetConnect, + kOnMissedRequest } = require('./mock-symbols') const { buildURL, nop } = require('../core/util') const { STATUS_CODES } = require('node:http') @@ -307,6 +308,10 @@ function buildMockDispatch () { mockDispatch.call(this, opts, handler) } catch (error) { if (error instanceof MockNotMatchedError) { + if (agent[kOnMissedRequest]) { + agent[kOnMissedRequest](error) + } + const netConnect = agent[kGetNetConnect]() if (netConnect === false) { throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`) diff --git a/test/mock-agent.js b/test/mock-agent.js index 42dfb9554cc..e369bc9ee6a 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -3,6 +3,7 @@ const { test } = require('tap') const { createServer } = require('node:http') const { promisify } = require('node:util') +const { stub } = require('sinon') const { request, setGlobalDispatcher, MockAgent, Agent } = require('..') const { getResponse } = require('../lib/mock/mock-utils') const { kClients, kConnected } = require('../lib/core/symbols') @@ -385,6 +386,44 @@ test('MockAgent - basic intercept with request', async (t) => { }) }) +test('MockAgent - basic missed request', async (t) => { + t.plan(5) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('hello from the other side') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + const onMissedRequest = stub() + const mockAgent = new MockAgent({ onMissedRequest }) + + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + const mockPool = mockAgent.get(baseUrl) + + mockPool.intercept({ + path: '/justatest?hello=there&see=ya', + method: 'GET' + }).reply(200, { wont: 'match' }, { + headers: { 'content-type': 'application/json' } + }) + + const { statusCode, headers, body } = await request(`${baseUrl}/justatest?hello=there&another=one`, { + method: 'GET' + }) + + t.equal(statusCode, 200, 'Status code is 200') + t.equal(headers['content-type'], 'text/plain', 'Content type is text/plain') + const responseBody = await getResponse(body) + t.equal(responseBody, 'hello from the other side', 'Body is from the local server') + t.equal(onMissedRequest.calledOnce, true, 'onMissedRequest calledOnce') + t.ok(onMissedRequest.firstCall.args[0].message.includes('Mock dispatch not matched for path \'/justatest?another=one&hello=there\''), 'Error message matches') +}) + test('MockAgent - should support local agents', async (t) => { t.plan(4)