A batteries included HTTP/1.1 client in OCaml
at claude-test 237 lines 9.5 kB view raw
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 ]