A batteries included HTTP/1.1 client in OCaml
at main 435 lines 14 kB view raw
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 ] )