Skip to content

Commit 140e3c9

Browse files
committed
test(http-request): add 16 more tests for complete coverage of new features
- hooks edge cases: onRequest-only, onResponse-only, empty hooks, httpJson/httpText passthrough, response headers in hook, duration - maxResponseSize edge cases: exact size match, zero (no limit), enforcement after redirect - rawResponse edge cases: after redirect (final response), on server error - enriched errors integration: method+url in timeout, method+url in connection error, cause chain preserved, url in enrichErrorMessage
1 parent 5271f13 commit 140e3c9

1 file changed

Lines changed: 178 additions & 12 deletions

File tree

test/unit/http-request.test.mts

Lines changed: 178 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1894,7 +1894,9 @@ abc123def456789012345678901234567890123456789012345678901234abcd
18941894
expect(info.error).toBeDefined()
18951895
}
18961896
} finally {
1897-
await new Promise<void>(resolve => { testServer.close(() => resolve()) })
1897+
await new Promise<void>(resolve => {
1898+
testServer.close(() => resolve())
1899+
})
18981900
}
18991901
})
19001902

@@ -1994,8 +1996,8 @@ abc123def456789012345678901234567890123456789012345678901234abcd
19941996

19951997
// At least one hook call should contain the size limit error
19961998
expect(responseInfos.length).toBeGreaterThanOrEqual(1)
1997-
const sizeError = responseInfos.find(
1998-
info => info.error?.message?.includes('exceeds maximum size limit'),
1999+
const sizeError = responseInfos.find(info =>
2000+
info.error?.message?.includes('exceeds maximum size limit'),
19992001
)
20002002
expect(sizeError).toBeDefined()
20012003
})
@@ -2010,7 +2012,9 @@ abc123def456789012345678901234567890123456789012345678901234abcd
20102012

20112013
it('should have headers on rawResponse', async () => {
20122014
const response = await httpRequest(`${httpBaseUrl}/json`)
2013-
expect(response.rawResponse!.headers['content-type']).toContain('application/json')
2015+
expect(response.rawResponse!.headers['content-type']).toContain(
2016+
'application/json',
2017+
)
20142018
})
20152019

