diff options
Diffstat (limited to 'test/mock-agent.js')
-rw-r--r-- | test/mock-agent.js | 2637 |
1 files changed, 2637 insertions, 0 deletions
diff --git a/test/mock-agent.js b/test/mock-agent.js new file mode 100644 index 0000000..c9ffda4 --- /dev/null +++ b/test/mock-agent.js @@ -0,0 +1,2637 @@ +'use strict' + +const { test } = require('tap') +const { createServer } = require('http') +const { promisify } = require('util') +const { request, setGlobalDispatcher, MockAgent, Agent } = require('..') +const { getResponse } = require('../lib/mock/mock-utils') +const { kClients, kConnected } = require('../lib/core/symbols') +const { InvalidArgumentError, ClientDestroyedError } = require('../lib/core/errors') +const { nodeMajor } = require('../lib/core/util') +const MockClient = require('../lib/mock/mock-client') +const MockPool = require('../lib/mock/mock-pool') +const { kAgent } = require('../lib/mock/mock-symbols') +const Dispatcher = require('../lib/dispatcher') +const { MockNotMatchedError } = require('../lib/mock/mock-errors') + +test('MockAgent - constructor', t => { + t.plan(5) + + t.test('sets up mock agent', t => { + t.plan(1) + t.doesNotThrow(() => new MockAgent()) + }) + + t.test('should implement the Dispatcher API', t => { + t.plan(1) + + const mockAgent = new MockAgent() + t.type(mockAgent, Dispatcher) + }) + + t.test('sets up mock agent with single connection', t => { + t.plan(1) + t.doesNotThrow(() => new MockAgent({ connections: 1 })) + }) + + t.test('should error passed agent is not valid', t => { + t.plan(2) + t.throws(() => new MockAgent({ agent: {} }), new InvalidArgumentError('Argument opts.agent must implement Agent')) + t.throws(() => new MockAgent({ agent: { dispatch: '' } }), new InvalidArgumentError('Argument opts.agent must implement Agent')) + }) + + t.test('should be able to specify the agent to mock', t => { + t.plan(1) + const agent = new Agent() + t.teardown(agent.close.bind(agent)) + const mockAgent = new MockAgent({ agent }) + + t.equal(mockAgent[kAgent], agent) + }) +}) + +test('MockAgent - get', t => { + t.plan(3) + + t.test('should return MockClient', (t) => { + t.plan(1) + + const baseUrl = 'http://localhost:9999' + + const mockAgent = new MockAgent({ connections: 1 }) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockClient = mockAgent.get(baseUrl) + t.type(mockClient, MockClient) + }) + + t.test('should return MockPool', (t) => { + t.plan(1) + + const baseUrl = 'http://localhost:9999' + + const mockAgent = new MockAgent() + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + t.type(mockPool, MockPool) + }) + + t.test('should return the same instance if already created', (t) => { + t.plan(1) + + const baseUrl = 'http://localhost:9999' + + const mockAgent = new MockAgent() + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool1 = mockAgent.get(baseUrl) + const mockPool2 = mockAgent.get(baseUrl) + t.equal(mockPool1, mockPool2) + }) +}) + +test('MockAgent - dispatch', t => { + t.plan(3) + + t.test('should call the dispatch method of the MockPool', (t) => { + t.plan(1) + + const baseUrl = 'http://localhost:9999' + + const mockAgent = new MockAgent() + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'hello') + + t.doesNotThrow(() => mockAgent.dispatch({ + origin: baseUrl, + path: '/foo', + method: 'GET' + }, { + onHeaders: (_statusCode, _headers, resume) => resume(), + onData: () => {}, + onComplete: () => {}, + onError: () => {} + })) + }) + + t.test('should call the dispatch method of the MockClient', (t) => { + t.plan(1) + + const baseUrl = 'http://localhost:9999' + + const mockAgent = new MockAgent({ connections: 1 }) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockClient = mockAgent.get(baseUrl) + + mockClient.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'hello') + + t.doesNotThrow(() => mockAgent.dispatch({ + origin: baseUrl, + path: '/foo', + method: 'GET' + }, { + onHeaders: (_statusCode, _headers, resume) => resume(), + onData: () => {}, + onComplete: () => {}, + onError: () => {} + })) + }) + + t.test('should throw if handler is not valid on redirect', (t) => { + t.plan(7) + + const baseUrl = 'http://localhost:9999' + + const mockAgent = new MockAgent() + t.teardown(mockAgent.close.bind(mockAgent)) + + t.throws(() => mockAgent.dispatch({ + origin: baseUrl, + path: '/foo', + method: 'GET' + }, { + onError: 'INVALID' + }), new InvalidArgumentError('invalid onError method')) + + t.throws(() => mockAgent.dispatch({ + origin: baseUrl, + path: '/foo', + method: 'GET' + }, { + onError: (err) => { throw err }, + onConnect: 'INVALID' + }), new InvalidArgumentError('invalid onConnect method')) + + t.throws(() => mockAgent.dispatch({ + origin: baseUrl, + path: '/foo', + method: 'GET' + }, { + onError: (err) => { throw err }, + onConnect: () => {}, + onBodySent: 'INVALID' + }), new InvalidArgumentError('invalid onBodySent method')) + + t.throws(() => mockAgent.dispatch({ + origin: baseUrl, + path: '/foo', + method: 'CONNECT' + }, { + onError: (err) => { throw err }, + onConnect: () => {}, + onBodySent: () => {}, + onUpgrade: 'INVALID' + }), new InvalidArgumentError('invalid onUpgrade method')) + + t.throws(() => mockAgent.dispatch({ + origin: baseUrl, + path: '/foo', + method: 'GET' + }, { + onError: (err) => { throw err }, + onConnect: () => {}, + onBodySent: () => {}, + onHeaders: 'INVALID' + }), new InvalidArgumentError('invalid onHeaders method')) + + t.throws(() => mockAgent.dispatch({ + origin: baseUrl, + path: '/foo', + method: 'GET' + }, { + onError: (err) => { throw err }, + onConnect: () => {}, + onBodySent: () => {}, + onHeaders: () => {}, + onData: 'INVALID' + }), new InvalidArgumentError('invalid onData method')) + + t.throws(() => mockAgent.dispatch({ + origin: baseUrl, + path: '/foo', + method: 'GET' + }, { + onError: (err) => { throw err }, + onConnect: () => {}, + onBodySent: () => {}, + onHeaders: () => {}, + onData: () => {}, + onComplete: 'INVALID' + }), new InvalidArgumentError('invalid onComplete method')) + }) +}) + +test('MockAgent - .close should clean up registered pools', async (t) => { + t.plan(5) + + const baseUrl = 'http://localhost:9999' + + const mockAgent = new MockAgent() + + // Register a pool + const mockPool = mockAgent.get(baseUrl) + t.type(mockPool, MockPool) + + t.equal(mockPool[kConnected], 1) + t.equal(mockAgent[kClients].size, 1) + await mockAgent.close() + t.equal(mockPool[kConnected], 0) + t.equal(mockAgent[kClients].size, 0) +}) + +test('MockAgent - .close should clean up registered clients', async (t) => { + t.plan(5) + + const baseUrl = 'http://localhost:9999' + + const mockAgent = new MockAgent({ connections: 1 }) + + // Register a pool + const mockClient = mockAgent.get(baseUrl) + t.type(mockClient, MockClient) + + t.equal(mockClient[kConnected], 1) + t.equal(mockAgent[kClients].size, 1) + await mockAgent.close() + t.equal(mockClient[kConnected], 0) + t.equal(mockAgent[kClients].size, 0) +}) + +test('MockAgent - [kClients] should match encapsulated agent', async (t) => { + t.plan(1) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const agent = new Agent() + t.teardown(agent.close.bind(agent)) + + const mockAgent = new MockAgent({ agent }) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'hello') + + // The MockAgent should encapsulate the input agent clients + t.equal(mockAgent[kClients].size, agent[kClients].size) +}) + +test('MockAgent - basic intercept with MockAgent.request', async (t) => { + t.plan(4) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + t.teardown(mockAgent.close.bind(mockAgent)) + const mockPool = mockAgent.get(baseUrl) + + mockPool.intercept({ + path: '/foo?hello=there&see=ya', + method: 'POST', + body: 'form1=data1&form2=data2' + }).reply(200, { foo: 'bar' }, { + headers: { 'content-type': 'application/json' }, + trailers: { 'Content-MD5': 'test' } + }) + + const { statusCode, headers, trailers, body } = await mockAgent.request({ + origin: baseUrl, + path: '/foo?hello=there&see=ya', + method: 'POST', + body: 'form1=data1&form2=data2' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'application/json') + t.same(trailers, { 'content-md5': 'test' }) + + const jsonResponse = JSON.parse(await getResponse(body)) + t.same(jsonResponse, { + foo: 'bar' + }) +}) + +test('MockAgent - basic intercept with request', async (t) => { + t.plan(4) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + const mockPool = mockAgent.get(baseUrl) + + mockPool.intercept({ + path: '/foo?hello=there&see=ya', + method: 'POST', + body: 'form1=data1&form2=data2' + }).reply(200, { foo: 'bar' }, { + headers: { 'content-type': 'application/json' }, + trailers: { 'Content-MD5': 'test' } + }) + + const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, { + method: 'POST', + body: 'form1=data1&form2=data2' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'application/json') + t.same(trailers, { 'content-md5': 'test' }) + + const jsonResponse = JSON.parse(await getResponse(body)) + t.same(jsonResponse, { + foo: 'bar' + }) +}) + +test('MockAgent - should support local agents', async (t) => { + t.plan(4) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + + t.teardown(mockAgent.close.bind(mockAgent)) + const mockPool = mockAgent.get(baseUrl) + + mockPool.intercept({ + path: '/foo?hello=there&see=ya', + method: 'POST', + body: 'form1=data1&form2=data2' + }).reply(200, { foo: 'bar' }, { + headers: { + 'content-type': 'application/json' + }, + trailers: { 'Content-MD5': 'test' } + }) + + const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, { + method: 'POST', + body: 'form1=data1&form2=data2', + dispatcher: mockAgent + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'application/json') + t.same(trailers, { 'content-md5': 'test' }) + + const jsonResponse = JSON.parse(await getResponse(body)) + t.same(jsonResponse, { + foo: 'bar' + }) +}) + +test('MockAgent - should support specifying custom agents to mock', async (t) => { + t.plan(4) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const agent = new Agent() + t.teardown(agent.close.bind(agent)) + + const mockAgent = new MockAgent({ agent }) + setGlobalDispatcher(mockAgent) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo?hello=there&see=ya', + method: 'POST', + body: 'form1=data1&form2=data2' + }).reply(200, { foo: 'bar' }, { + headers: { + 'content-type': 'application/json' + }, + trailers: { 'Content-MD5': 'test' } + }) + + const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, { + method: 'POST', + body: 'form1=data1&form2=data2' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'application/json') + t.same(trailers, { 'content-md5': 'test' }) + + const jsonResponse = JSON.parse(await getResponse(body)) + t.same(jsonResponse, { + foo: 'bar' + }) +}) + +test('MockAgent - basic Client intercept with request', async (t) => { + t.plan(4) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent({ connections: 1 }) + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockClient = mockAgent.get(baseUrl) + mockClient.intercept({ + path: '/foo?hello=there&see=ya', + method: 'POST', + body: 'form1=data1&form2=data2' + }).reply(200, { foo: 'bar' }, { + headers: { + 'content-type': 'application/json' + }, + trailers: { 'Content-MD5': 'test' } + }) + + const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, { + method: 'POST', + body: 'form1=data1&form2=data2' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'application/json') + t.same(trailers, { 'content-md5': 'test' }) + + const jsonResponse = JSON.parse(await getResponse(body)) + t.same(jsonResponse, { + foo: 'bar' + }) +}) + +test('MockAgent - basic intercept with multiple pools', async (t) => { + t.plan(4) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + const mockPool1 = mockAgent.get(baseUrl) + const mockPool2 = mockAgent.get('http://localhost:9999') + + mockPool1.intercept({ + path: '/foo?hello=there&see=ya', + method: 'POST', + body: 'form1=data1&form2=data2' + }).reply(200, { foo: 'bar-1' }, { + headers: { + 'content-type': 'application/json' + }, + trailers: { 'Content-MD5': 'test' } + }) + + mockPool2.intercept({ + path: '/foo?hello=there&see=ya', + method: 'GET', + body: 'form1=data1&form2=data2' + }).reply(200, { foo: 'bar-2' }) + + const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, { + method: 'POST', + body: 'form1=data1&form2=data2' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'application/json') + t.same(trailers, { 'content-md5': 'test' }) + + const jsonResponse = JSON.parse(await getResponse(body)) + t.same(jsonResponse, { + foo: 'bar-1' + }) +}) + +test('MockAgent - should handle multiple responses for an interceptor', async (t) => { + t.plan(6) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + + const interceptor = mockPool.intercept({ + path: '/foo', + method: 'POST' + }) + interceptor.reply(200, { foo: 'bar' }, { + headers: { + 'content-type': 'application/json' + } + }) + interceptor.reply(200, { hello: 'there' }, { + headers: { + 'content-type': 'application/json' + } + }) + + { + const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { + method: 'POST' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'application/json') + + const jsonResponse = JSON.parse(await getResponse(body)) + t.same(jsonResponse, { + foo: 'bar' + }) + } + + { + const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { + method: 'POST' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'application/json') + + const jsonResponse = JSON.parse(await getResponse(body)) + t.same(jsonResponse, { + hello: 'there' + }) + } +}) + +test('MockAgent - should call original Pool dispatch if request not found', async (t) => { + t.plan(5) + + const server = createServer((req, res) => { + t.equal(req.url, '/foo') + t.equal(req.method, 'GET') + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'text/plain') + + const response = await getResponse(body) + t.equal(response, 'hello') +}) + +test('MockAgent - should call original Client dispatch if request not found', async (t) => { + t.plan(5) + + const server = createServer((req, res) => { + t.equal(req.url, '/foo') + t.equal(req.method, 'GET') + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent({ connections: 1 }) + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'text/plain') + + const response = await getResponse(body) + t.equal(response, 'hello') +}) + +test('MockAgent - should handle string responses', async (t) => { + t.plan(2) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'POST' + }).reply(200, 'hello') + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'POST' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'hello') +}) + +test('MockAgent - should handle basic concurrency for requests', { jobs: 5 }, async (t) => { + t.plan(5) + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + await Promise.all([...Array(5).keys()].map(idx => + t.test(`concurrent job (${idx})`, async (innerTest) => { + innerTest.plan(2) + + const baseUrl = 'http://localhost:9999' + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'POST' + }).reply(200, { foo: `bar ${idx}` }) + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'POST' + }) + innerTest.equal(statusCode, 200) + + const jsonResponse = JSON.parse(await getResponse(body)) + innerTest.same(jsonResponse, { + foo: `bar ${idx}` + }) + }) + )) +}) + +test('MockAgent - handle delays to simulate work', async (t) => { + t.plan(3) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'POST' + }).reply(200, 'hello').delay(50) + + const start = process.hrtime() + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'POST' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'hello') + const elapsedInMs = process.hrtime(start)[1] / 1e6 + t.ok(elapsedInMs >= 50, `Elapsed time is not greater than 50ms: ${elapsedInMs}`) +}) + +test('MockAgent - should persist requests', async (t) => { + t.plan(8) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo?hello=there&see=ya', + method: 'POST', + body: 'form1=data1&form2=data2' + }).reply(200, { foo: 'bar' }, { + headers: { + 'content-type': 'application/json' + }, + trailers: { 'Content-MD5': 'test' } + }).persist() + + { + const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, { + method: 'POST', + body: 'form1=data1&form2=data2' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'application/json') + t.same(trailers, { 'content-md5': 'test' }) + + const jsonResponse = JSON.parse(await getResponse(body)) + t.same(jsonResponse, { + foo: 'bar' + }) + } + + { + const { statusCode, headers, trailers, body } = await request(`${baseUrl}/foo?hello=there&see=ya`, { + method: 'POST', + body: 'form1=data1&form2=data2' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'application/json') + t.same(trailers, { 'content-md5': 'test' }) + + const jsonResponse = JSON.parse(await getResponse(body)) + t.same(jsonResponse, { + foo: 'bar' + }) + } +}) + +test('MockAgent - handle persists with delayed requests', async (t) => { + t.plan(4) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'POST' + }).reply(200, 'hello').delay(1).persist() + + { + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'POST' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'hello') + } + + { + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'POST' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'hello') + } +}) + +test('MockAgent - calling close on a mock pool should not affect other mock pools', async (t) => { + t.plan(4) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPoolToClose = mockAgent.get('http://localhost:9999') + mockPoolToClose.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'should-not-be-returned') + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'foo') + mockPool.intercept({ + path: '/bar', + method: 'POST' + }).reply(200, 'bar') + + await mockPoolToClose.close() + + { + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') + } + + { + const { statusCode, body } = await request(`${baseUrl}/bar`, { + method: 'POST' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'bar') + } +}) + +test('MockAgent - close removes all registered mock clients', async (t) => { + t.plan(2) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent({ connections: 1 }) + setGlobalDispatcher(mockAgent) + + const mockClient = mockAgent.get(baseUrl) + mockClient.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'foo') + + await mockAgent.close() + t.equal(mockAgent[kClients].size, 0) + + try { + await request(`${baseUrl}/foo`, { method: 'GET' }) + } catch (err) { + t.type(err, ClientDestroyedError) + } +}) + +test('MockAgent - close removes all registered mock pools', async (t) => { + t.plan(2) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'foo') + + await mockAgent.close() + t.equal(mockAgent[kClients].size, 0) + + try { + await request(`${baseUrl}/foo`, { method: 'GET' }) + } catch (err) { + t.type(err, ClientDestroyedError) + } +}) + +test('MockAgent - should handle replyWithError', async (t) => { + t.plan(1) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).replyWithError(new Error('kaboom')) + + await t.rejects(request(`${baseUrl}/foo`, { method: 'GET' }), new Error('kaboom')) +}) + +test('MockAgent - should support setting a reply to respond a set amount of times', async (t) => { + t.plan(9) + + const server = createServer((req, res) => { + t.equal(req.url, '/foo') + t.equal(req.method, 'GET') + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'foo').times(2) + + { + const { statusCode, body } = await request(`${baseUrl}/foo`) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') + } + + { + const { statusCode, body } = await request(`${baseUrl}/foo`) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') + } + + { + const { statusCode, headers, body } = await request(`${baseUrl}/foo`) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'text/plain') + + const response = await getResponse(body) + t.equal(response, 'hello') + } +}) + +test('MockAgent - persist overrides times', async (t) => { + t.plan(6) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'foo').times(2).persist() + + { + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') + } + + { + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') + } + + { + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') + } +}) + +test('MockAgent - matcher should not find mock dispatch if path is of unsupported type', async (t) => { + t.plan(4) + + const server = createServer((req, res) => { + t.equal(req.url, '/foo') + t.equal(req.method, 'GET') + res.end('hello') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: {}, + method: 'GET' + }).reply(200, 'foo') + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'hello') +}) + +test('MockAgent - should match path with regex', async (t) => { + t.plan(4) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: /foo/, + method: 'GET' + }).reply(200, 'foo').persist() + + { + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') + } + + { + const { statusCode, body } = await request(`${baseUrl}/hello/foobar`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') + } +}) + +test('MockAgent - should match path with function', async (t) => { + t.plan(2) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: (value) => value === '/foo', + method: 'GET' + }).reply(200, 'foo') + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') +}) + +test('MockAgent - should match method with regex', async (t) => { + t.plan(2) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: /^GET$/ + }).reply(200, 'foo') + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') +}) + +test('MockAgent - should match method with function', async (t) => { + t.plan(2) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: (value) => value === 'GET' + }).reply(200, 'foo') + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') +}) + +test('MockAgent - should match body with regex', async (t) => { + t.plan(2) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET', + body: /hello/ + }).reply(200, 'foo') + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET', + body: 'hello=there' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') +}) + +test('MockAgent - should match body with function', async (t) => { + t.plan(2) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET', + body: (value) => value.startsWith('hello') + }).reply(200, 'foo') + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET', + body: 'hello=there' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') +}) + +test('MockAgent - should match headers with string', async (t) => { + t.plan(6) + + const server = createServer((req, res) => { + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET', + headers: { + 'User-Agent': 'undici', + Host: 'example.com' + } + }).reply(200, 'foo') + + // Disable net connect so we can make sure it matches properly + mockAgent.disableNetConnect() + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET' + }), MockNotMatchedError, 'should reject with MockNotMatchedError') + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + foo: 'bar' + } + }), MockNotMatchedError, 'should reject with MockNotMatchedError') + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + foo: 'bar', + 'User-Agent': 'undici' + } + }), MockNotMatchedError, 'should reject with MockNotMatchedError') + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + foo: 'bar', + 'User-Agent': 'undici', + Host: 'wrong' + } + }), MockNotMatchedError, 'should reject with MockNotMatchedError') + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + foo: 'bar', + 'User-Agent': 'undici', + Host: 'example.com' + } + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') +}) + +test('MockAgent - should match headers with regex', async (t) => { + t.plan(6) + + const server = createServer((req, res) => { + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET', + headers: { + 'User-Agent': /^undici$/, + Host: /^example.com$/ + } + }).reply(200, 'foo') + + // Disable net connect so we can make sure it matches properly + mockAgent.disableNetConnect() + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET' + }), MockNotMatchedError, 'should reject with MockNotMatchedError') + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + foo: 'bar' + } + }), MockNotMatchedError, 'should reject with MockNotMatchedError') + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + foo: 'bar', + 'User-Agent': 'undici' + } + }), MockNotMatchedError, 'should reject with MockNotMatchedError') + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + foo: 'bar', + 'User-Agent': 'undici', + Host: 'wrong' + } + }), MockNotMatchedError, 'should reject with MockNotMatchedError') + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + foo: 'bar', + 'User-Agent': 'undici', + Host: 'example.com' + } + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') +}) + +test('MockAgent - should match headers with function', async (t) => { + t.plan(6) + + const server = createServer((req, res) => { + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET', + headers: { + 'User-Agent': (value) => value === 'undici', + Host: (value) => value === 'example.com' + } + }).reply(200, 'foo') + + // Disable net connect so we can make sure it matches properly + mockAgent.disableNetConnect() + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET' + }), MockNotMatchedError, 'should reject with MockNotMatchedError') + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + foo: 'bar' + } + }), MockNotMatchedError, 'should reject with MockNotMatchedError') + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + foo: 'bar', + 'User-Agent': 'undici' + } + }), MockNotMatchedError, 'should reject with MockNotMatchedError') + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + foo: 'bar', + 'User-Agent': 'undici', + Host: 'wrong' + } + }), MockNotMatchedError, 'should reject with MockNotMatchedError') + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + foo: 'bar', + 'User-Agent': 'undici', + Host: 'example.com' + } + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') +}) + +test('MockAgent - should match url with regex', async (t) => { + t.plan(2) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(new RegExp(baseUrl)) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'foo') + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') +}) + +test('MockAgent - should match url with function', async (t) => { + t.plan(2) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get((value) => baseUrl === value) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'foo') + + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') +}) + +test('MockAgent - handle default reply headers', async (t) => { + t.plan(3) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).defaultReplyHeaders({ foo: 'bar' }).reply(200, 'foo', { headers: { hello: 'there' } }) + + const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + t.same(headers, { + foo: 'bar', + hello: 'there' + }) + + const response = await getResponse(body) + t.equal(response, 'foo') +}) + +test('MockAgent - handle default reply trailers', async (t) => { + t.plan(3) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).defaultReplyTrailers({ foo: 'bar' }).reply(200, 'foo', { trailers: { hello: 'there' } }) + + const { statusCode, trailers, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + t.same(trailers, { + foo: 'bar', + hello: 'there' + }) + + const response = await getResponse(body) + t.equal(response, 'foo') +}) + +test('MockAgent - return calculated content-length if specified', async (t) => { + t.plan(3) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).replyContentLength().reply(200, 'foo', { headers: { hello: 'there' } }) + + const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + t.same(headers, { + hello: 'there', + 'content-length': 3 + }) + + const response = await getResponse(body) + t.equal(response, 'foo') +}) + +test('MockAgent - return calculated content-length for object response if specified', async (t) => { + t.plan(3) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + res.end('should not be called') + t.fail('should not be called') + t.end() + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).replyContentLength().reply(200, { foo: 'bar' }, { headers: { hello: 'there' } }) + + const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + t.same(headers, { + hello: 'there', + 'content-length': 13 + }) + + const jsonResponse = JSON.parse(await getResponse(body)) + t.same(jsonResponse, { foo: 'bar' }) +}) + +test('MockAgent - should activate and deactivate mock clients', async (t) => { + t.plan(9) + + const server = createServer((req, res) => { + t.equal(req.url, '/foo') + t.equal(req.method, 'GET') + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'foo').persist() + + { + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') + } + + mockAgent.deactivate() + + { + const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'text/plain') + + const response = await getResponse(body) + t.equal(response, 'hello') + } + + mockAgent.activate() + + { + const { statusCode, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + + const response = await getResponse(body) + t.equal(response, 'foo') + } +}) + +test('MockAgent - enableNetConnect should allow all original dispatches to be called if dispatch not found', async (t) => { + t.plan(5) + + const server = createServer((req, res) => { + t.equal(req.url, '/foo') + t.equal(req.method, 'GET') + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/wrong', + method: 'GET' + }).reply(200, 'foo') + + mockAgent.enableNetConnect() + + const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'text/plain') + + const response = await getResponse(body) + t.equal(response, 'hello') +}) + +test('MockAgent - enableNetConnect with a host string should allow all original dispatches to be called if mockDispatch not found', async (t) => { + t.plan(5) + + const server = createServer((req, res) => { + t.equal(req.url, '/foo') + t.equal(req.method, 'GET') + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/wrong', + method: 'GET' + }).reply(200, 'foo') + + mockAgent.enableNetConnect(`localhost:${server.address().port}`) + + const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'text/plain') + + const response = await getResponse(body) + t.equal(response, 'hello') +}) + +test('MockAgent - enableNetConnect when called with host string multiple times should allow all original dispatches to be called if mockDispatch not found', async (t) => { + t.plan(5) + + const server = createServer((req, res) => { + t.equal(req.url, '/foo') + t.equal(req.method, 'GET') + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/wrong', + method: 'GET' + }).reply(200, 'foo') + + mockAgent.enableNetConnect('example.com:9999') + mockAgent.enableNetConnect(`localhost:${server.address().port}`) + + const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'text/plain') + + const response = await getResponse(body) + t.equal(response, 'hello') +}) + +test('MockAgent - enableNetConnect with a host regex should allow all original dispatches to be called if mockDispatch not found', async (t) => { + t.plan(5) + + const server = createServer((req, res) => { + t.equal(req.url, '/foo') + t.equal(req.method, 'GET') + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/wrong', + method: 'GET' + }).reply(200, 'foo') + + mockAgent.enableNetConnect(new RegExp(`localhost:${server.address().port}`)) + + const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'text/plain') + + const response = await getResponse(body) + t.equal(response, 'hello') +}) + +test('MockAgent - enableNetConnect with a function should allow all original dispatches to be called if mockDispatch not found', async (t) => { + t.plan(5) + + const server = createServer((req, res) => { + t.equal(req.url, '/foo') + t.equal(req.method, 'GET') + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/wrong', + method: 'GET' + }).reply(200, 'foo') + + mockAgent.enableNetConnect((value) => value === `localhost:${server.address().port}`) + + const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'text/plain') + + const response = await getResponse(body) + t.equal(response, 'hello') +}) + +test('MockAgent - enableNetConnect with an unknown input should throw', async (t) => { + t.plan(1) + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get('http://localhost:9999') + mockPool.intercept({ + path: '/wrong', + method: 'GET' + }).reply(200, 'foo') + + t.throws(() => mockAgent.enableNetConnect({}), new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.')) +}) + +test('MockAgent - enableNetConnect should throw if dispatch not matched for path and the origin was not allowed by net connect', async (t) => { + t.plan(1) + + const server = createServer((req, res) => { + t.fail('should not be called') + t.end() + res.end('should not be called') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'foo') + + mockAgent.enableNetConnect('example.com:9999') + + await t.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)`)) +}) + +test('MockAgent - enableNetConnect should throw if dispatch not matched for method and the origin was not allowed by net connect', async (t) => { + t.plan(1) + + const server = createServer((req, res) => { + t.fail('should not be called') + t.end() + res.end('should not be called') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'foo') + + mockAgent.enableNetConnect('example.com:9999') + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'WRONG' + }), new MockNotMatchedError(`Mock dispatch not matched for method 'WRONG': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) +}) + +test('MockAgent - enableNetConnect should throw if dispatch not matched for body and the origin was not allowed by net connect', async (t) => { + t.plan(1) + + const server = createServer((req, res) => { + t.fail('should not be called') + t.end() + res.end('should not be called') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET', + body: 'hello' + }).reply(200, 'foo') + + mockAgent.enableNetConnect('example.com:9999') + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET', + body: 'wrong' + }), new MockNotMatchedError(`Mock dispatch not matched for body 'wrong': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) +}) + +test('MockAgent - enableNetConnect should throw if dispatch not matched for headers and the origin was not allowed by net connect', async (t) => { + t.plan(1) + + const server = createServer((req, res) => { + t.fail('should not be called') + t.end() + res.end('should not be called') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'GET', + headers: { + 'User-Agent': 'undici' + } + }).reply(200, 'foo') + + mockAgent.enableNetConnect('example.com:9999') + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + 'User-Agent': 'wrong' + } + }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"User-Agent":"wrong"}': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) +}) + +test('MockAgent - disableNetConnect should throw if dispatch not found by net connect', async (t) => { + t.plan(1) + + const server = createServer((req, res) => { + t.equal(req.url, '/foo') + t.equal(req.method, 'GET') + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/wrong', + method: 'GET' + }).reply(200, 'foo') + + mockAgent.disableNetConnect() + + await t.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)`)) +}) + +test('MockAgent - headers function interceptor', async (t) => { + t.plan(7) + + const server = createServer((req, res) => { + t.fail('should not be called') + t.end() + res.end('should not be called') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + const mockPool = mockAgent.get(baseUrl) + + // Disable net connect so we can make sure it matches properly + mockAgent.disableNetConnect() + + mockPool.intercept({ + path: '/foo', + method: 'GET', + headers (headers) { + t.equal(typeof headers, 'object') + return !Object.keys(headers).includes('authorization') + } + }).reply(200, 'foo').times(2) + + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + Authorization: 'Bearer foo' + } + }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"Authorization":"Bearer foo"}': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`)) + + { + const { statusCode } = await request(`${baseUrl}/foo`, { + method: 'GET', + headers: { + foo: 'bar' + } + }) + t.equal(statusCode, 200) + } + + { + const { statusCode } = await request(`${baseUrl}/foo`, { + method: 'GET' + }) + t.equal(statusCode, 200) + } +}) + +test('MockAgent - clients are not garbage collected', async (t) => { + const samples = 250 + t.plan(2) + + const server = createServer((req, res) => { + t.fail('should not be called') + t.end() + res.end('should not be called') + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const baseUrl = `http://localhost:${server.address().port}` + + // Create the dispatcher and isable net connect so we can make sure it matches properly + const dispatcher = new MockAgent() + dispatcher.disableNetConnect() + + // When Node 16 is the minimum supported, this can be replaced by simply requiring setTimeout from timers/promises + function sleep (delay) { + return new Promise(resolve => { + setTimeout(resolve, delay) + }) + } + + // Purposely create the pool inside a function so that the reference is lost + function intercept () { + // Create the pool and add a lot of intercepts + const pool = dispatcher.get(baseUrl) + + for (let i = 0; i < samples; i++) { + pool.intercept({ + path: `/foo/${i}`, + method: 'GET' + }).reply(200, Buffer.alloc(1024 * 1024)) + } + } + + intercept() + + const results = new Set() + for (let i = 0; i < samples; i++) { + // Let's make some time pass to allow garbage collection to happen + await sleep(10) + + const { statusCode } = await request(`${baseUrl}/foo/${i}`, { method: 'GET', dispatcher }) + results.add(statusCode) + } + + t.equal(results.size, 1) + t.ok(results.has(200)) +}) + +// https://github.com/nodejs/undici/issues/1321 +test('MockAgent - using fetch yields correct statusText', { skip: nodeMajor < 16 }, async (t) => { + const { fetch } = require('..') + + const mockAgent = new MockAgent() + mockAgent.disableNetConnect() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get('http://localhost:3000') + + mockPool.intercept({ + path: '/statusText', + method: 'GET' + }).reply(200, 'Body') + + const { status, statusText } = await fetch('http://localhost:3000/statusText') + + t.equal(status, 200) + t.equal(statusText, 'OK') + + mockPool.intercept({ + path: '/unknownStatusText', + method: 'GET' + }).reply(420, 'Everyday') + + const unknownStatusCodeRes = await fetch('http://localhost:3000/unknownStatusText') + t.equal(unknownStatusCodeRes.status, 420) + t.equal(unknownStatusCodeRes.statusText, 'unknown') + + t.end() +}) + +// https://github.com/nodejs/undici/issues/1556 +test('MockAgent - using fetch yields a headers object in the reply callback', { skip: nodeMajor < 16 }, async (t) => { + const { fetch } = require('..') + + const mockAgent = new MockAgent() + mockAgent.disableNetConnect() + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get('http://localhost:3000') + + mockPool.intercept({ + path: '/headers', + method: 'GET' + }).reply(200, (opts) => { + t.same(opts.headers, { + accept: '*/*', + 'accept-language': '*', + 'sec-fetch-mode': 'cors', + 'user-agent': 'undici', + 'accept-encoding': 'gzip, deflate' + }) + + return {} + }) + + await fetch('http://localhost:3000/headers', { + dispatcher: mockAgent + }) + + t.end() +}) + +// https://github.com/nodejs/undici/issues/1579 +test('MockAgent - headers in mock dispatcher intercept should be case-insensitive', { skip: nodeMajor < 16 }, async (t) => { + const { fetch } = require('..') + + const mockAgent = new MockAgent() + mockAgent.disableNetConnect() + setGlobalDispatcher(mockAgent) + t.teardown(mockAgent.close.bind(mockAgent)) + + const mockPool = mockAgent.get('https://example.com') + + mockPool + .intercept({ + path: '/', + headers: { + authorization: 'Bearer 12345', + 'USER-agent': 'undici' + } + }) + .reply(200) + + await fetch('https://example.com', { + headers: { + Authorization: 'Bearer 12345', + 'user-AGENT': 'undici' + } + }) + + t.end() +}) + +// https://github.com/nodejs/undici/issues/1757 +test('MockAgent - reply callback can be asynchronous', { skip: nodeMajor < 16 }, async (t) => { + const { fetch } = require('..') + const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream + + class MiniflareDispatcher extends Dispatcher { + constructor (inner, options) { + super(options) + this.inner = inner + } + + dispatch (options, handler) { + return this.inner.dispatch(options, handler) + } + + close (...args) { + return this.inner.close(...args) + } + + destroy (...args) { + return this.inner.destroy(...args) + } + } + + const mockAgent = new MockAgent() + const mockClient = mockAgent.get('http://localhost:3000') + mockAgent.disableNetConnect() + setGlobalDispatcher(new MiniflareDispatcher(mockAgent)) + + t.teardown(mockAgent.close.bind(mockAgent)) + + mockClient.intercept({ + path: () => true, + method: () => true + }).reply(200, async (opts) => { + if (opts.body && opts.body[Symbol.asyncIterator]) { + const chunks = [] + for await (const chunk of opts.body) { + chunks.push(chunk) + } + + return Buffer.concat(chunks) + } + + return opts.body + }).persist() + + { + const response = await fetch('http://localhost:3000', { + method: 'POST', + body: JSON.stringify({ foo: 'bar' }) + }) + + t.same(await response.json(), { foo: 'bar' }) + } + + { + const response = await fetch('http://localhost:3000', { + method: 'POST', + body: new ReadableStream({ + start (controller) { + controller.enqueue(new TextEncoder().encode('{"foo":')) + + setTimeout(() => { + controller.enqueue(new TextEncoder().encode('"bar"}')) + controller.close() + }, 100) + } + }), + duplex: 'half' + }) + + t.same(await response.json(), { foo: 'bar' }) + } +}) + +test('MockAgent - headers should be array of strings', async (t) => { + const mockAgent = new MockAgent() + mockAgent.disableNetConnect() + setGlobalDispatcher(mockAgent) + + const mockPool = mockAgent.get('http://localhost:3000') + + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).reply(200, 'foo', { + headers: { + 'set-cookie': [ + 'foo=bar', + 'bar=baz', + 'baz=qux' + ] + } + }) + + const { headers } = await request('http://localhost:3000/foo', { + method: 'GET' + }) + + t.same(headers['set-cookie'], [ + 'foo=bar', + 'bar=baz', + 'baz=qux' + ]) +}) + +// https://github.com/nodejs/undici/issues/2418 +test('MockAgent - Sending ReadableStream body', { skip: nodeMajor < 16 }, async (t) => { + t.plan(1) + const { fetch } = require('..') + const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + req.pipe(res) + }) + + t.teardown(mockAgent.close.bind(mockAgent)) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const url = `http://localhost:${server.address().port}` + + const response = await fetch(url, { + method: 'POST', + body: new ReadableStream({ + start (controller) { + controller.enqueue('test') + controller.close() + } + }), + duplex: 'half' + }) + + t.same(await response.text(), 'test') +}) |