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(** Comprehensive tests for HTTP-date parsing per RFC 9110 Section 5.6.7 *)
7
8open Requests
9
10(** Alcotest testable for Ptime.t *)
11module Alcotest_ptime = struct
12 let pp = Ptime.pp_rfc3339 ()
13 let equal = Ptime.equal
14 let testable = Alcotest.testable pp equal
15end
16
17(** Helper to create expected Ptime.t values *)
18let make_time year month day hour min sec =
19 match Ptime.of_date_time ((year, month, day), ((hour, min, sec), 0)) with
20 | Some t -> t
21 | None -> failwith (Printf.sprintf "Invalid test time: %d-%02d-%02d %02d:%02d:%02d"
22 year month day hour min sec)
23
24(** {1 RFC 1123 Format Tests} *)
25
26let test_rfc1123_basic () =
27 (* RFC 9110 Section 5.6.7: preferred format "Sun, 06 Nov 1994 08:49:37 GMT" *)
28 let result = Http_date.parse "Sun, 06 Nov 1994 08:49:37 GMT" in
29 let expected = Some (make_time 1994 11 6 8 49 37) in
30 Alcotest.(check (option Alcotest_ptime.testable))
31 "RFC 1123 basic parsing" expected result
32
33let test_rfc1123_all_months () =
34 (* Test all month names *)
35 let months = [
36 ("Jan", 1); ("Feb", 2); ("Mar", 3); ("Apr", 4);
37 ("May", 5); ("Jun", 6); ("Jul", 7); ("Aug", 8);
38 ("Sep", 9); ("Oct", 10); ("Nov", 11); ("Dec", 12);
39 ] in
40 List.iter (fun (month_str, month_num) ->
41 let date_str = Printf.sprintf "Mon, 01 %s 2020 00:00:00 GMT" month_str in
42 let result = Http_date.parse date_str in
43 let expected = Some (make_time 2020 month_num 1 0 0 0) in
44 Alcotest.(check (option Alcotest_ptime.testable))
45 (Printf.sprintf "RFC 1123 month %s" month_str) expected result
46 ) months
47
48let test_rfc1123_all_weekdays () =
49 (* Test all weekday names - the weekday is not validated, just skipped *)
50 let weekdays = ["Sun"; "Mon"; "Tue"; "Wed"; "Thu"; "Fri"; "Sat"] in
51 List.iter (fun wday ->
52 let date_str = Printf.sprintf "%s, 06 Nov 1994 08:49:37 GMT" wday in
53 let result = Http_date.parse date_str in
54 let expected = Some (make_time 1994 11 6 8 49 37) in
55 Alcotest.(check (option Alcotest_ptime.testable))
56 (Printf.sprintf "RFC 1123 weekday %s" wday) expected result
57 ) weekdays
58
59let test_rfc1123_edge_dates () =
60 (* Test edge cases for dates *)
61 let test_cases = [
62 ("Thu, 01 Jan 1970 00:00:00 GMT", 1970, 1, 1, 0, 0, 0, "Unix epoch");
63 ("Fri, 31 Dec 1999 23:59:59 GMT", 1999, 12, 31, 23, 59, 59, "Y2K eve");
64 ("Sat, 01 Jan 2000 00:00:00 GMT", 2000, 1, 1, 0, 0, 0, "Y2K");
65 ("Tue, 29 Feb 2000 12:00:00 GMT", 2000, 2, 29, 12, 0, 0, "Leap year");
66 ("Fri, 13 Dec 2024 23:59:59 GMT", 2024, 12, 13, 23, 59, 59, "Near current");
67 ] in
68 List.iter (fun (date_str, y, m, d, h, min, s, desc) ->
69 let result = Http_date.parse date_str in
70 let expected = Some (make_time y m d h min s) in
71 Alcotest.(check (option Alcotest_ptime.testable))
72 (Printf.sprintf "RFC 1123 edge: %s" desc) expected result
73 ) test_cases
74
75(** {1 RFC 850 Format Tests (Obsolete)} *)
76
77let test_rfc850_basic () =
78 (* RFC 850 format: "Sunday, 06-Nov-94 08:49:37 GMT" *)
79 let result = Http_date.parse "Sunday, 06-Nov-94 08:49:37 GMT" in
80 let expected = Some (make_time 1994 11 6 8 49 37) in
81 Alcotest.(check (option Alcotest_ptime.testable))
82 "RFC 850 basic parsing (2-digit year)" expected result
83
84let test_rfc850_year_interpretation () =
85 (* Test Y2K two-digit year interpretation: 70-99 -> 1970-1999, 00-69 -> 2000-2069 *)
86 let test_cases = [
87 ("Monday, 01-Jan-70 00:00:00 GMT", 1970, "Year 70 -> 1970");
88 ("Tuesday, 01-Jan-99 00:00:00 GMT", 1999, "Year 99 -> 1999");
89 ("Saturday, 01-Jan-00 00:00:00 GMT", 2000, "Year 00 -> 2000");
90 ("Sunday, 01-Jan-25 00:00:00 GMT", 2025, "Year 25 -> 2025");
91 ("Thursday, 01-Jan-69 00:00:00 GMT", 2069, "Year 69 -> 2069");
92 ] in
93 List.iter (fun (date_str, expected_year, desc) ->
94 let result = Http_date.parse date_str in
95 let expected = Some (make_time expected_year 1 1 0 0 0) in
96 Alcotest.(check (option Alcotest_ptime.testable))
97 (Printf.sprintf "RFC 850 %s" desc) expected result
98 ) test_cases
99
100(** {1 ANSI C asctime() Format Tests (Obsolete)} *)
101
102let test_asctime_basic () =
103 (* asctime() format: "Sun Nov 6 08:49:37 1994" *)
104 let result = Http_date.parse "Sun Nov 6 08:49:37 1994" in
105 let expected = Some (make_time 1994 11 6 8 49 37) in
106 Alcotest.(check (option Alcotest_ptime.testable))
107 "asctime basic parsing" expected result
108
109let test_asctime_single_digit_day () =
110 (* asctime has space-padded day for single digits *)
111 let test_cases = [
112 ("Sun Nov 1 08:49:37 1994", 1, "Day 1");
113 ("Sun Nov 9 08:49:37 1994", 9, "Day 9");
114 ] in
115 List.iter (fun (date_str, day, desc) ->
116 let result = Http_date.parse date_str in
117 let expected = Some (make_time 1994 11 day 8 49 37) in
118 Alcotest.(check (option Alcotest_ptime.testable))
119 (Printf.sprintf "asctime %s" desc) expected result
120 ) test_cases
121
122(** {1 Invalid Input Tests} *)
123
124let test_invalid_completely_wrong () =
125 (* Completely invalid strings *)
126 let invalid_inputs = [
127 "";
128 "not a date";
129 "2024-12-13"; (* ISO 8601 not supported *)
130 "12/13/2024"; (* US format not supported *)
131 "13-Dec-2024"; (* No day name *)
132 ] in
133 List.iter (fun input ->
134 let result = Http_date.parse input in
135 Alcotest.(check (option Alcotest_ptime.testable))
136 (Printf.sprintf "Invalid input: %S" input) None result
137 ) invalid_inputs
138
139let test_invalid_month_names () =
140 (* Invalid month names *)
141 let invalid_months = [
142 "Sun, 06 Foo 1994 08:49:37 GMT";
143 "Sun, 06 13 1994 08:49:37 GMT"; (* Numeric month *)
144 "Sun, 06 November 1994 08:49:37 GMT"; (* Full month name *)
145 ] in
146 List.iter (fun input ->
147 let result = Http_date.parse input in
148 Alcotest.(check (option Alcotest_ptime.testable))
149 (Printf.sprintf "Invalid month: %S" input) None result
150 ) invalid_months
151
152let test_invalid_dates () =
153 (* Dates that are syntactically correct but semantically invalid *)
154 let invalid_dates = [
155 "Sun, 32 Jan 2020 00:00:00 GMT"; (* Day 32 *)
156 "Sun, 00 Jan 2020 00:00:00 GMT"; (* Day 0 *)
157 "Sun, 29 Feb 2021 00:00:00 GMT"; (* Feb 29 in non-leap year *)
158 "Sun, 31 Apr 2020 00:00:00 GMT"; (* April has 30 days *)
159 ] in
160 List.iter (fun input ->
161 let result = Http_date.parse input in
162 Alcotest.(check (option Alcotest_ptime.testable))
163 (Printf.sprintf "Invalid date: %S" input) None result
164 ) invalid_dates
165
166let test_invalid_times () =
167 (* Invalid time components *)
168 let invalid_times = [
169 "Sun, 06 Nov 1994 25:00:00 GMT"; (* Hour 25 *)
170 "Sun, 06 Nov 1994 00:60:00 GMT"; (* Minute 60 *)
171 "Sun, 06 Nov 1994 00:00:60 GMT"; (* Second 60 (no leap second support) *)
172 ] in
173 List.iter (fun input ->
174 let result = Http_date.parse input in
175 Alcotest.(check (option Alcotest_ptime.testable))
176 (Printf.sprintf "Invalid time: %S" input) None result
177 ) invalid_times
178
179(** {1 Whitespace and Case Tests} *)
180
181let test_trimming_whitespace () =
182 (* Should handle leading/trailing whitespace *)
183 let test_cases = [
184 " Sun, 06 Nov 1994 08:49:37 GMT ";
185 "\tSun, 06 Nov 1994 08:49:37 GMT\t";
186 "\n Sun, 06 Nov 1994 08:49:37 GMT \n";
187 ] in
188 let expected = Some (make_time 1994 11 6 8 49 37) in
189 List.iter (fun input ->
190 let result = Http_date.parse input in
191 Alcotest.(check (option Alcotest_ptime.testable))
192 "Whitespace trimming" expected result
193 ) test_cases
194
195let test_case_insensitive_months () =
196 (* Month names should be case-insensitive *)
197 let test_cases = [
198 ("Sun, 06 nov 1994 08:49:37 GMT", "lowercase");
199 ("Sun, 06 NOV 1994 08:49:37 GMT", "uppercase");
200 ("Sun, 06 NoV 1994 08:49:37 GMT", "mixed case");
201 ] in
202 let expected = Some (make_time 1994 11 6 8 49 37) in
203 List.iter (fun (input, desc) ->
204 let result = Http_date.parse input in
205 Alcotest.(check (option Alcotest_ptime.testable))
206 (Printf.sprintf "Case insensitive: %s" desc) expected result
207 ) test_cases
208
209(** {1 Test Suite} *)
210
211let () =
212 Alcotest.run "HTTP Date Parsing (RFC 9110 Section 5.6.7)" [
213 ("RFC 1123 format", [
214 Alcotest.test_case "Basic parsing" `Quick test_rfc1123_basic;
215 Alcotest.test_case "All months" `Quick test_rfc1123_all_months;
216 Alcotest.test_case "All weekdays" `Quick test_rfc1123_all_weekdays;
217 Alcotest.test_case "Edge dates" `Quick test_rfc1123_edge_dates;
218 ]);
219 ("RFC 850 format (obsolete)", [
220 Alcotest.test_case "Basic parsing" `Quick test_rfc850_basic;
221 Alcotest.test_case "Y2K year interpretation" `Quick test_rfc850_year_interpretation;
222 ]);
223 ("asctime format (obsolete)", [
224 Alcotest.test_case "Basic parsing" `Quick test_asctime_basic;
225 Alcotest.test_case "Single digit day" `Quick test_asctime_single_digit_day;
226 ]);
227 ("Invalid inputs", [
228 Alcotest.test_case "Completely wrong format" `Quick test_invalid_completely_wrong;
229 Alcotest.test_case "Invalid month names" `Quick test_invalid_month_names;
230 Alcotest.test_case "Invalid dates" `Quick test_invalid_dates;
231 Alcotest.test_case "Invalid times" `Quick test_invalid_times;
232 ]);
233 ("Whitespace and case", [
234 Alcotest.test_case "Trimming whitespace" `Quick test_trimming_whitespace;
235 Alcotest.test_case "Case insensitive months" `Quick test_case_insensitive_months;
236 ]);
237 ]