'use strict'
const { test } = require('node:test')
const assert = require('node:assert')
const { Stream } = require('node:stream')
const { EventEmitter } = require('node:events')
const util = require('../../lib/core/util')
const { headerNameLowerCasedRecord } = require('../../lib/core/constants')
const { InvalidArgumentError } = require('../../lib/core/errors')
test('isStream', () => {
const stream = new Stream()
assert.ok(util.isStream(stream))
const buffer = Buffer.alloc(0)
assert.ok(util.isStream(buffer) === false)
const ee = new EventEmitter()
assert.ok(util.isStream(ee) === false)
})
test('addAbortListener supports AbortSignal', async () => {
const ac = new AbortController()
let calls = 0
util.addAbortListener(ac.signal, () => {
calls++
})
ac.abort()
await new Promise((resolve) => setImmediate(resolve))
assert.equal(calls, 1)
})
test('addAbortListener removes native AbortSignal listener', async () => {
const ac = new AbortController()
let calls = 0
const remove = util.addAbortListener(ac.signal, () => {
calls++
})
remove()
ac.abort()
await new Promise((resolve) => setImmediate(resolve))
assert.equal(calls, 0)
})
test('getServerName', () => {
assert.equal(util.getServerName('1.1.1.1'), '')
assert.equal(util.getServerName('1.1.1.1:443'), '')
assert.equal(util.getServerName('example.com'), 'example.com')
assert.equal(util.getServerName('example.com:80'), 'example.com')
assert.equal(util.getServerName('[2606:4700:4700::1111]'), '')
assert.equal(util.getServerName('[2606:4700:4700::1111]:443'), '')
})
test('assertRequestHandler', () => {
assert.throws(() => util.assertRequestHandler(null), InvalidArgumentError, 'handler must be an object')
assert.throws(() => util.assertRequestHandler({
onRequestStart: null
}), InvalidArgumentError, 'invalid onRequestStart method')
assert.throws(() => util.assertRequestHandler({
onRequestStart: () => {},
onResponseError: null
}), InvalidArgumentError, 'invalid onResponseError method')
assert.throws(() => util.assertRequestHandler({
onRequestStart: () => {},
onResponseError: () => {},
onBodySent: null
}), InvalidArgumentError, 'invalid onBodySent method')
assert.throws(() => util.assertRequestHandler({
onRequestStart: () => {},
onResponseError: () => {},
onRequestSent: null
}), InvalidArgumentError, 'invalid onRequestSent method')
assert.throws(() => util.assertRequestHandler({
onRequestStart: () => {},
onResponseError: () => {},
onBodySent: () => {},
onRequestSent: () => {},
onResponseStart: null
}), InvalidArgumentError, 'invalid onResponseStart method')
assert.throws(() => util.assertRequestHandler({
onRequestStart: () => {},
onResponseError: () => {},
onBodySent: () => {},
onRequestSent: () => {},
onResponseStart: () => {},
onResponseData: null
}), InvalidArgumentError, 'invalid onResponseData method')
assert.throws(() => util.assertRequestHandler({
onRequestStart: () => {},
onResponseError: () => {},
onBodySent: () => {},
onRequestSent: () => {},
onResponseStart: () => {},
onResponseData: () => {},
onResponseEnd: null
}), InvalidArgumentError, 'invalid onResponseEnd method')
assert.throws(() => util.assertRequestHandler({
onRequestStart: () => {},
onResponseError: () => {},
onBodySent: () => {},
onRequestUpgrade: 'null'
}, 'CONNECT'), InvalidArgumentError, 'invalid onRequestUpgrade method')
assert.throws(() => util.assertRequestHandler({
onRequestStart: () => {},
onResponseError: () => {},
onBodySent: () => {},
onRequestUpgrade: 'null'
}, 'CONNECT', () => {}), InvalidArgumentError, 'invalid onRequestUpgrade method')
})
test('parseHeaders', () => {
assert.deepEqual(util.parseHeaders(['key', 'value']), { key: 'value' })
assert.deepEqual(util.parseHeaders([Buffer.from('key'), Buffer.from('value')]), { key: 'value' })
assert.deepEqual(util.parseHeaders(['Key', 'Value']), { key: 'Value' })
assert.deepEqual(util.parseHeaders(['Key', 'value', 'key', 'Value']), { key: ['value', 'Value'] })
assert.deepEqual(util.parseHeaders(['key', ['value1', 'value2', 'value3']]), { key: ['value1', 'value2', 'value3'] })
assert.deepEqual(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]]), { key: ['value1', 'value2', 'value3'] })
})
test('parseHeaders decodes values as latin1, not utf8', () => {
// These bytes (0xE2, 0x80, 0xA6) are the UTF-8 encoding of U+2026 (ellipsis)
// When decoded as latin1, they should be 3 separate characters: â, €, ¦
// When incorrectly decoded as UTF-8, they would be a single character: …
const latin1Bytes = Buffer.from([0xe2, 0x80, 0xa6])
const result = util.parseHeaders([Buffer.from('x-test'), latin1Bytes])
assert.strictEqual(result['x-test'].length, 3)
assert.strictEqual(result['x-test'].charCodeAt(0), 0xe2)
assert.strictEqual(result['x-test'].charCodeAt(1), 0x80)
assert.strictEqual(result['x-test'].charCodeAt(2), 0xa6)
})
test('parseHeaders decodes duplicate header values as latin1', () => {
const latin1Bytes = Buffer.from([0xe2, 0x80, 0xa6])
const result = util.parseHeaders([
Buffer.from('x-test'), Buffer.from('first'),
Buffer.from('x-test'), latin1Bytes
])
assert.deepEqual(result['x-test'][0], 'first')
assert.strictEqual(result['x-test'][1].length, 3)
assert.strictEqual(result['x-test'][1].charCodeAt(0), 0xe2)
})
test('parseHeaders decodes array header values as latin1', () => {
const latin1Bytes = Buffer.from([0xe2, 0x80, 0xa6])
const result = util.parseHeaders([Buffer.from('x-test'), [latin1Bytes, latin1Bytes]])
assert.strictEqual(result['x-test'].length, 2)
assert.strictEqual(result['x-test'][0].length, 3)
assert.strictEqual(result['x-test'][0].charCodeAt(0), 0xe2)
})
test('parseRawHeaders', () => {
assert.deepEqual(util.parseRawHeaders(), [])
assert.deepEqual(util.parseRawHeaders(null), [])
assert.deepEqual(util.parseRawHeaders(['key', 'value', Buffer.from('key'), Buffer.from('value')]), ['key', 'value', 'key', 'value'])
assert.deepEqual(util.parseRawHeaders(['content-length', 'value', 'content-disposition', 'form-data; name="fieldName"']), ['content-length', 'value', 'content-disposition', 'form-data; name="fieldName"'])
assert.deepEqual(util.parseRawHeaders({ key: 'value', 'set-cookie': ['a=1', 'b=2'] }), ['key', 'value', 'set-cookie', 'a=1', 'set-cookie', 'b=2'])
})
test('parseRawHeaders decodes values as latin1, not utf8', () => {
// These bytes (0xE2, 0x80, 0xA6) are the UTF-8 encoding of U+2026 (ellipsis)
// When decoded as latin1, they should be 3 separate characters
// When incorrectly decoded as UTF-8, they would be a single character
const latin1Bytes = Buffer.from([0xe2, 0x80, 0xa6])
const result = util.parseRawHeaders([Buffer.from('x-test'), latin1Bytes])
assert.strictEqual(result[0], 'x-test')
assert.strictEqual(result[1].length, 3)
assert.strictEqual(result[1].charCodeAt(0), 0xe2)
assert.strictEqual(result[1].charCodeAt(1), 0x80)
assert.strictEqual(result[1].charCodeAt(2), 0xa6)
})
test('serializePathWithQuery', () => {
const tests = [
[{ id: BigInt(123456) }, 'id=123456'],
[{ date: new Date() }, 'date='],
[{ obj: { id: 1 } }, 'obj='],
[{ params: ['a', 'b', 'c'] }, 'params=a¶ms=b¶ms=c'],
[{ bool: true }, 'bool=true'],
[{ number: 123456 }, 'number=123456'],
[{ string: 'hello' }, 'string=hello'],
[{ null: null }, 'null='],
[{ void: undefined }, 'void='],
[{ fn: function () {} }, 'fn='],
[{}, '']
]
const base = 'https://www.google.com'
for (const [input, output] of tests) {
const expected = `${base}${output ? `?${output}` : output}`
assert.deepEqual(util.serializePathWithQuery(base, input), expected)
}
})
test('headerNameLowerCasedRecord', () => {
assert.ok(typeof headerNameLowerCasedRecord.hasOwnProperty !== 'function')
})