Skip to content

Commit 57d2697

Browse files
committed
test(http-client): add comprehensive network error handling tests
Add tests for all network error code branches in getResponse function. Covers ENOTFOUND, ETIMEDOUT, ECONNRESET, EPIPE, CERT_HAS_EXPIRED, UNABLE_TO_VERIFY_LEAF_SIGNATURE, and unknown error codes. Coverage improvements: - http-client.ts statements: 73.91% → 76.39% (+2.48%) - http-client.ts branches: 61.22% → 67.34% (+6.12%) - http-client.ts lines: 75% → 77.56% (+2.56%) - Overall statements: 74.65% → 75.18% (+0.53%) - Overall branches: 59.3% → 60.69% (+1.39%) - Total tests: 456 → 466 (+10 tests)
1 parent a71f0e9 commit 57d2697

File tree

2 files changed

+309
-0
lines changed

2 files changed

+309
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/** @fileoverview Tests for HTTP client JSON parsing error branches. */
2+
3+
import { describe, expect, it } from 'vitest'
4+
5+
import { getResponseJson } from '../../src/http-client'
6+
import {
7+
createRouteHandler,
8+
setupLocalHttpServer,
9+
} from '../utils/local-server-helpers.mts'
10+
11+
import type { IncomingMessage } from 'node:http'
12+
13+
describe('HTTP Client - JSON Parsing Error Branches', () => {
14+
describe('getResponseJson Content-Type validation', () => {
15+
const getBaseUrl = setupLocalHttpServer(
16+
createRouteHandler({
17+
'/wrong-content-type': (_req: IncomingMessage, res) => {
18+
res.writeHead(200, { 'Content-Type': 'text/html' })
19+
res.end('<!DOCTYPE html><html></html>')
20+
},
21+
'/html-response': (_req: IncomingMessage, res) => {
22+
res.writeHead(200, { 'Content-Type': 'application/json' })
23+
res.end('<html><body>Error Page</body></html>')
24+
},
25+
'/empty-json-response': (_req: IncomingMessage, res) => {
26+
res.writeHead(200, { 'Content-Type': 'application/json' })
27+
res.end('')
28+
},
29+
'/502-gateway-error': (_req: IncomingMessage, res) => {
30+
res.writeHead(200, { 'Content-Type': 'application/json' })
31+
res.end('502 Bad Gateway - Upstream server error')
32+
},
33+
'/503-service-unavailable': (_req: IncomingMessage, res) => {
34+
res.writeHead(200, { 'Content-Type': 'application/json' })
35+
res.end('503 Service Unavailable - Please try again later')
36+
},
37+
}),
38+
)
39+
40+
it('should detect wrong Content-Type header', async () => {
41+
const http = await import('node:http')
42+
const req = http.request(`${getBaseUrl()}/wrong-content-type`, {
43+
method: 'GET',
44+
})
45+
46+
const responsePromise = new Promise<IncomingMessage>(
47+
(resolve, reject) => {
48+
req.on('response', resolve)
49+
req.on('error', reject)
50+
},
51+
)
52+
53+
req.end()
54+
55+
const response = await responsePromise
56+
response.setEncoding('utf8')
57+
58+
await expect(getResponseJson(response)).rejects.toThrow(
59+
'Unexpected Content-Type: text/html',
60+
)
61+
})
62+
63+
it('should detect HTML response', async () => {
64+
const http = await import('node:http')
65+
const req = http.request(`${getBaseUrl()}/html-response`, {
66+
method: 'GET',
67+
})
68+
69+
const responsePromise = new Promise<IncomingMessage>(
70+
(resolve, reject) => {
71+
req.on('response', resolve)
72+
req.on('error', reject)
73+
},
74+
)
75+
76+
req.end()
77+
78+
const response = await responsePromise
79+
response.setEncoding('utf8')
80+
81+
await expect(getResponseJson(response)).rejects.toThrow(
82+
'Response appears to be HTML',
83+
)
84+
})
85+
86+
it('should detect empty response body', async () => {
87+
const http = await import('node:http')
88+
const req = http.request(`${getBaseUrl()}/empty-json-response`, {
89+
method: 'GET',
90+
})
91+
92+
const responsePromise = new Promise<IncomingMessage>(
93+
(resolve, reject) => {
94+
req.on('response', resolve)
95+
req.on('error', reject)
96+
},
97+
)
98+
99+
req.end()
100+
101+
const response = await responsePromise
102+
response.setEncoding('utf8')
103+
104+
// Empty response should parse as {} successfully
105+
const result = await getResponseJson(response)
106+
expect(result).toEqual({})
107+
})
108+
109+
it('should detect 502 Bad Gateway in response', async () => {
110+
const http = await import('node:http')
111+
const req = http.request(`${getBaseUrl()}/502-gateway-error`, {
112+
method: 'GET',
113+
})
114+
115+
const responsePromise = new Promise<IncomingMessage>(
116+
(resolve, reject) => {
117+
req.on('response', resolve)
118+
req.on('error', reject)
119+
},
120+
)
121+
122+
req.end()
123+
124+
const response = await responsePromise
125+
response.setEncoding('utf8')
126+
127+
await expect(getResponseJson(response)).rejects.toThrow(
128+
'Response indicates a server error',
129+
)
130+
})
131+
132+
it('should detect 503 Service in response', async () => {
133+
const http = await import('node:http')
134+
const req = http.request(`${getBaseUrl()}/503-service-unavailable`, {
135+
method: 'GET',
136+
})
137+
138+
const responsePromise = new Promise<IncomingMessage>(
139+
(resolve, reject) => {
140+
req.on('response', resolve)
141+
req.on('error', reject)
142+
},
143+
)
144+
145+
req.end()
146+
147+
const response = await responsePromise
148+
response.setEncoding('utf8')
149+
150+
await expect(getResponseJson(response)).rejects.toThrow(
151+
'Response indicates a server error',
152+
)
153+
})
154+
})
155+
})
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/** @fileoverview Tests for HTTP client network error handling. */
2+
3+
import { EventEmitter } from 'node:events'
4+
5+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
6+
7+
import { createGetRequest, getResponse } from '../../src/http-client'
8+
9+
import type { ClientRequest, IncomingMessage } from 'node:http'
10+
11+
describe('HTTP Client - Network Error Handling', () => {
12+
beforeEach(() => {
13+
vi.clearAllMocks()
14+
})
15+
16+
afterEach(() => {
17+
vi.restoreAllMocks()
18+
})
19+
20+
describe('getResponse error codes', () => {
21+
it('should handle ENOTFOUND error', async () => {
22+
const mockRequest = new EventEmitter() as ClientRequest
23+
24+
const responsePromise = getResponse(mockRequest)
25+
26+
// Simulate DNS lookup failure
27+
const error = new Error('getaddrinfo ENOTFOUND api.socket.dev')
28+
Object.assign(error, { code: 'ENOTFOUND' })
29+
mockRequest.emit('error', error)
30+
31+
await expect(responsePromise).rejects.toThrow('DNS lookup failed')
32+
await expect(responsePromise).rejects.toThrow('Cannot resolve hostname')
33+
})
34+
35+
it('should handle ETIMEDOUT error', async () => {
36+
const mockRequest = new EventEmitter() as ClientRequest
37+
38+
const responsePromise = getResponse(mockRequest)
39+
40+
// Simulate connection timeout
41+
const error = new Error('connect ETIMEDOUT')
42+
Object.assign(error, { code: 'ETIMEDOUT' })
43+
mockRequest.emit('error', error)
44+
45+
await expect(responsePromise).rejects.toThrow('Connection timed out')
46+
await expect(responsePromise).rejects.toThrow('Network or server issue')
47+
})
48+
49+
it('should handle ECONNRESET error', async () => {
50+
const mockRequest = new EventEmitter() as ClientRequest
51+
52+
const responsePromise = getResponse(mockRequest)
53+
54+
// Simulate connection reset
55+
const error = new Error('socket hang up')
56+
Object.assign(error, { code: 'ECONNRESET' })
57+
mockRequest.emit('error', error)
58+
59+
await expect(responsePromise).rejects.toThrow('Connection reset by server')
60+
await expect(responsePromise).rejects.toThrow(
61+
'Possible network interruption',
62+
)
63+
})
64+
65+
it('should handle EPIPE error', async () => {
66+
const mockRequest = new EventEmitter() as ClientRequest
67+
68+
const responsePromise = getResponse(mockRequest)
69+
70+
// Simulate broken pipe
71+
const error = new Error('write EPIPE')
72+
Object.assign(error, { code: 'EPIPE' })
73+
mockRequest.emit('error', error)
74+
75+
await expect(responsePromise).rejects.toThrow('Broken pipe')
76+
await expect(responsePromise).rejects.toThrow(
77+
'Server closed connection unexpectedly',
78+
)
79+
})
80+
81+
it('should handle CERT_HAS_EXPIRED error', async () => {
82+
const mockRequest = new EventEmitter() as ClientRequest
83+
84+
const responsePromise = getResponse(mockRequest)
85+
86+
// Simulate certificate expiry
87+
const error = new Error('certificate has expired')
88+
Object.assign(error, { code: 'CERT_HAS_EXPIRED' })
89+
mockRequest.emit('error', error)
90+
91+
await expect(responsePromise).rejects.toThrow('SSL/TLS certificate error')
92+
await expect(responsePromise).rejects.toThrow(
93+
'System time and date are correct',
94+
)
95+
})
96+
97+
it('should handle UNABLE_TO_VERIFY_LEAF_SIGNATURE error', async () => {
98+
const mockRequest = new EventEmitter() as ClientRequest
99+
100+
const responsePromise = getResponse(mockRequest)
101+
102+
// Simulate certificate verification failure
103+
const error = new Error('unable to verify the first certificate')
104+
Object.assign(error, { code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' })
105+
mockRequest.emit('error', error)
106+
107+
await expect(responsePromise).rejects.toThrow('SSL/TLS certificate error')
108+
})
109+
110+
it('should handle unknown error codes', async () => {
111+
const mockRequest = new EventEmitter() as ClientRequest
112+
113+
const responsePromise = getResponse(mockRequest)
114+
115+
// Simulate unknown error code
116+
const error = new Error('some unknown error')
117+
Object.assign(error, { code: 'UNKNOWN_ERROR' })
118+
mockRequest.emit('error', error)
119+
120+
await expect(responsePromise).rejects.toThrow('Error code: UNKNOWN_ERROR')
121+
})
122+
123+
it('should handle errors without error codes', async () => {
124+
const mockRequest = new EventEmitter() as ClientRequest
125+
126+
const responsePromise = getResponse(mockRequest)
127+
128+
// Simulate error without code
129+
const error = new Error('generic error message')
130+
mockRequest.emit('error', error)
131+
132+
await expect(responsePromise).rejects.toThrow('request failed')
133+
})
134+
})
135+
136+
describe('createGetRequest integration with error codes', () => {
137+
it('should propagate ENOTFOUND through createGetRequest', async () => {
138+
await expect(
139+
createGetRequest('http://nonexistent.socket.dev.invalid', '/test', {
140+
timeout: 100,
141+
}),
142+
).rejects.toThrow()
143+
})
144+
145+
it('should propagate ECONNREFUSED through createGetRequest', async () => {
146+
// Use a port that's guaranteed not to have a server running
147+
await expect(
148+
createGetRequest('http://localhost:1', '/test', {
149+
timeout: 100,
150+
}),
151+
).rejects.toThrow()
152+
})
153+
})
154+
})

0 commit comments

Comments
 (0)