Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix mock bugs #1323

Merged
merged 5 commits into from Apr 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/mock/mock-interceptor.js
Expand Up @@ -74,6 +74,9 @@ class MockInterceptor {
const parsedURL = new URL(opts.path, 'data://')
opts.path = parsedURL.pathname + parsedURL.search
}
if (typeof opts.method === 'string') {
opts.method = opts.method.toUpperCase()
}

this[kDispatchKey] = buildKey(opts)
this[kDispatches] = mockDispatches
Expand Down
77 changes: 76 additions & 1 deletion lib/mock/mock-utils.js
Expand Up @@ -132,6 +132,80 @@ function generateKeyValues (data) {
return Object.entries(data).reduce((keyValuePairs, [key, value]) => [...keyValuePairs, key, value], [])
}

/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
* @param {number} statusCode
*/
function getStatusText (statusCode) {
switch (statusCode) {
case 100: return 'Continue'
case 101: return 'Switching Protocols'
case 102: return 'Processing'
case 103: return 'Early Hints'
case 200: return 'OK'
case 201: return 'Created'
case 202: return 'Accepted'
case 203: return 'Non-Authoritative Information'
case 204: return 'No Content'
case 205: return 'Reset Content'
case 206: return 'Partial Content'
case 207: return 'Multi-Status'
case 208: return 'Already Reported'
case 226: return 'IM Used'
case 300: return 'Multiple Choice'
case 301: return 'Moved Permanently'
case 302: return 'Found'
case 303: return 'See Other'
case 304: return 'Not Modified'
case 305: return 'Use Proxy'
case 306: return 'unused'
case 307: return 'Temporary Redirect'
case 308: return 'Permanent Redirect'
case 400: return 'Bad Request'
case 401: return 'Unauthorized'
case 402: return 'Payment Required'
case 403: return 'Forbidden'
case 404: return 'Not Found'
case 405: return 'Method Not Allowed'
case 406: return 'Not Acceptable'
case 407: return 'Proxy Authentication Required'
case 408: return 'Request Timeout'
case 409: return 'Conflict'
case 410: return 'Gone'
case 411: return 'Length Required'
case 412: return 'Precondition Failed'
case 413: return 'Payload Too Large'
case 414: return 'URI Too Large'
case 415: return 'Unsupported Media Type'
case 416: return 'Range Not Satisfiable'
case 417: return 'Expectation Failed'
case 418: return 'I\'m a teapot'
case 421: return 'Misdirected Request'
case 422: return 'Unprocessable Entity'
case 423: return 'Locked'
case 424: return 'Failed Dependency'
case 425: return 'Too Early'
case 426: return 'Upgrade Required'
case 428: return 'Precondition Required'
case 429: return 'Too Many Requests'
case 431: return 'Request Header Fields Too Large'
case 451: return 'Unavailable For Legal Reasons'
case 500: return 'Internal Server Error'
case 501: return 'Not Implemented'
case 502: return 'Bad Gateway'
case 503: return 'Service Unavailable'
case 504: return 'Gateway Timeout'
case 505: return 'HTTP Version Not Supported'
case 506: return 'Variant Also Negotiates'
case 507: return 'Insufficient Storage'
case 508: return 'Loop Detected'
case 510: return 'Not Extended'
case 511: return 'Network Authentication Required'
default:
throw new ReferenceError(`Unknown status code "${statusCode}"!`)
}
}

async function getResponse (body) {
const buffers = []
for await (const data of body) {
Expand Down Expand Up @@ -189,7 +263,7 @@ function mockDispatch (opts, handler) {
const responseHeaders = generateKeyValues(headers)
const responseTrailers = generateKeyValues(trailers)

handler.onHeaders(statusCode, responseHeaders, resume)
handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode))
handler.onData(Buffer.from(responseData))
handler.onComplete(responseTrailers)
deleteMockDispatch(mockDispatches, key)
Expand Down Expand Up @@ -256,6 +330,7 @@ module.exports = {
generateKeyValues,
matchValue,
getResponse,
getStatusText,
mockDispatch,
buildMockDispatch,
checkNetConnect,
Expand Down
36 changes: 36 additions & 0 deletions test/mock-agent.js
Expand Up @@ -13,6 +13,8 @@ const { kAgent } = require('../lib/mock/mock-symbols')
const Dispatcher = require('../lib/dispatcher')
const { MockNotMatchedError } = require('../lib/mock/mock-errors')

const nodeMajor = Number(process.versions.node.split('.')[0])

test('MockAgent - constructor', t => {
t.plan(5)

Expand Down Expand Up @@ -2396,3 +2398,37 @@ test('MockAgent - clients are not garbage collected', async (t) => {
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: '/badStatusText',
method: 'GET'
}).reply(420, 'Everyday')

await t.rejects(
fetch('http://localhost:3000/badStatusText'),
TypeError
)

t.end()
})
18 changes: 17 additions & 1 deletion test/mock-client.js
Expand Up @@ -125,7 +125,7 @@ test('MockClient - intercept should return a MockInterceptor', (t) => {
})

test('MockClient - intercept validation', (t) => {
t.plan(3)
t.plan(4)

t.test('it should error if no options specified in the intercept', t => {
t.plan(1)
Expand Down Expand Up @@ -156,6 +156,22 @@ test('MockClient - intercept validation', (t) => {

t.throws(() => mockClient.intercept({ path: '/foo' }), new InvalidArgumentError('opts.method must be defined'))
})

t.test('it should uppercase the method - https://github.com/nodejs/undici/issues/1320', t => {
t.plan(1)

const mockAgent = new MockAgent()
const mockClient = mockAgent.get('http://localhost:3000')

t.teardown(mockAgent.close.bind(mockAgent))

mockClient.intercept({
path: '/test',
method: 'patch'
}).reply(200, 'Hello!')

t.equal(mockClient[kDispatches][0].method, 'PATCH')
})
})

test('MockClient - close should run without error', async (t) => {
Expand Down
22 changes: 21 additions & 1 deletion test/mock-utils.js
Expand Up @@ -5,7 +5,8 @@ const { MockNotMatchedError } = require('../lib/mock/mock-errors')
const {
deleteMockDispatch,
getMockDispatch,
getResponseData
getResponseData,
getStatusText
} = require('../lib/mock/mock-utils')

test('deleteMockDispatch - should do nothing if not able to find mock dispatch', (t) => {
Expand Down Expand Up @@ -109,3 +110,22 @@ test('getResponseData', (t) => {
t.ok(Buffer.isBuffer(responseData))
})
})

test('getStatusText', (t) => {
for (const statusCode of [
100, 101, 102, 103, 200, 201, 202, 203,
204, 205, 206, 207, 208, 226, 300, 301,
302, 303, 304, 305, 306, 307, 308, 400,
401, 402, 403, 404, 405, 406, 407, 408,
409, 410, 411, 412, 413, 414, 415, 416,
417, 418, 421, 422, 423, 424, 425, 426,
428, 429, 431, 451, 500, 501, 502, 503,
504, 505, 506, 507, 508, 510, 511
]) {
t.ok(getStatusText(statusCode))
}

t.throws(() => getStatusText(420), ReferenceError)

t.end()
})