Skip to content

Commit 4369def

Browse files
majiayu000claude
andcommitted
test(http): add unit tests for timeout parsing logic
Add comprehensive unit tests to verify: - Numeric timeout parsing - String timeout parsing - MAX_TIMEOUT_MS cap (600000ms) - DEFAULT_TIMEOUT_MS fallback (120000ms) - Invalid value handling - AbortSignal.timeout integration - Timeout error identification 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 048ff62 commit 4369def

File tree

1 file changed

+222
-0
lines changed

1 file changed

+222
-0
lines changed

apps/sim/tools/timeout.test.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2+
3+
/**
4+
* Tests for timeout functionality in handleProxyRequest and handleInternalRequest
5+
*/
6+
describe('HTTP Timeout Support', () => {
7+
const originalFetch = global.fetch
8+
9+
beforeEach(() => {
10+
vi.useFakeTimers()
11+
})
12+
13+
afterEach(() => {
14+
global.fetch = originalFetch
15+
vi.useRealTimers()
16+
vi.restoreAllMocks()
17+
})
18+
19+
describe('Timeout Parameter Parsing', () => {
20+
it('should parse numeric timeout correctly', () => {
21+
const params = { timeout: 5000 }
22+
const DEFAULT_TIMEOUT_MS = 120000
23+
const MAX_TIMEOUT_MS = 600000
24+
25+
let timeoutMs = DEFAULT_TIMEOUT_MS
26+
if (typeof params.timeout === 'number' && params.timeout > 0) {
27+
timeoutMs = Math.min(params.timeout, MAX_TIMEOUT_MS)
28+
}
29+
30+
expect(timeoutMs).toBe(5000)
31+
})
32+
33+
it('should parse string timeout correctly', () => {
34+
const params = { timeout: '30000' }
35+
const DEFAULT_TIMEOUT_MS = 120000
36+
const MAX_TIMEOUT_MS = 600000
37+
38+
let timeoutMs = DEFAULT_TIMEOUT_MS
39+
if (typeof params.timeout === 'number' && params.timeout > 0) {
40+
timeoutMs = Math.min(params.timeout, MAX_TIMEOUT_MS)
41+
} else if (typeof params.timeout === 'string') {
42+
const parsed = Number.parseInt(params.timeout, 10)
43+
if (!Number.isNaN(parsed) && parsed > 0) {
44+
timeoutMs = Math.min(parsed, MAX_TIMEOUT_MS)
45+
}
46+
}
47+
48+
expect(timeoutMs).toBe(30000)
49+
})
50+
51+
it('should cap timeout at MAX_TIMEOUT_MS', () => {
52+
const params = { timeout: 1000000 } // 1000 seconds, exceeds max
53+
const DEFAULT_TIMEOUT_MS = 120000
54+
const MAX_TIMEOUT_MS = 600000
55+
56+
let timeoutMs = DEFAULT_TIMEOUT_MS
57+
if (typeof params.timeout === 'number' && params.timeout > 0) {
58+
timeoutMs = Math.min(params.timeout, MAX_TIMEOUT_MS)
59+
}
60+
61+
expect(timeoutMs).toBe(MAX_TIMEOUT_MS)
62+
})
63+
64+
it('should use default timeout when no timeout provided', () => {
65+
const params = {}
66+
const DEFAULT_TIMEOUT_MS = 120000
67+
const MAX_TIMEOUT_MS = 600000
68+
69+
let timeoutMs = DEFAULT_TIMEOUT_MS
70+
if (typeof (params as any).timeout === 'number' && (params as any).timeout > 0) {
71+
timeoutMs = Math.min((params as any).timeout, MAX_TIMEOUT_MS)
72+
}
73+
74+
expect(timeoutMs).toBe(DEFAULT_TIMEOUT_MS)
75+
})
76+
77+
it('should use default timeout for invalid string', () => {
78+
const params = { timeout: 'invalid' }
79+
const DEFAULT_TIMEOUT_MS = 120000
80+
const MAX_TIMEOUT_MS = 600000
81+
82+
let timeoutMs = DEFAULT_TIMEOUT_MS
83+
if (typeof params.timeout === 'number' && params.timeout > 0) {
84+
timeoutMs = Math.min(params.timeout, MAX_TIMEOUT_MS)
85+
} else if (typeof params.timeout === 'string') {
86+
const parsed = Number.parseInt(params.timeout, 10)
87+
if (!Number.isNaN(parsed) && parsed > 0) {
88+
timeoutMs = Math.min(parsed, MAX_TIMEOUT_MS)
89+
}
90+
}
91+
92+
expect(timeoutMs).toBe(DEFAULT_TIMEOUT_MS)
93+
})
94+
95+
it('should use default timeout for zero or negative values', () => {
96+
const testCases = [{ timeout: 0 }, { timeout: -1000 }, { timeout: '0' }, { timeout: '-500' }]
97+
const DEFAULT_TIMEOUT_MS = 120000
98+
const MAX_TIMEOUT_MS = 600000
99+
100+
for (const params of testCases) {
101+
let timeoutMs = DEFAULT_TIMEOUT_MS
102+
if (typeof params.timeout === 'number' && params.timeout > 0) {
103+
timeoutMs = Math.min(params.timeout, MAX_TIMEOUT_MS)
104+
} else if (typeof params.timeout === 'string') {
105+
const parsed = Number.parseInt(params.timeout, 10)
106+
if (!Number.isNaN(parsed) && parsed > 0) {
107+
timeoutMs = Math.min(parsed, MAX_TIMEOUT_MS)
108+
}
109+
}
110+
111+
expect(timeoutMs).toBe(DEFAULT_TIMEOUT_MS)
112+
}
113+
})
114+
})
115+
116+
describe('AbortSignal.timeout Integration', () => {
117+
it('should create AbortSignal with correct timeout', () => {
118+
const timeoutMs = 5000
119+
const signal = AbortSignal.timeout(timeoutMs)
120+
121+
expect(signal).toBeDefined()
122+
expect(signal.aborted).toBe(false)
123+
})
124+
125+
it('should abort after timeout period', async () => {
126+
vi.useRealTimers() // Need real timers for this test
127+
128+
const timeoutMs = 100 // Very short timeout for testing
129+
const signal = AbortSignal.timeout(timeoutMs)
130+
131+
// Wait for timeout to trigger
132+
await new Promise((resolve) => setTimeout(resolve, timeoutMs + 50))
133+
134+
expect(signal.aborted).toBe(true)
135+
})
136+
})
137+
138+
describe('Timeout Error Handling', () => {
139+
it('should identify TimeoutError correctly', () => {
140+
const timeoutError = new Error('The operation was aborted')
141+
timeoutError.name = 'TimeoutError'
142+
143+
const isTimeoutError =
144+
timeoutError instanceof Error && timeoutError.name === 'TimeoutError'
145+
146+
expect(isTimeoutError).toBe(true)
147+
})
148+
149+
it('should generate user-friendly timeout message', () => {
150+
const timeoutMs = 5000
151+
const errorMessage = `Request timed out after ${timeoutMs}ms. Consider increasing the timeout value.`
152+
153+
expect(errorMessage).toBe(
154+
'Request timed out after 5000ms. Consider increasing the timeout value.'
155+
)
156+
})
157+
})
158+
159+
describe('Fetch with Timeout Signal', () => {
160+
it('should pass signal to fetch options', async () => {
161+
vi.useRealTimers()
162+
163+
const mockFetch = vi.fn().mockResolvedValue(
164+
new Response(JSON.stringify({ success: true }), {
165+
status: 200,
166+
headers: { 'Content-Type': 'application/json' },
167+
})
168+
)
169+
global.fetch = mockFetch
170+
171+
const timeoutMs = 5000
172+
await fetch('https://example.com/api', {
173+
method: 'POST',
174+
headers: { 'Content-Type': 'application/json' },
175+
body: JSON.stringify({ test: true }),
176+
signal: AbortSignal.timeout(timeoutMs),
177+
})
178+
179+
expect(mockFetch).toHaveBeenCalledWith(
180+
'https://example.com/api',
181+
expect.objectContaining({
182+
signal: expect.any(AbortSignal),
183+
})
184+
)
185+
})
186+
187+
it('should throw TimeoutError when request times out', async () => {
188+
vi.useRealTimers()
189+
190+
// Mock a slow fetch that will be aborted
191+
global.fetch = vi.fn().mockImplementation(
192+
(_url: string, options: RequestInit) =>
193+
new Promise((_resolve, reject) => {
194+
if (options?.signal) {
195+
options.signal.addEventListener('abort', () => {
196+
const error = new Error('The operation was aborted')
197+
error.name = 'TimeoutError'
198+
reject(error)
199+
})
200+
}
201+
})
202+
)
203+
204+
const timeoutMs = 100
205+
let caughtError: Error | null = null
206+
207+
try {
208+
await fetch('https://example.com/slow-api', {
209+
signal: AbortSignal.timeout(timeoutMs),
210+
})
211+
} catch (error) {
212+
caughtError = error as Error
213+
}
214+
215+
// Wait a bit for the timeout to trigger
216+
await new Promise((resolve) => setTimeout(resolve, timeoutMs + 50))
217+
218+
expect(caughtError).not.toBeNull()
219+
expect(caughtError?.name).toBe('TimeoutError')
220+
})
221+
})
222+
})

0 commit comments

Comments
 (0)