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 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 ] )