A batteries included HTTP/1.1 client in OCaml
at main 275 lines 9.7 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 Headers module *) 7 8module Headers = Requests.Headers 9module Header_name = Requests.Header_name 10 11(** {1 empty Tests} *) 12 13let test_empty_has_no_headers () = 14 let h = Headers.empty in 15 let l = Headers.to_list h in 16 Alcotest.(check int) "empty has no headers" 0 (List.length l) 17 18(** {1 of_list / to_list Tests} *) 19 20let test_of_list_roundtrip () = 21 let pairs = [ ("Content-Type", "text/html"); ("Host", "example.com") ] in 22 let h = Headers.of_list pairs in 23 let l = Headers.to_list h in 24 Alcotest.(check int) "two headers" 2 (List.length l) 25 26let test_of_list_preserves_values () = 27 let h = Headers.of_list [ ("Content-Type", "text/html") ] in 28 let v = Headers.find `Content_type h in 29 Alcotest.(check (option string)) "Content-Type value" (Some "text/html") v 30 31(** {1 add / get / remove Tests} *) 32 33let test_add_and_get () = 34 let h = Headers.empty |> Headers.add `Content_type "application/json" in 35 let v = Headers.find `Content_type h in 36 Alcotest.(check (option string)) "get after add" (Some "application/json") v 37 38let test_add_multiple () = 39 let h = 40 Headers.empty 41 |> Headers.add `Set_cookie "a=1" 42 |> Headers.add `Set_cookie "b=2" 43 in 44 let values = Headers.all `Set_cookie h in 45 Alcotest.(check int) "multiple values" 2 (List.length values) 46 47let test_get_missing () = 48 let h = Headers.empty in 49 let v = Headers.find `Content_type h in 50 Alcotest.(check (option string)) "missing header" None v 51 52let test_remove () = 53 let h = 54 Headers.empty 55 |> Headers.add `Content_type "text/html" 56 |> Headers.remove `Content_type 57 in 58 let v = Headers.find `Content_type h in 59 Alcotest.(check (option string)) "removed header" None v 60 61(** {1 set Tests} *) 62 63let test_set_replaces () = 64 let h = 65 Headers.empty 66 |> Headers.add `Content_type "text/html" 67 |> Headers.set `Content_type "application/json" 68 in 69 let v = Headers.find `Content_type h in 70 Alcotest.(check (option string)) "set replaces" (Some "application/json") v 71 72let test_set_replaces_all () = 73 let h = 74 Headers.empty 75 |> Headers.add `Set_cookie "a=1" 76 |> Headers.add `Set_cookie "b=2" 77 |> Headers.set `Set_cookie "c=3" 78 in 79 let values = Headers.all `Set_cookie h in 80 Alcotest.(check int) "set replaces all" 1 (List.length values); 81 Alcotest.(check string) "set value" "c=3" (List.hd values) 82 83(** {1 all Tests} *) 84 85let test_get_all_empty () = 86 let h = Headers.empty in 87 let values = Headers.all `Content_type h in 88 Alcotest.(check int) "all empty" 0 (List.length values) 89 90let test_get_all_multiple () = 91 let h = 92 Headers.empty 93 |> Headers.add `Set_cookie "session=abc" 94 |> Headers.add `Set_cookie "lang=en" 95 in 96 let values = Headers.all `Set_cookie h in 97 Alcotest.(check int) "all multiple" 2 (List.length values) 98 99(** {1 merge Tests} *) 100 101let test_merge_combines () = 102 let a = Headers.empty |> Headers.set `Content_type "text/html" in 103 let b = Headers.empty |> Headers.set `Host "example.com" in 104 let merged = Headers.merge a b in 105 Alcotest.(check (option string)) 106 "merged content-type" (Some "text/html") 107 (Headers.find `Content_type merged); 108 Alcotest.(check (option string)) 109 "merged host" (Some "example.com") 110 (Headers.find `Host merged) 111 112let test_merge_override () = 113 let a = Headers.empty |> Headers.set `Content_type "text/html" in 114 let b = Headers.empty |> Headers.set `Content_type "application/json" in 115 let merged = Headers.merge a b in 116 Alcotest.(check (option string)) 117 "override replaces" (Some "application/json") 118 (Headers.find `Content_type merged) 119 120(** {1 Convenience Constructor Tests} *) 121 122let test_content_type () = 123 let h = 124 Headers.empty |> Headers.content_type (Requests.Mime.of_string "text/html") 125 in 126 let v = Headers.find `Content_type h in 127 Alcotest.(check (option string)) "content_type" (Some "text/html") v 128 129let test_content_length () = 130 let h = Headers.empty |> Headers.content_length 42L in 131 let v = Headers.find `Content_length h in 132 Alcotest.(check (option string)) "content_length" (Some "42") v 133 134let test_host () = 135 let h = Headers.empty |> Headers.host "example.com" in 136 let v = Headers.find `Host h in 137 Alcotest.(check (option string)) "host" (Some "example.com") v 138 139(** {1 Authentication Tests} *) 140 141let test_bearer () = 142 let h = Headers.empty |> Headers.bearer "token123" in 143 let v = Headers.find `Authorization h in 144 Alcotest.(check (option string)) "bearer" (Some "Bearer token123") v 145 146let test_basic () = 147 let h = Headers.empty |> Headers.basic ~username:"user" ~password:"pass" in 148 let v = Headers.find `Authorization h in 149 match v with 150 | Some auth -> 151 Alcotest.(check bool) 152 "starts with Basic" true 153 (String.length auth > 6 && String.sub auth 0 6 = "Basic ") 154 | None -> Alcotest.fail "Expected Authorization header" 155 156(** {1 Connection Header Tests} *) 157 158let test_connection_close () = 159 let h = Headers.of_list [ ("Connection", "close") ] in 160 Alcotest.(check bool) "connection_close" true (Headers.connection_close h) 161 162let test_connection_close_false () = 163 let h = Headers.of_list [ ("Connection", "keep-alive") ] in 164 Alcotest.(check bool) 165 "not connection_close" false 166 (Headers.connection_close h) 167 168let test_connection_keep_alive () = 169 let h = Headers.of_list [ ("Connection", "keep-alive") ] in 170 Alcotest.(check bool) 171 "connection_keep_alive" true 172 (Headers.connection_keep_alive h) 173 174let test_parse_connection_header () = 175 let h = Headers.of_list [ ("Connection", "close, X-Custom") ] in 176 let names = Headers.parse_connection_header h in 177 Alcotest.(check bool) "has entries" true (List.length names > 0) 178 179(** {1 HTTP/2 Pseudo-Header Tests} *) 180 181let test_is_pseudo_header () = 182 Alcotest.(check bool) 183 ":method is pseudo" true 184 (Headers.is_pseudo_header ":method"); 185 Alcotest.(check bool) 186 "content-type not pseudo" false 187 (Headers.is_pseudo_header "content-type") 188 189let test_set_get_pseudo () = 190 let h = Headers.empty |> Headers.set_pseudo "method" "GET" in 191 let v = Headers.pseudo "method" h in 192 Alcotest.(check (option string)) "get pseudo" (Some "GET") v 193 194let test_pseudo_roundtrip () = 195 let h = 196 Headers.empty 197 |> Headers.set_pseudo "method" "GET" 198 |> Headers.set_pseudo "scheme" "https" 199 |> Headers.set_pseudo "path" "/" 200 in 201 Alcotest.(check bool) "has pseudo headers" true (Headers.has_pseudo_headers h); 202 let pseudos = Headers.pseudo_headers h in 203 Alcotest.(check int) "3 pseudo headers" 3 (List.length pseudos) 204 205let test_remove_pseudo () = 206 let h = 207 Headers.empty 208 |> Headers.set_pseudo "method" "GET" 209 |> Headers.remove_pseudo "method" 210 in 211 Alcotest.(check bool) "removed pseudo" false (Headers.mem_pseudo "method" h) 212 213let test_regular_headers_exclude_pseudo () = 214 let h = 215 Headers.empty 216 |> Headers.set_pseudo "method" "GET" 217 |> Headers.set `Content_type "text/html" 218 in 219 let regular = Headers.regular_headers h in 220 let has_pseudo = 221 List.exists 222 (fun (name, _) -> String.length name > 0 && name.[0] = ':') 223 regular 224 in 225 Alcotest.(check bool) "no pseudo in regular" false has_pseudo 226 227(** {1 mem Tests} *) 228 229let test_mem_present () = 230 let h = Headers.empty |> Headers.set `Content_type "text/html" in 231 Alcotest.(check bool) "mem present" true (Headers.mem `Content_type h) 232 233let test_mem_absent () = 234 let h = Headers.empty in 235 Alcotest.(check bool) "mem absent" false (Headers.mem `Content_type h) 236 237(** {1 Test Suite} *) 238 239let suite = 240 ( "headers", 241 [ 242 Alcotest.test_case "no headers" `Quick test_empty_has_no_headers; 243 Alcotest.test_case "roundtrip" `Quick test_of_list_roundtrip; 244 Alcotest.test_case "preserves values" `Quick test_of_list_preserves_values; 245 Alcotest.test_case "add and get" `Quick test_add_and_get; 246 Alcotest.test_case "add multiple" `Quick test_add_multiple; 247 Alcotest.test_case "get missing" `Quick test_get_missing; 248 Alcotest.test_case "remove" `Quick test_remove; 249 Alcotest.test_case "replaces existing" `Quick test_set_replaces; 250 Alcotest.test_case "replaces all" `Quick test_set_replaces_all; 251 Alcotest.test_case "empty" `Quick test_get_all_empty; 252 Alcotest.test_case "multiple values" `Quick test_get_all_multiple; 253 Alcotest.test_case "combines" `Quick test_merge_combines; 254 Alcotest.test_case "override" `Quick test_merge_override; 255 Alcotest.test_case "content_type" `Quick test_content_type; 256 Alcotest.test_case "content_length" `Quick test_content_length; 257 Alcotest.test_case "host" `Quick test_host; 258 Alcotest.test_case "bearer" `Quick test_bearer; 259 Alcotest.test_case "basic" `Quick test_basic; 260 Alcotest.test_case "connection_close" `Quick test_connection_close; 261 Alcotest.test_case "connection_close false" `Quick 262 test_connection_close_false; 263 Alcotest.test_case "connection_keep_alive" `Quick 264 test_connection_keep_alive; 265 Alcotest.test_case "parse_connection_header" `Quick 266 test_parse_connection_header; 267 Alcotest.test_case "is_pseudo_header" `Quick test_is_pseudo_header; 268 Alcotest.test_case "set and get" `Quick test_set_get_pseudo; 269 Alcotest.test_case "roundtrip" `Quick test_pseudo_roundtrip; 270 Alcotest.test_case "remove" `Quick test_remove_pseudo; 271 Alcotest.test_case "regular excludes pseudo" `Quick 272 test_regular_headers_exclude_pseudo; 273 Alcotest.test_case "present" `Quick test_mem_present; 274 Alcotest.test_case "absent" `Quick test_mem_absent; 275 ] )