forked from
anil.recoil.org/ocaml-requests
A batteries included HTTP/1.1 client in OCaml
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** Tests for Error module *)
7
8module Error = Requests.Error
9
10(** {1 is_timeout Tests} *)
11
12let test_is_timeout_true () =
13 let e = Error.Timeout { operation = "connect"; duration = Some 30.0 } in
14 Alcotest.(check bool) "timeout is_timeout" true (Error.is_timeout e)
15
16let test_is_timeout_false () =
17 let e = Error.Dns_resolution_failed { hostname = "example.com" } in
18 Alcotest.(check bool) "dns is not timeout" false (Error.is_timeout e)
19
20(** {1 is_dns Tests} *)
21
22let test_is_dns_true () =
23 let e = Error.Dns_resolution_failed { hostname = "example.com" } in
24 Alcotest.(check bool) "dns is_dns" true (Error.is_dns e)
25
26let test_is_dns_false () =
27 let e = Error.Timeout { operation = "read"; duration = None } in
28 Alcotest.(check bool) "timeout is not dns" false (Error.is_dns e)
29
30(** {1 is_retryable Tests} *)
31
32let test_is_retryable_timeout () =
33 let e = Error.Timeout { operation = "connect"; duration = Some 5.0 } in
34 Alcotest.(check bool) "timeout is retryable" true (Error.is_retryable e)
35
36let test_is_retryable_dns () =
37 let e = Error.Dns_resolution_failed { hostname = "example.com" } in
38 Alcotest.(check bool) "dns is retryable" true (Error.is_retryable e)
39
40let test_is_retryable_503 () =
41 let e =
42 Error.Http_error
43 {
44 url = "https://example.com";
45 status = 503;
46 reason = "Service Unavailable";
47 body_preview = None;
48 headers = [];
49 }
50 in
51 Alcotest.(check bool) "503 is retryable" true (Error.is_retryable e)
52
53let test_is_retryable_502 () =
54 let e =
55 Error.Http_error
56 {
57 url = "https://example.com";
58 status = 502;
59 reason = "Bad Gateway";
60 body_preview = None;
61 headers = [];
62 }
63 in
64 Alcotest.(check bool) "502 is retryable" true (Error.is_retryable e)
65
66let test_is_retryable_429 () =
67 let e =
68 Error.Http_error
69 {
70 url = "https://example.com";
71 status = 429;
72 reason = "Too Many Requests";
73 body_preview = None;
74 headers = [];
75 }
76 in
77 Alcotest.(check bool) "429 is retryable" true (Error.is_retryable e)
78
79let test_is_retryable_connection () =
80 let e =
81 Error.Tcp_connect_failed
82 { host = "example.com"; port = 443; reason = "refused" }
83 in
84 Alcotest.(check bool)
85 "tcp connect failed is retryable" true (Error.is_retryable e)
86
87let test_is_retryable_404_false () =
88 let e =
89 Error.Http_error
90 {
91 url = "https://example.com";
92 status = 404;
93 reason = "Not Found";
94 body_preview = None;
95 headers = [];
96 }
97 in
98 Alcotest.(check bool) "404 is not retryable" false (Error.is_retryable e)
99
100(** {1 is_client_error Tests} *)
101
102let test_is_client_error_400 () =
103 let e =
104 Error.Http_error
105 {
106 url = "https://example.com";
107 status = 400;
108 reason = "Bad Request";
109 body_preview = None;
110 headers = [];
111 }
112 in
113 Alcotest.(check bool) "400 is client error" true (Error.is_client_error e)
114
115let test_is_client_error_404 () =
116 let e =
117 Error.Http_error
118 {
119 url = "https://example.com";
120 status = 404;
121 reason = "Not Found";
122 body_preview = None;
123 headers = [];
124 }
125 in
126 Alcotest.(check bool) "404 is client error" true (Error.is_client_error e)
127
128let test_is_client_error_499 () =
129 let e =
130 Error.Http_error
131 {
132 url = "https://example.com";
133 status = 499;
134 reason = "Client Closed Request";
135 body_preview = None;
136 headers = [];
137 }
138 in
139 Alcotest.(check bool) "499 is client error" true (Error.is_client_error e)
140
141let test_client_error_500_false () =
142 let e =
143 Error.Http_error
144 {
145 url = "https://example.com";
146 status = 500;
147 reason = "Internal Server Error";
148 body_preview = None;
149 headers = [];
150 }
151 in
152 Alcotest.(check bool)
153 "500 is not client error" false (Error.is_client_error e)
154
155(** {1 is_server_error Tests} *)
156
157let test_is_server_error_500 () =
158 let e =
159 Error.Http_error
160 {
161 url = "https://example.com";
162 status = 500;
163 reason = "Internal Server Error";
164 body_preview = None;
165 headers = [];
166 }
167 in
168 Alcotest.(check bool) "500 is server error" true (Error.is_server_error e)
169
170let test_is_server_error_503 () =
171 let e =
172 Error.Http_error
173 {
174 url = "https://example.com";
175 status = 503;
176 reason = "Service Unavailable";
177 body_preview = None;
178 headers = [];
179 }
180 in
181 Alcotest.(check bool) "503 is server error" true (Error.is_server_error e)
182
183let test_server_error_400_false () =
184 let e =
185 Error.Http_error
186 {
187 url = "https://example.com";
188 status = 400;
189 reason = "Bad Request";
190 body_preview = None;
191 headers = [];
192 }
193 in
194 Alcotest.(check bool)
195 "400 is not server error" false (Error.is_server_error e)
196
197(** {1 is_security_error Tests} *)
198
199let test_is_security_error_tls () =
200 let e =
201 Error.Tls_handshake_failed { host = "example.com"; reason = "cert expired" }
202 in
203 Alcotest.(check bool)
204 "tls is not security error" false
205 (Error.is_security_error e)
206
207let test_security_error_body_large () =
208 let e = Error.Body_too_large { limit = 1048576L; actual = Some 2097152L } in
209 Alcotest.(check bool)
210 "body_too_large is security error" true
211 (Error.is_security_error e)
212
213let test_security_decompression_bomb () =
214 let e = Error.Decompression_bomb { limit = 10485760L; ratio = 100.0 } in
215 Alcotest.(check bool)
216 "decompression_bomb is security error" true
217 (Error.is_security_error e)
218
219let test_security_invalid_header () =
220 let e = Error.Invalid_header { name = "Host"; reason = "contains newline" } in
221 Alcotest.(check bool)
222 "invalid_header is security error" true
223 (Error.is_security_error e)
224
225let test_security_timeout_false () =
226 let e = Error.Timeout { operation = "read"; duration = None } in
227 Alcotest.(check bool)
228 "timeout is not security error" false
229 (Error.is_security_error e)
230
231(** {1 sanitize_url Tests} *)
232
233let test_sanitize_url_no_credentials () =
234 let url = "https://example.com/path" in
235 let sanitized = Error.sanitize_url url in
236 Alcotest.(check string) "no change" "https://example.com/path" sanitized
237
238let has_substring s sub =
239 let len_s = String.length s in
240 let len_sub = String.length sub in
241 if len_sub > len_s then false
242 else
243 let found = ref false in
244 for i = 0 to len_s - len_sub do
245 if String.sub s i len_sub = sub then found := true
246 done;
247 !found
248
249let test_sanitize_url_with_userinfo () =
250 let url = "https://user:pass@example.com/path" in
251 let sanitized = Error.sanitize_url url in
252 Alcotest.(check bool)
253 "no user:pass" false
254 (has_substring sanitized "user:pass")
255
256let test_sanitize_url_user_only () =
257 let url = "https://user@example.com/path" in
258 let sanitized = Error.sanitize_url url in
259 Alcotest.(check bool) "no user@" false (has_substring sanitized "user@")
260
261(** {1 is_sensitive_header Tests} *)
262
263let test_is_sensitive_authorization () =
264 Alcotest.(check bool)
265 "Authorization is sensitive" true
266 (Error.is_sensitive_header "Authorization")
267
268let test_is_sensitive_cookie () =
269 Alcotest.(check bool)
270 "Cookie is sensitive" true
271 (Error.is_sensitive_header "Cookie")
272
273let test_is_sensitive_set_cookie () =
274 Alcotest.(check bool)
275 "Set-Cookie is sensitive" true
276 (Error.is_sensitive_header "Set-Cookie")
277
278let test_is_sensitive_case_insensitive () =
279 Alcotest.(check bool)
280 "authorization (lowercase) is sensitive" true
281 (Error.is_sensitive_header "authorization")
282
283let test_sensitive_content_type_false () =
284 Alcotest.(check bool)
285 "Content-Type is not sensitive" false
286 (Error.is_sensitive_header "Content-Type")
287
288(** {1 Error Constructor Tests} *)
289
290let test_err_creates_eio_exn () =
291 let exn =
292 Error.err (Error.Timeout { operation = "connect"; duration = Some 5.0 })
293 in
294 match exn with
295 | Eio.Io (Error.E (Timeout _), _) -> Alcotest.(check pass) "is Eio.Io" () ()
296 | _ -> Alcotest.fail "Expected Eio.Io with Timeout"
297
298let test_of_eio_exn () =
299 let exn =
300 Error.err (Error.Timeout { operation = "connect"; duration = Some 5.0 })
301 in
302 match Error.of_eio_exn exn with
303 | Some (Error.Timeout { operation; _ }) ->
304 Alcotest.(check string) "operation" "connect" operation
305 | _ -> Alcotest.fail "Expected Some Timeout"
306
307let test_to_string () =
308 let e = Error.Timeout { operation = "connect"; duration = Some 5.0 } in
309 let s = Error.to_string e in
310 Alcotest.(check bool) "contains operation" true (String.length s > 0)
311
312(** {1 is_tls Tests} *)
313
314let test_is_tls_true () =
315 let e =
316 Error.Tls_handshake_failed { host = "example.com"; reason = "cert invalid" }
317 in
318 Alcotest.(check bool) "tls handshake is_tls" true (Error.is_tls e)
319
320let test_is_tls_false () =
321 let e = Error.Dns_resolution_failed { hostname = "example.com" } in
322 Alcotest.(check bool) "dns is not tls" false (Error.is_tls e)
323
324(** {1 is_connection Tests} *)
325
326let test_is_connection_dns () =
327 let e = Error.Dns_resolution_failed { hostname = "example.com" } in
328 Alcotest.(check bool) "dns is connection" true (Error.is_connection e)
329
330let test_is_connection_tcp () =
331 let e =
332 Error.Tcp_connect_failed
333 { host = "example.com"; port = 443; reason = "refused" }
334 in
335 Alcotest.(check bool) "tcp is connection" true (Error.is_connection e)
336
337let test_is_connection_tls () =
338 let e =
339 Error.Tls_handshake_failed { host = "example.com"; reason = "cert invalid" }
340 in
341 Alcotest.(check bool) "tls is connection" true (Error.is_connection e)
342
343let test_is_connection_timeout_false () =
344 let e = Error.Timeout { operation = "read"; duration = None } in
345 Alcotest.(check bool)
346 "timeout is not connection" false (Error.is_connection e)
347
348(** {1 HTTP Status Helpers} *)
349
350let test_get_http_status () =
351 let e =
352 Error.Http_error
353 {
354 url = "https://example.com";
355 status = 404;
356 reason = "Not Found";
357 body_preview = None;
358 headers = [];
359 }
360 in
361 Alcotest.(check (option int)) "status" (Some 404) (Error.http_status e)
362
363let test_get_http_status_none () =
364 let e = Error.Timeout { operation = "read"; duration = None } in
365 Alcotest.(check (option int)) "no status" None (Error.http_status e)
366
367let test_get_url () =
368 let e =
369 Error.Http_error
370 {
371 url = "https://example.com/path";
372 status = 500;
373 reason = "ISE";
374 body_preview = None;
375 headers = [];
376 }
377 in
378 Alcotest.(check (option string))
379 "url" (Some "https://example.com/path") (Error.url e)
380
381(** {1 Test Suite} *)
382
383let suite =
384 ( "error",
385 [
386 Alcotest.test_case "true for Timeout" `Quick test_is_timeout_true;
387 Alcotest.test_case "false for DNS" `Quick test_is_timeout_false;
388 Alcotest.test_case "true for DNS" `Quick test_is_dns_true;
389 Alcotest.test_case "false for Timeout" `Quick test_is_dns_false;
390 Alcotest.test_case "timeout" `Quick test_is_retryable_timeout;
391 Alcotest.test_case "dns" `Quick test_is_retryable_dns;
392 Alcotest.test_case "503" `Quick test_is_retryable_503;
393 Alcotest.test_case "502" `Quick test_is_retryable_502;
394 Alcotest.test_case "429" `Quick test_is_retryable_429;
395 Alcotest.test_case "connection" `Quick test_is_retryable_connection;
396 Alcotest.test_case "404 not retryable" `Quick test_is_retryable_404_false;
397 Alcotest.test_case "400" `Quick test_is_client_error_400;
398 Alcotest.test_case "404" `Quick test_is_client_error_404;
399 Alcotest.test_case "499" `Quick test_is_client_error_499;
400 Alcotest.test_case "500 not client" `Quick test_client_error_500_false;
401 Alcotest.test_case "500" `Quick test_is_server_error_500;
402 Alcotest.test_case "503" `Quick test_is_server_error_503;
403 Alcotest.test_case "400 not server" `Quick test_server_error_400_false;
404 Alcotest.test_case "TLS" `Quick test_is_security_error_tls;
405 Alcotest.test_case "body too large" `Quick test_security_error_body_large;
406 Alcotest.test_case "decompression bomb" `Quick
407 test_security_decompression_bomb;
408 Alcotest.test_case "invalid header" `Quick test_security_invalid_header;
409 Alcotest.test_case "timeout not security" `Quick
410 test_security_timeout_false;
411 Alcotest.test_case "no credentials" `Quick
412 test_sanitize_url_no_credentials;
413 Alcotest.test_case "with userinfo" `Quick test_sanitize_url_with_userinfo;
414 Alcotest.test_case "with user only" `Quick test_sanitize_url_user_only;
415 Alcotest.test_case "Authorization" `Quick test_is_sensitive_authorization;
416 Alcotest.test_case "Cookie" `Quick test_is_sensitive_cookie;
417 Alcotest.test_case "Set-Cookie" `Quick test_is_sensitive_set_cookie;
418 Alcotest.test_case "case insensitive" `Quick
419 test_is_sensitive_case_insensitive;
420 Alcotest.test_case "Content-Type not sensitive" `Quick
421 test_sensitive_content_type_false;
422 Alcotest.test_case "err creates Eio.Io" `Quick test_err_creates_eio_exn;
423 Alcotest.test_case "of_eio_exn" `Quick test_of_eio_exn;
424 Alcotest.test_case "to_string" `Quick test_to_string;
425 Alcotest.test_case "true for TLS" `Quick test_is_tls_true;
426 Alcotest.test_case "false for DNS" `Quick test_is_tls_false;
427 Alcotest.test_case "DNS" `Quick test_is_connection_dns;
428 Alcotest.test_case "TCP" `Quick test_is_connection_tcp;
429 Alcotest.test_case "TLS" `Quick test_is_connection_tls;
430 Alcotest.test_case "timeout not connection" `Quick
431 test_is_connection_timeout_false;
432 Alcotest.test_case "http_status" `Quick test_get_http_status;
433 Alcotest.test_case "http_status none" `Quick test_get_http_status_none;
434 Alcotest.test_case "url" `Quick test_get_url;
435 ] )