'use strict'
const { describe, test, after } = require('node:test')
const { MockInterceptor, MockScope } = require('../lib/mock/mock-interceptor')
const MockAgent = require('../lib/mock/mock-agent')
const { kDispatchKey } = require('../lib/mock/mock-symbols')
const { InvalidArgumentError } = require('../lib/core/errors')
const { MockNotMatchedError } = require('../lib/mock/mock-errors')
const { fetch } = require('../lib/web/fetch/index')
const { request } = require('../index')
describe('MockInterceptor - path', () => {
test('should remove hash fragment from paths', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '#foobar',
method: ''
}, [])
t.assert.strictEqual(mockInterceptor[kDispatchKey].path, '')
})
})
describe('MockInterceptor - reply', () => {
test('should return MockScope', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.reply(200, 'hello')
t.assert.ok(result instanceof MockScope)
})
test('should error if passed options invalid', t => {
t.plan(2)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
t.assert.throws(() => mockInterceptor.reply(), new InvalidArgumentError('statusCode must be defined'))
t.assert.throws(() => mockInterceptor.reply(200, '', 'hello'), new InvalidArgumentError('responseOptions must be an object'))
})
})
describe('MockInterceptor - reply callback', () => {
test('should return MockScope', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.reply(200, () => 'hello')
t.assert.ok(result instanceof MockScope)
})
test('should error if passed options invalid', t => {
t.plan(3)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
t.assert.throws(() => mockInterceptor.reply(), new InvalidArgumentError('statusCode must be defined'))
t.assert.throws(() => mockInterceptor.reply(200, () => { }, 'hello'), new InvalidArgumentError('responseOptions must be an object'))
t.assert.throws(() => mockInterceptor.reply(200, () => { }, null), new InvalidArgumentError('responseOptions must be an object'))
})
})
describe('MockInterceptor - reply options callback', () => {
test('should return MockScope', t => {
t.plan(2)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.reply((options) => ({
statusCode: 200,
data: 'hello'
}))
t.assert.ok(result instanceof MockScope)
// Test parameters
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/test',
method: 'GET'
}).reply((options) => {
t.assert.deepStrictEqual(options, { path: '/test', method: 'GET', headers: { foo: 'bar' } })
return { statusCode: 200, data: 'hello' }
})
mockPool.dispatch({
path: '/test',
method: 'GET',
headers: { foo: 'bar' }
}, {
onRequestStart: () => {},
onResponseStart: () => {},
onResponseData: () => {},
onResponseEnd: () => {},
onResponseError: () => {}
})
})
test('should handle undefined data', t => {
t.plan(2)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.reply((options) => ({
statusCode: 200,
data: undefined
}))
t.assert.ok(result instanceof MockScope)
// Test parameters
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/test',
method: 'GET'
}).reply((options) => {
t.assert.deepStrictEqual(options, { path: '/test', method: 'GET', headers: { foo: 'bar' } })
return { statusCode: 200, data: 'hello' }
})
mockPool.dispatch({
path: '/test',
method: 'GET',
headers: { foo: 'bar' }
}, {
onRequestStart: () => {},
onResponseStart: () => {},
onResponseData: () => {},
onResponseEnd: () => {},
onResponseError: () => {}
})
})
test('should error if passed options invalid', async (t) => {
t.plan(4)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/test-return-undefined',
method: 'GET'
}).reply(() => { })
mockPool.intercept({
path: '/test-return-null',
method: 'GET'
}).reply(() => { return null })
mockPool.intercept({
path: '/test3',
method: 'GET'
}).reply(() => ({
statusCode: 200,
data: 'hello',
responseOptions: 42
}))
mockPool.intercept({
path: '/test4',
method: 'GET'
}).reply(() => ({
data: 'hello',
responseOptions: 42
}))
t.assert.throws(() => mockPool.dispatch({
path: '/test-return-undefined',
method: 'GET'
}, {
onRequestStart: () => {},
onResponseStart: () => {},
onResponseData: () => {},
onResponseEnd: () => {},
onResponseError: () => {}
}), new InvalidArgumentError('reply options callback must return an object'))
t.assert.throws(() => mockPool.dispatch({
path: '/test-return-null',
method: 'GET'
}, {
onRequestStart: () => {},
onResponseStart: () => {},
onResponseData: () => {},
onResponseEnd: () => {},
onResponseError: () => {}
}), new InvalidArgumentError('reply options callback must return an object'))
t.assert.throws(() => mockPool.dispatch({
path: '/test3',
method: 'GET'
}, {
onRequestStart: () => {},
onResponseStart: () => {},
onResponseData: () => {},
onResponseEnd: () => {},
onResponseError: () => {}
}), new InvalidArgumentError('responseOptions must be an object'))
t.assert.throws(() => mockPool.dispatch({
path: '/test4',
method: 'GET'
}, {
onRequestStart: () => {},
onResponseStart: () => {},
onResponseData: () => {},
onResponseEnd: () => {},
onResponseError: () => {}
}), new InvalidArgumentError('statusCode must be defined'))
})
test('should re-invoke the callback for every persistent reply', async t => {
t.plan(7)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
let calls = 0
mockPool.intercept({
path: '/test',
method: 'GET'
}).reply(() => ({
statusCode: 200,
data: `call ${++calls}`
})).persist()
for (let i = 1; i <= 3; i++) {
const { statusCode, body } = await request(`${baseUrl}/test`, { dispatcher: mockAgent })
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(await body.text(), `call ${i}`)
}
t.assert.strictEqual(calls, 3)
})
})
describe('MockInterceptor - asynchronous reply options callback', () => {
test('should resolve the reply from an asynchronous callback', async t => {
t.plan(3)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/test',
method: 'GET'
}).reply(async (options) => {
t.assert.strictEqual(options.path, '/test')
await new Promise((resolve) => setImmediate(resolve))
return { statusCode: 201, data: 'hello' }
})
const { statusCode, body } = await request(`${baseUrl}/test`, { dispatcher: mockAgent })
t.assert.strictEqual(statusCode, 201)
t.assert.strictEqual(await body.text(), 'hello')
})
test('should apply default headers and content length after resolution', async t => {
t.plan(4)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/test',
method: 'GET'
}).defaultReplyHeaders({ foo: 'bar' }).replyContentLength().reply(async () => ({
statusCode: 200,
data: 'hello'
}))
const { statusCode, headers, body } = await request(`${baseUrl}/test`, { dispatcher: mockAgent })
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(headers.foo, 'bar')
t.assert.strictEqual(headers['content-length'], '5')
t.assert.strictEqual(await body.text(), 'hello')
})
test('should support times() with an asynchronous callback', async t => {
t.plan(3)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/test',
method: 'GET'
}).reply(async () => ({ statusCode: 200, data: 'hello' })).times(2)
for (let i = 0; i < 2; i++) {
const { statusCode, body } = await request(`${baseUrl}/test`, { dispatcher: mockAgent })
t.assert.strictEqual(statusCode, 200)
await body.text()
}
await t.assert.rejects(request(`${baseUrl}/test`, { dispatcher: mockAgent }), MockNotMatchedError)
})
test('should re-invoke an asynchronous callback for every persistent reply', async t => {
t.plan(5)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
let calls = 0
mockPool.intercept({
path: '/test',
method: 'GET'
}).reply(async () => ({
statusCode: 200,
data: `async call ${++calls}`
})).persist()
for (let i = 1; i <= 2; i++) {
const { statusCode, body } = await request(`${baseUrl}/test`, { dispatcher: mockAgent })
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(await body.text(), `async call ${i}`)
}
t.assert.strictEqual(calls, 2)
})
test('should reject if an asynchronous callback resolves to an invalid format', async t => {
t.plan(2)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/test-resolve-null',
method: 'GET'
}).reply(async () => null)
mockPool.intercept({
path: '/test-no-status-code',
method: 'GET'
}).reply(async () => ({ data: 'hello' }))
await t.assert.rejects(
request(`${baseUrl}/test-resolve-null`, { dispatcher: mockAgent }),
new InvalidArgumentError('reply options callback must return an object')
)
await t.assert.rejects(
request(`${baseUrl}/test-no-status-code`, { dispatcher: mockAgent }),
new InvalidArgumentError('statusCode must be defined')
)
})
test('should reject with the reason of a rejected asynchronous callback', async t => {
t.plan(1)
const baseUrl = 'http://localhost:9999'
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
after(() => mockAgent.close())
const mockPool = mockAgent.get(baseUrl)
mockPool.intercept({
path: '/test',
method: 'GET'
}).reply(async () => {
throw new Error('kaboom')
})
await t.assert.rejects(request(`${baseUrl}/test`, { dispatcher: mockAgent }), new Error('kaboom'))
})
})
describe('MockInterceptor - replyWithError', () => {
test('should return MockScope', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.replyWithError(new Error('kaboom'))
t.assert.ok(result instanceof MockScope)
})
test('should error if passed options invalid', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
t.assert.throws(() => mockInterceptor.replyWithError(), new InvalidArgumentError('error must be defined'))
})
})
describe('MockInterceptor - defaultReplyHeaders', () => {
test('should return MockInterceptor', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.defaultReplyHeaders({})
t.assert.ok(result instanceof MockInterceptor)
})
test('should error if passed options invalid', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
t.assert.throws(() => mockInterceptor.defaultReplyHeaders(), new InvalidArgumentError('headers must be defined'))
})
})
describe('MockInterceptor - defaultReplyTrailers', () => {
test('should return MockInterceptor', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.defaultReplyTrailers({})
t.assert.ok(result instanceof MockInterceptor)
})
test('should error if passed options invalid', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
t.assert.throws(() => mockInterceptor.defaultReplyTrailers(), new InvalidArgumentError('trailers must be defined'))
})
})
describe('MockInterceptor - replyContentLength', () => {
test('should return MockInterceptor', t => {
t.plan(1)
const mockInterceptor = new MockInterceptor({
path: '',
method: ''
}, [])
const result = mockInterceptor.defaultReplyTrailers({})
t.assert.ok(result instanceof MockInterceptor)
})
})
describe('https://github.com/nodejs/undici/issues/3649', () => {
[
['/api/some-path', '/api/some-path'],
['/api/some-path/', '/api/some-path'],
['/api/some-path', '/api/some-path/'],
['/api/some-path/', '/api/some-path/'],
['/api/some-path////', '/api/some-path//'],
['', ''],
['/', ''],
['', '/'],
['/', '/'],
['/api/some-path?a=1', '/api/some-path/?a=1'],
['/api/some-path/?a=1', '/api/some-path?a=1'],
['/api/some-path/?a=1', '/api/some-path/?a=1']
].forEach(([interceptPath, fetchedPath], index) => {
test(`MockAgent should match with or without trailing slash by setting ignoreTrailingSlash as MockAgent option /${index}`, async (t) => {
t.plan(1)
const mockAgent = new MockAgent({ ignoreTrailingSlash: true })
mockAgent.disableNetConnect()
mockAgent
.get('https://localhost')
.intercept({ path: interceptPath }).reply(200, { ok: true })
const res = await fetch(new URL(fetchedPath, 'https://localhost'), { dispatcher: mockAgent })
t.assert.deepStrictEqual(await res.json(), { ok: true })
})
test(`MockAgent should match with or without trailing slash by setting ignoreTrailingSlash as intercept option /${index}`, async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
mockAgent
.get('https://localhost')
.intercept({ path: interceptPath, ignoreTrailingSlash: true }).reply(200, { ok: true })
const res = await fetch(new URL(fetchedPath, 'https://localhost'), { dispatcher: mockAgent })
t.assert.deepStrictEqual(await res.json(), { ok: true })
})
if (
(interceptPath === fetchedPath && (interceptPath !== '' && fetchedPath !== '')) ||
(interceptPath === '/' && fetchedPath === '')
) {
test(`MockAgent should should match on strict equal cases of paths when ignoreTrailingSlash is not set /${index}`, async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
mockAgent
.get('https://localhost')
.intercept({ path: interceptPath }).reply(200, { ok: true })
const res = await fetch(new URL(fetchedPath, 'https://localhost'), { dispatcher: mockAgent })
t.assert.deepStrictEqual(await res.json(), { ok: true })
})
} else {
test(`MockAgent should should reject on not strict equal cases of paths when ignoreTrailingSlash is not set /${index}`, async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
mockAgent
.get('https://localhost')
.intercept({ path: interceptPath }).reply(200, { ok: true })
await t.assert.rejects(fetch(new URL(fetchedPath, 'https://localhost'), { dispatcher: mockAgent }))
})
}
})
})
describe('MockInterceptor - ignoreTrailingSlash with non-string path matchers', () => {
[
['RegExp', /^\/api\/some-path$/],
['Function', (path) => path === '/api/some-path']
].forEach(([matcherType, path]) => {
['/api/some-path', '/api/some-path/', '/api/some-path///'].forEach((fetchedPath) => {
test(`a ${matcherType} path matcher matches '${fetchedPath}' as a MockAgent option`, async (t) => {
t.plan(1)
const mockAgent = new MockAgent({ ignoreTrailingSlash: true })
mockAgent.disableNetConnect()
t.after(() => mockAgent.close())
mockAgent
.get('https://localhost')
.intercept({ path }).reply(200, { ok: true })
const res = await fetch(new URL(fetchedPath, 'https://localhost'), { dispatcher: mockAgent })
t.assert.deepStrictEqual(await res.json(), { ok: true })
})
test(`a ${matcherType} path matcher matches '${fetchedPath}' as an intercept option`, async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
t.after(() => mockAgent.close())
mockAgent
.get('https://localhost')
.intercept({ path, ignoreTrailingSlash: true }).reply(200, { ok: true })
const res = await fetch(new URL(fetchedPath, 'https://localhost'), { dispatcher: mockAgent })
t.assert.deepStrictEqual(await res.json(), { ok: true })
})
})
test(`a ${matcherType} path matcher still rejects a non-matching path`, async (t) => {
t.plan(1)
const mockAgent = new MockAgent({ ignoreTrailingSlash: true })
mockAgent.disableNetConnect()
t.after(() => mockAgent.close())
mockAgent
.get('https://localhost')
.intercept({ path }).reply(200, { ok: true })
await t.assert.rejects(
fetch(new URL('/api/other-path', 'https://localhost'), { dispatcher: mockAgent }),
(err) => err.cause instanceof MockNotMatchedError
)
})
})
})
describe('MockInterceptor - different payloads', () => {
[
// Buffer
['arrayBuffer', 'ArrayBuffer', 'ArrayBuffer', new TextEncoder().encode('{"test":true}').buffer, new TextEncoder().encode('{"test":true}').buffer],
['json', 'ArrayBuffer', 'Object', new TextEncoder().encode('{"test":true}').buffer, { test: true }],
['bytes', 'ArrayBuffer', 'Uint8Array', new TextEncoder().encode('{"test":true}').buffer, new TextEncoder().encode('{"test":true}')],
['text', 'ArrayBuffer', 'string', new TextEncoder().encode('{"test":true}').buffer, '{"test":true}'],
// Buffer
['arrayBuffer', 'Buffer', 'ArrayBuffer', Buffer.from('{"test":true}'), new TextEncoder().encode('{"test":true}').buffer],
['json', 'Buffer', 'Object', Buffer.from('{"test":true}'), { test: true }],
['bytes', 'Buffer', 'Uint8Array', Buffer.from('{"test":true}'), new TextEncoder().encode('{"test":true}')],
['text', 'Buffer', 'string', Buffer.from('{"test":true}'), '{"test":true}'],
// Uint8Array
['arrayBuffer', 'Uint8Array', 'ArrayBuffer', new TextEncoder().encode('{"test":true}'), new TextEncoder().encode('{"test":true}').buffer],
['json', 'Uint8Array', 'Object', new TextEncoder().encode('{"test":true}'), { test: true }],
['bytes', 'Uint8Array', 'Uint8Array', new TextEncoder().encode('{"test":true}'), new TextEncoder().encode('{"test":true}')],
['text', 'Uint8Array', 'string', new TextEncoder().encode('{"test":true}'), '{"test":true}'],
// string
['arrayBuffer', 'string', 'ArrayBuffer', '{"test":true}', new TextEncoder().encode('{"test":true}').buffer],
['json', 'string', 'Object', '{"test":true}', { test: true }],
['bytes', 'string', 'Uint8Array', '{"test":true}', new TextEncoder().encode('{"test":true}')],
['text', 'string', 'string', '{"test":true}', '{"test":true}'],
// DataView
['arrayBuffer', 'DataView', 'ArrayBuffer', new DataView(new TextEncoder().encode('{"test":true}').buffer), new TextEncoder().encode('{"test":true}').buffer],
['json', 'DataView', 'Object', new DataView(new TextEncoder().encode('{"test":true}').buffer), { test: true }],
['bytes', 'DataView', 'Uint8Array', new DataView(new TextEncoder().encode('{"test":true}').buffer), new TextEncoder().encode('{"test":true}')],
['text', 'DataView', 'string', new DataView(new TextEncoder().encode('{"test":true}').buffer), '{"test":true}'],
// DataView covering only part of its backing ArrayBuffer
['text', 'DataView with an offset', 'string', new DataView(new TextEncoder().encode('xx{"test":true}yy').buffer, 2, 13), '{"test":true}'],
// object
['arrayBuffer', 'Object', 'ArrayBuffer', { test: true }, new TextEncoder().encode('{"test":true}').buffer],
['json', 'Object', 'Object', { test: true }, { test: true }],
['bytes', 'Object', 'Uint8Array', { test: true }, new TextEncoder().encode('{"test":true}')],
['text', 'Object', 'string', { test: true }, '{"test":true}']
].forEach(([method, inputType, outputType, input, output]) => {
test(`${inputType} will be returned as ${outputType} via ${method}()`, async (t) => {
t.plan(1)
const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
mockAgent
.get('https://localhost')
.intercept({ path: '/' }).reply(200, input)
const response = await fetch('https://localhost', { dispatcher: mockAgent })
t.assert.deepStrictEqual(await response[method](), output)
})
})
})