20162020
it('should be available on non-2xx responses', async () => {
@@ -2022,52 +2026,68 @@ abc123def456789012345678901234567890123456789012345678901234abcd
20222026

20232027
describe('enrichErrorMessage', () => {
20242028
it('should enrich ECONNREFUSED', () => {
2025-
const err = Object.assign(new Error('connect failed'), { code: 'ECONNREFUSED' }) as NodeJS.ErrnoException
2029+
const err = Object.assign(new Error('connect failed'), {
2030+
code: 'ECONNREFUSED',
2031+
}) as NodeJS.ErrnoException
20262032
const msg = enrichErrorMessage('http://localhost:1', 'GET', err)
20272033
expect(msg).toContain('Connection refused')
20282034
expect(msg).toContain('GET request failed')
20292035
})
20302036

20312037
it('should enrich ENOTFOUND', () => {
2032-
const err = Object.assign(new Error('not found'), { code: 'ENOTFOUND' }) as NodeJS.ErrnoException
2038+
const err = Object.assign(new Error('not found'), {
2039+
code: 'ENOTFOUND',
2040+
}) as NodeJS.ErrnoException
20332041
const msg = enrichErrorMessage('http://no-such-host.invalid', 'POST', err)
20342042
expect(msg).toContain('DNS lookup failed')
20352043
expect(msg).toContain('POST request failed')
20362044
})
20372045

20382046
it('should enrich ETIMEDOUT', () => {
2039-
const err = Object.assign(new Error('timed out'), { code: 'ETIMEDOUT' }) as NodeJS.ErrnoException
2047+
const err = Object.assign(new Error('timed out'), {
2048+
code: 'ETIMEDOUT',
2049+
}) as NodeJS.ErrnoException
20402050
const msg = enrichErrorMessage('http://example.com', 'GET', err)
20412051
expect(msg).toContain('Connection timed out')
20422052
})
20432053

20442054
it('should enrich ECONNRESET', () => {
2045-
const err = Object.assign(new Error('reset'), { code: 'ECONNRESET' }) as NodeJS.ErrnoException
2055+
const err = Object.assign(new Error('reset'), {
2056+
code: 'ECONNRESET',
2057+
}) as NodeJS.ErrnoException
20462058
const msg = enrichErrorMessage('http://example.com', 'GET', err)
20472059
expect(msg).toContain('Connection reset')
20482060
})
20492061

20502062
it('should enrich EPIPE', () => {
2051-
const err = Object.assign(new Error('broken pipe'), { code: 'EPIPE' }) as NodeJS.ErrnoException
2063+
const err = Object.assign(new Error('broken pipe'), {
2064+
code: 'EPIPE',
2065+
}) as NodeJS.ErrnoException
20522066
const msg = enrichErrorMessage('http://example.com', 'PUT', err)
20532067
expect(msg).toContain('Broken pipe')
20542068
expect(msg).toContain('PUT request failed')
20552069
})
20562070

20572071
it('should enrich CERT_HAS_EXPIRED', () => {
2058-
const err = Object.assign(new Error('cert expired'), { code: 'CERT_HAS_EXPIRED' }) as NodeJS.ErrnoException
2072+
const err = Object.assign(new Error('cert expired'), {
2073+
code: 'CERT_HAS_EXPIRED',
2074+
}) as NodeJS.ErrnoException
20592075
const msg = enrichErrorMessage('https://expired.example.com', 'GET', err)
20602076
expect(msg).toContain('SSL/TLS certificate error')
20612077
})
20622078

20632079
it('should enrich UNABLE_TO_VERIFY_LEAF_SIGNATURE', () => {
2064-
const err = Object.assign(new Error('leaf sig'), { code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' }) as NodeJS.ErrnoException
2080+
const err = Object.assign(new Error('leaf sig'), {
2081+
code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
2082+
}) as NodeJS.ErrnoException
20652083
const msg = enrichErrorMessage('https://badcert.example.com', 'GET', err)
20662084
expect(msg).toContain('SSL/TLS certificate error')
20672085
})
20682086

20692087
it('should include error code for unknown codes', () => {
2070-
const err = Object.assign(new Error('something'), { code: 'ESOMETHING' }) as NodeJS.ErrnoException
2088+
const err = Object.assign(new Error('something'), {
2089+
code: 'ESOMETHING',
2090+
}) as NodeJS.ErrnoException
20712091
const msg = enrichErrorMessage('http://example.com', 'DELETE', err)
20722092
expect(msg).toContain('Error code: ESOMETHING')
20732093
expect(msg).toContain('DELETE request failed')
@@ -2079,5 +2099,151 @@ abc123def456789012345678901234567890123456789012345678901234abcd
20792099
expect(msg).toContain('GET request failed')
20802100
expect(msg).not.toContain('Error code:')
20812101
})
2102+
2103+
it('should include url in the message', () => {
2104+
const err = Object.assign(new Error('fail'), { code: 'ECONNREFUSED' }) as NodeJS.ErrnoException
2105+
const msg = enrichErrorMessage('http://my-server:8080/api', 'GET', err)
2106+
expect(msg).toContain('http://my-server:8080/api')
2107+
})
2108+
})
2109+
2110+
describe('hooks — edge cases', () => {
2111+
it('should work with only onRequest (no onResponse)', async () => {
2112+
const infos: HttpHookRequestInfo[] = []
2113+
await httpRequest(`${httpBaseUrl}/json`, {
2114+
hooks: { onRequest: info => infos.push(info) },
2115+
})
2116+
expect(infos).toHaveLength(1)
2117+
})
2118+
2119+
it('should work with only onResponse (no onRequest)', async () => {
2120+
const infos: HttpHookResponseInfo[] = []
2121+
await httpRequest(`${httpBaseUrl}/json`, {
2122+
hooks: { onResponse: info => infos.push(info) },
2123+
})
2124+
expect(infos).toHaveLength(1)
2125+
expect(infos[0]!.status).toBe(200)
2126+
})
2127+
2128+
it('should work with empty hooks object', async () => {
2129+
const response = await httpRequest(`${httpBaseUrl}/json`, { hooks: {} })
2130+
expect(response.ok).toBe(true)
2131+
})
2132+
2133+
it('should pass hooks through httpJson', async () => {
2134+
const infos: HttpHookResponseInfo[] = []
2135+
await httpJson(`${httpBaseUrl}/json`, {
2136+
hooks: { onResponse: info => infos.push(info) },
2137+
})
2138+
expect(infos).toHaveLength(1)
2139+
expect(infos[0]!.status).toBe(200)
2140+
})
2141+
2142+
it('should pass hooks through httpText', async () => {
2143+
const infos: HttpHookResponseInfo[] = []
2144+
await httpText(`${httpBaseUrl}/text`, {
2145+
hooks: { onResponse: info => infos.push(info) },
2146+
})
2147+
expect(infos).toHaveLength(1)
2148+
expect(infos[0]!.status).toBe(200)
2149+
})
2150+
2151+
it('should include response headers in onResponse', async () => {
2152+
const infos: HttpHookResponseInfo[] = []
2153+
await httpRequest(`${httpBaseUrl}/json`, {
2154+
hooks: { onResponse: info => infos.push(info) },
2155+
})
2156+
expect(infos[0]!.headers).toBeDefined()
2157+
const ct = infos[0]!.headers?.['content-type']
2158+
expect(ct).toContain('application/json')
2159+
})
2160+
2161+
it('should report non-zero duration in onResponse', async () => {
2162+
const infos: HttpHookResponseInfo[] = []
2163+
await httpRequest(`${httpBaseUrl}/slow`, {
2164+
hooks: { onResponse: info => infos.push(info) },
2165+
})
2166+
expect(infos[0]!.duration).toBeGreaterThanOrEqual(0)
2167+
})
2168+
})
2169+
2170+
describe('maxResponseSize — edge cases', () => {
2171+
it('should allow response exactly at maxResponseSize', async () => {
2172+
// /json body is small (<1000 bytes); set limit to its exact size
2173+
const probe = await httpRequest(`${httpBaseUrl}/json`)
2174+
const exactSize = probe.body.length
2175+
2176+
const response = await httpRequest(`${httpBaseUrl}/json`, {
2177+
maxResponseSize: exactSize,
2178+
})
2179+
expect(response.ok).toBe(true)
2180+
expect(response.body.length).toBe(exactSize)
2181+
})
2182+
2183+
it('should reject when maxResponseSize is 0', async () => {
2184+
// 0 is falsy so should be treated as "no limit"
2185+
const response = await httpRequest(`${httpBaseUrl}/json`, {
2186+
maxResponseSize: 0,
2187+
})
2188+
expect(response.ok).toBe(true)
2189+
})
2190+
2191+
it('should enforce maxResponseSize on redirected response', async () => {
2192+
// /redirect -> /text (19 bytes "Plain text response")
2193+
await expect(
2194+
httpRequest(`${httpBaseUrl}/redirect`, {
2195+
maxResponseSize: 5,
2196+
}),
2197+
).rejects.toThrow(/exceeds maximum size limit/)
2198+
})
2199+
})
2200+
2201+
describe('rawResponse — edge cases', () => {
2202+
it('should have rawResponse after redirect', async () => {
2203+
const response = await httpRequest(`${httpBaseUrl}/redirect`)
2204+
expect(response.rawResponse).toBeDefined()
2205+
// rawResponse should be from the final response, not the redirect
2206+
expect(response.rawResponse!.statusCode).toBe(200)
2207+
})
2208+
2209+
it('should have rawResponse on server error', async () => {
2210+
const response = await httpRequest(`${httpBaseUrl}/server-error`)
2211+
expect(response.rawResponse).toBeDefined()
2212+
expect(response.rawResponse!.statusCode).toBe(500)
2213+
})
2214+
})
2215+
2216+
describe('enriched error messages — integration', () => {
2217+
it('should include method and url in timeout errors', async () => {
2218+
try {
2219+
await httpRequest(`${httpBaseUrl}/timeout`, { timeout: 50 })
2220+
expect.unreachable('should have thrown')
2221+
} catch (e) {
2222+
const msg = (e as Error).message
2223+
expect(msg).toContain('GET')
2224+
expect(msg).toContain('timed out')
2225+
expect(msg).toContain(`${httpBaseUrl}/timeout`)
2226+
}
2227+
})
2228+
2229+
it('should include method and url in connection errors', async () => {
2230+
try {
2231+
await httpRequest('http://localhost:1/no-server', { timeout: 100 })
2232+
expect.unreachable('should have thrown')
2233+
} catch (e) {
2234+
const msg = (e as Error).message
2235+
expect(msg).toContain('request failed')
2236+
expect(msg).toContain('localhost:1')
2237+
}
2238+
})
2239+
2240+
it('should preserve cause chain on network errors', async () => {
2241+
try {
2242+
await httpRequest('http://localhost:1/no-server', { timeout: 100 })
2243+
expect.unreachable('should have thrown')
2244+
} catch (e) {
2245+
expect((e as Error).cause).toBeDefined()
2246+
}
2247+
})
20822248
})
20832249
})

0 commit comments

Comments
 (0)