···
"only session cookie remains" "session"
(Cookeio.name (List.hd remaining))
382
+
let test_get_cookies_filters_expired () =
383
+
Eio_mock.Backend.run @@ fun () ->
384
+
let clock = Eio_mock.Clock.make () in
385
+
Eio_mock.Clock.set_time clock 1000.0;
387
+
let jar = create () in
389
+
(* Add an expired cookie (expired at time 500) *)
390
+
let expired = Ptime.of_float_s 500.0 |> Option.get in
391
+
let cookie_expired =
392
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"expired"
393
+
~value:"old" ~secure:false ~http_only:false
394
+
~expires:(`DateTime expired)
395
+
~creation_time:(Ptime.of_float_s 100.0 |> Option.get)
396
+
~last_access:(Ptime.of_float_s 100.0 |> Option.get)
400
+
(* Add a valid cookie (expires at time 2000) *)
401
+
let valid_time = Ptime.of_float_s 2000.0 |> Option.get in
403
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"valid"
404
+
~value:"current" ~secure:false ~http_only:false
405
+
~expires:(`DateTime valid_time)
406
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
407
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
411
+
(* Add a session cookie (no expiry) *)
412
+
let cookie_session =
413
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"session"
414
+
~value:"sess" ~secure:false ~http_only:false
415
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
416
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
420
+
add_cookie jar cookie_expired;
421
+
add_cookie jar cookie_valid;
422
+
add_cookie jar cookie_session;
424
+
(* get_all_cookies returns all including expired (for inspection) *)
425
+
Alcotest.(check int) "get_all_cookies includes expired" 3
426
+
(List.length (get_all_cookies jar));
428
+
(* get_cookies should automatically filter out expired cookies *)
430
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
432
+
Alcotest.(check int) "get_cookies filters expired" 2 (List.length cookies);
434
+
let names = List.map Cookeio.name cookies |> List.sort String.compare in
435
+
Alcotest.(check (list string))
436
+
"only non-expired cookies returned"
437
+
[ "session"; "valid" ]
let test_max_age_parsing_with_mock_clock () =
Eio_mock.Backend.run @@ fun () ->
let clock = Eio_mock.Clock.make () in
···
~domain:"example.com" ~path:"/" header
399
-
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
457
+
Alcotest.(check bool) "cookie parsed" true (Result.is_ok cookie_opt);
401
-
let cookie = Option.get cookie_opt in
459
+
let cookie = Result.get_ok cookie_opt in
Alcotest.(check string) "cookie name" "session" (Cookeio.name cookie);
Alcotest.(check string) "cookie value" "abc123" (Cookeio.value cookie);
Alcotest.(check bool) "cookie secure" true (Cookeio.secure cookie);
···
~domain:"example.com" ~path:"/" header
484
-
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
542
+
Alcotest.(check bool) "cookie parsed" true (Result.is_ok cookie_opt);
486
-
let cookie = Option.get cookie_opt in
544
+
let cookie = Result.get_ok cookie_opt in
Alcotest.(check string) "cookie name" "id" (Cookeio.name cookie);
Alcotest.(check string) "cookie value" "xyz789" (Cookeio.value cookie);
Alcotest.(check string) "cookie domain" "example.com" (Cookeio.domain cookie);
···
"invalid cookie rejected" true
526
-
(Option.is_none cookie_opt);
584
+
(Result.is_error cookie_opt);
(* This should be accepted: SameSite=None with Secure *)
let valid_header = "token=abc; SameSite=None; Secure" in
···
"valid cookie accepted" true
540
-
(Option.is_some cookie_opt2);
598
+
(Result.is_ok cookie_opt2);
542
-
let cookie = Option.get cookie_opt2 in
600
+
let cookie = Result.get_ok cookie_opt2 in
Alcotest.(check bool) "cookie is secure" true (Cookeio.secure cookie);
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header
569
-
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
570
-
let cookie = Option.get cookie_opt in
627
+
Alcotest.(check bool) "cookie parsed" true (Result.is_ok cookie_opt);
628
+
let cookie = Result.get_ok cookie_opt in
"domain normalized" "example.com" (Cookeio.domain cookie);
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header
607
-
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
665
+
Alcotest.(check bool) "cookie parsed" true (Result.is_ok cookie_opt);
609
-
let cookie = Option.get cookie_opt in
667
+
let cookie = Result.get_ok cookie_opt in
(* Verify max_age is stored as a Ptime.Span *)
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header
645
-
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
703
+
Alcotest.(check bool) "cookie parsed" true (Result.is_ok cookie_opt);
647
-
let cookie = Option.get cookie_opt in
705
+
let cookie = Result.get_ok cookie_opt in
(* Verify max_age is stored as 0 per RFC 6265 *)
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header
735
-
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
736
-
let cookie = Option.get cookie_opt in
793
+
Alcotest.(check bool) "cookie parsed" true (Result.is_ok cookie_opt);
794
+
let cookie = Result.get_ok cookie_opt in
(* Generate Set-Cookie header from the cookie *)
let set_cookie_header = make_set_cookie_header cookie in
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" set_cookie_header
751
-
Alcotest.(check bool) "cookie re-parsed" true (Option.is_some cookie2_opt);
752
-
let cookie2 = Option.get cookie2_opt in
809
+
Alcotest.(check bool) "cookie re-parsed" true (Result.is_ok cookie2_opt);
810
+
let cookie2 = Result.get_ok cookie2_opt in
(* Verify max_age is preserved *)
Alcotest.(check (option int))
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header
824
-
Alcotest.(check bool) "FMT1 cookie parsed" true (Option.is_some cookie_opt);
882
+
Alcotest.(check bool) "FMT1 cookie parsed" true (Result.is_ok cookie_opt);
826
-
let cookie = Option.get cookie_opt in
884
+
let cookie = Result.get_ok cookie_opt in
(Option.is_some (Cookeio.expires cookie));
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header
856
-
Alcotest.(check bool) "FMT2 cookie parsed" true (Option.is_some cookie_opt);
914
+
Alcotest.(check bool) "FMT2 cookie parsed" true (Result.is_ok cookie_opt);
858
-
let cookie = Option.get cookie_opt in
916
+
let cookie = Result.get_ok cookie_opt in
(Option.is_some (Cookeio.expires cookie));
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header
888
-
Alcotest.(check bool) "FMT3 cookie parsed" true (Option.is_some cookie_opt);
946
+
Alcotest.(check bool) "FMT3 cookie parsed" true (Result.is_ok cookie_opt);
890
-
let cookie = Option.get cookie_opt in
948
+
let cookie = Result.get_ok cookie_opt in
(Option.is_some (Cookeio.expires cookie));
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header
919
-
Alcotest.(check bool) "FMT4 cookie parsed" true (Option.is_some cookie_opt);
977
+
Alcotest.(check bool) "FMT4 cookie parsed" true (Result.is_ok cookie_opt);
921
-
let cookie = Option.get cookie_opt in
979
+
let cookie = Result.get_ok cookie_opt in
(Option.is_some (Cookeio.expires cookie));
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header
950
-
let cookie = Option.get cookie_opt in
1008
+
let cookie = Result.get_ok cookie_opt in
let expected = Ptime.of_date_time ((1995, 10, 21), ((07, 28, 00), 0)) in
begin match expected with
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header2
970
-
let cookie2 = Option.get cookie_opt2 in
1028
+
let cookie2 = Result.get_ok cookie_opt2 in
let expected2 = Ptime.of_date_time ((1969, 9, 10), ((20, 0, 0), 0)) in
begin match expected2 with
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header3
990
-
let cookie3 = Option.get cookie_opt3 in
1048
+
let cookie3 = Result.get_ok cookie_opt3 in
let expected3 = Ptime.of_date_time ((1999, 9, 10), ((20, 0, 0), 0)) in
begin match expected3 with
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header
1015
-
let cookie = Option.get cookie_opt in
1073
+
let cookie = Result.get_ok cookie_opt in
let expected = Ptime.of_date_time ((2025, 10, 21), ((07, 28, 00), 0)) in
begin match expected with
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header2
1035
-
let cookie2 = Option.get cookie_opt2 in
1093
+
let cookie2 = Result.get_ok cookie_opt2 in
let expected2 = Ptime.of_date_time ((2000, 1, 1), ((0, 0, 0), 0)) in
begin match expected2 with
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header3
1055
-
let cookie3 = Option.get cookie_opt3 in
1113
+
let cookie3 = Result.get_ok cookie_opt3 in
let expected3 = Ptime.of_date_time ((2068, 9, 10), ((20, 0, 0), 0)) in
begin match expected3 with
···
"RFC 3339 cookie parsed" true
1082
-
(Option.is_some cookie_opt);
1140
+
(Result.is_ok cookie_opt);
1084
-
let cookie = Option.get cookie_opt in
1142
+
let cookie = Result.get_ok cookie_opt in
"RFC 3339 has expiry" true
(Option.is_some (Cookeio.expires cookie));
···
(* Cookie should still be parsed, just without expires *)
"cookie parsed despite invalid date" true
1117
-
(Option.is_some cookie_opt);
1118
-
let cookie = Option.get cookie_opt in
1175
+
(Result.is_ok cookie_opt);
1176
+
let cookie = Result.get_ok cookie_opt in
Alcotest.(check string) "cookie name correct" "session" (Cookeio.name cookie);
Alcotest.(check string) "cookie value correct" "abc" (Cookeio.value cookie);
(* expires should be None since date was invalid *)
···
(description ^ " parsed") true
1151
-
(Option.is_some cookie_opt);
1209
+
(Result.is_ok cookie_opt);
1153
-
let cookie = Option.get cookie_opt in
1211
+
let cookie = Result.get_ok cookie_opt in
(description ^ " has expiry")
···
(description ^ " parsed") true
1197
-
(Option.is_some cookie_opt);
1255
+
(Result.is_ok cookie_opt);
1199
-
let cookie = Option.get cookie_opt in
1257
+
let cookie = Result.get_ok cookie_opt in
(description ^ " has expiry")
···
|> Option.value ~default:Ptime.epoch)
~domain:"widget.com" ~path:"/" "id=123; Partitioned; Secure"
Alcotest.(check bool) "partitioned flag" true (partitioned c);
Alcotest.(check bool) "secure flag" true (secure c)
1529
-
| None -> Alcotest.fail "Should parse valid Partitioned cookie"
1587
+
| Error msg -> Alcotest.fail ("Should parse valid Partitioned cookie: " ^ msg)
let test_partitioned_serialization env =
let clock = Eio.Stdenv.clock env in
···
|> Option.value ~default:Ptime.epoch)
~domain:"widget.com" ~path:"/" "id=123; Partitioned"
1565
-
| None -> () (* Expected *)
1566
-
| Some _ -> Alcotest.fail "Should reject Partitioned without Secure"
1623
+
| Error _ -> () (* Expected *)
1624
+
| Ok _ -> Alcotest.fail "Should reject Partitioned without Secure"
(* Priority 2.2: Expiration Variants *)
···
|> Option.value ~default:Ptime.epoch)
~domain:"ex.com" ~path:"/" "id=123; Expires=0"
Alcotest.(check (option expiration_testable))
"expires=0 is session" (Some `Session) (expires c)
1611
-
| None -> Alcotest.fail "Should parse Expires=0"
1669
+
| Error msg -> Alcotest.fail ("Should parse Expires=0: " ^ msg)
let test_serialize_expiration_variants env =
let clock = Eio.Stdenv.clock env in
···
let test_quoted_cookie_values env =
let clock = Eio.Stdenv.clock env in
1706
+
(* Test valid RFC 6265 cookie values:
1707
+
cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
1708
+
Valid cases have either no quotes or properly paired DQUOTE wrapper *)
1650
-
("name=value", "value", "value");
1651
-
("name=\"value\"", "\"value\"", "value");
1652
-
("name=\"partial", "\"partial", "\"partial");
1653
-
("name=\"val\"\"", "\"val\"\"", "val\"");
1654
-
("name=val\"", "val\"", "val\"");
1655
-
("name=\"\"", "\"\"", "");
1711
+
("name=value", "value", "value"); (* No quotes *)
1712
+
("name=\"value\"", "\"value\"", "value"); (* Properly quoted *)
1713
+
("name=\"\"", "\"\"", ""); (* Empty quoted value *)
···
|> Option.value ~default:Ptime.epoch)
~domain:"ex.com" ~path:"/" input
(Printf.sprintf "raw value for %s" input)
(Printf.sprintf "trimmed value for %s" input)
expected_trimmed (value_trimmed c)
1675
-
| None -> Alcotest.fail ("Parse failed: " ^ input))
1733
+
| Error msg -> Alcotest.fail ("Parse failed: " ^ input ^ ": " ^ msg))
1736
+
(* Test invalid RFC 6265 cookie values are rejected *)
1737
+
let invalid_cases =
1739
+
"name=\"partial"; (* Opening quote without closing *)
1740
+
"name=\"val\"\""; (* Embedded quote *)
1741
+
"name=val\""; (* Trailing quote without opening *)
1748
+
of_set_cookie_header
1750
+
Ptime.of_float_s (Eio.Time.now clock)
1751
+
|> Option.value ~default:Ptime.epoch)
1752
+
~domain:"ex.com" ~path:"/" input
1754
+
| Error _ -> () (* Expected - invalid values are rejected *)
1757
+
(Printf.sprintf "Should reject invalid value: %s" input))
let test_trimmed_value_not_used_for_equality env =
let clock = Eio.Stdenv.clock env in
···
|> Option.value ~default:Ptime.epoch)
~domain:"ex.com" ~path:"/" "name=\"value\""
1688
-
| Some c1 -> begin
···
|> Option.value ~default:Ptime.epoch)
~domain:"ex.com" ~path:"/" "name=value"
(* Different raw values *)
"different raw values" false
···
(* Same trimmed values *)
"same trimmed values" (value_trimmed c1) (value_trimmed c2)
1704
-
| None -> Alcotest.fail "Parse failed for unquoted"
1786
+
| Error msg -> Alcotest.fail ("Parse failed for unquoted: " ^ msg)
1706
-
| None -> Alcotest.fail "Parse failed for quoted"
1788
+
| Error msg -> Alcotest.fail ("Parse failed for quoted: " ^ msg)
(* Priority 2.4: Cookie Header Parsing *)
let test_cookie_header_parsing_basic env =
let clock = Eio.Stdenv.clock env in
Ptime.of_float_s (Eio.Time.now clock)
···
~domain:"ex.com" ~path:"/" "session=abc123; theme=dark; lang=en"
1720
-
let cookies = List.filter_map Result.to_option results in
1721
-
Alcotest.(check int) "parsed 3 cookies" 3 (List.length cookies);
1803
+
| Error msg -> Alcotest.fail ("Parse failed: " ^ msg)
1805
+
Alcotest.(check int) "parsed 3 cookies" 3 (List.length cookies);
1723
-
let find name_val = List.find (fun c -> name c = name_val) cookies in
1724
-
Alcotest.(check string) "session value" "abc123" (value (find "session"));
1725
-
Alcotest.(check string) "theme value" "dark" (value (find "theme"));
1726
-
Alcotest.(check string) "lang value" "en" (value (find "lang"))
1807
+
let find name_val = List.find (fun c -> name c = name_val) cookies in
1808
+
Alcotest.(check string) "session value" "abc123" (value (find "session"));
1809
+
Alcotest.(check string) "theme value" "dark" (value (find "theme"));
1810
+
Alcotest.(check string) "lang value" "en" (value (find "lang"))
let test_cookie_header_defaults env =
let clock = Eio.Stdenv.clock env in
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/app" "session=xyz"
(* Domain and path from request context *)
Alcotest.(check string) "domain from context" "example.com" (domain c);
Alcotest.(check string) "path from context" "/app" (path c);
···
Alcotest.(check (option span_testable)) "no max_age" None (max_age c);
Alcotest.(check (option same_site_testable))
"no same_site" None (same_site c)
1754
-
| _ -> Alcotest.fail "Should parse single cookie"
1838
+
| Ok _ -> Alcotest.fail "Should parse single cookie"
1839
+
| Error msg -> Alcotest.fail ("Parse failed: " ^ msg)
let test_cookie_header_edge_cases env =
let clock = Eio.Stdenv.clock env in
let test input expected_count description =
Ptime.of_float_s (Eio.Time.now clock)
|> Option.value ~default:Ptime.epoch)
~domain:"ex.com" ~path:"/" input
1767
-
let cookies = List.filter_map Result.to_option results in
1768
-
Alcotest.(check int) description expected_count (List.length cookies)
1854
+
Alcotest.(check int) description expected_count (List.length cookies)
1856
+
Alcotest.fail (description ^ " failed: " ^ msg)
test "" 0 "empty string";
···
let test_cookie_header_with_errors env =
let clock = Eio.Stdenv.clock env in
1780
-
(* Mix of valid and invalid cookies *)
1868
+
(* Invalid cookie (empty name) should cause entire parse to fail *)
Ptime.of_float_s (Eio.Time.now clock)
···
~domain:"ex.com" ~path:"/" "valid=1;=noname;valid2=2"
1789
-
Alcotest.(check int) "total results" 3 (List.length results);
1791
-
let successes = List.filter Result.is_ok results in
1792
-
let errors = List.filter Result.is_error results in
1794
-
Alcotest.(check int) "successful parses" 2 (List.length successes);
1795
-
Alcotest.(check int) "failed parses" 1 (List.length errors);
1797
-
(* Error should have descriptive message *)
1877
+
(* Error should have descriptive message about the invalid cookie *)
let contains_substring s sub =
let _ = Str.search_forward (Str.regexp_string sub) s 0 in
1804
-
begin match List.hd errors with
let has_name = contains_substring msg "name" in
let has_empty = contains_substring msg "empty" in
"error mentions name or empty" true (has_name || has_empty)
1810
-
| Ok _ -> Alcotest.fail "Expected error"
1890
+
| Ok _ -> Alcotest.fail "Expected error for empty cookie name"
(* Max-Age and Expires Interaction *)
···
~domain:"ex.com" ~path:"/"
"id=123; Max-Age=3600; Expires=Wed, 21 Oct 2025 07:28:00 GMT"
(* Both should be stored *)
begin match max_age c with
···
| Some (`DateTime _) -> ()
| _ -> Alcotest.fail "expires should be parsed"
1886
-
| None -> Alcotest.fail "Should parse cookie with both attributes"
1965
+
| Error msg -> Alcotest.fail ("Should parse cookie with both attributes: " ^ msg)
(* ============================================================================ *)
(* Host-Only Flag Tests (RFC 6265 Section 5.3) *)
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header
1906
-
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
1907
-
let cookie = Option.get cookie_opt in
1985
+
Alcotest.(check bool) "cookie parsed" true (Result.is_ok cookie_opt);
1986
+
let cookie = Result.get_ok cookie_opt in
Alcotest.(check bool) "host_only is true" true (Cookeio.host_only cookie);
Alcotest.(check string) "domain is request host" "example.com" (Cookeio.domain cookie)
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header
1925
-
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
1926
-
let cookie = Option.get cookie_opt in
2004
+
Alcotest.(check bool) "cookie parsed" true (Result.is_ok cookie_opt);
2005
+
let cookie = Result.get_ok cookie_opt in
Alcotest.(check bool) "host_only is false" false (Cookeio.host_only cookie);
Alcotest.(check string) "domain is attribute value" "example.com" (Cookeio.domain cookie)
···
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" header
1944
-
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
1945
-
let cookie = Option.get cookie_opt in
2023
+
Alcotest.(check bool) "cookie parsed" true (Result.is_ok cookie_opt);
2024
+
let cookie = Result.get_ok cookie_opt in
Alcotest.(check bool) "host_only is false" false (Cookeio.host_only cookie);
Alcotest.(check string) "domain normalized" "example.com" (Cookeio.domain cookie)
···
Eio_mock.Clock.set_time clock 1000.0;
(* Cookies from Cookie header should have host_only=true *)
Ptime.of_float_s (Eio.Time.now clock)
|> Option.value ~default:Ptime.epoch)
~domain:"example.com" ~path:"/" "session=abc; theme=dark"
2001
-
let cookies = List.filter_map Result.to_option results in
2002
-
Alcotest.(check int) "parsed 2 cookies" 2 (List.length cookies);
2003
-
List.iter (fun c ->
2004
-
Alcotest.(check bool)
2005
-
("host_only is true for " ^ Cookeio.name c)
2006
-
true (Cookeio.host_only c)
2081
+
| Error msg -> Alcotest.fail ("Parse failed: " ^ msg)
2083
+
Alcotest.(check int) "parsed 2 cookies" 2 (List.length cookies);
2084
+
List.iter (fun c ->
2085
+
Alcotest.(check bool)
2086
+
("host_only is true for " ^ Cookeio.name c)
2087
+
true (Cookeio.host_only c)
let test_host_only_mozilla_format_round_trip () =
Eio_mock.Backend.run @@ fun () ->
···
Alcotest.(check int) "/foo/bar does NOT match /baz" 0 (List.length cookies3)
(* ============================================================================ *)
2291
+
(* Cookie Ordering Tests (RFC 6265 Section 5.4, Step 2) *)
2292
+
(* ============================================================================ *)
2294
+
let test_cookie_ordering_by_path_length () =
2295
+
Eio_mock.Backend.run @@ fun () ->
2296
+
let clock = Eio_mock.Clock.make () in
2297
+
Eio_mock.Clock.set_time clock 1000.0;
2299
+
let jar = create () in
2301
+
(* Add cookies with different path lengths, but same creation time *)
2302
+
let cookie_short =
2303
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"short" ~value:"v1"
2304
+
~secure:false ~http_only:false
2305
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2306
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2308
+
let cookie_medium =
2309
+
Cookeio.make ~domain:"example.com" ~path:"/foo" ~name:"medium" ~value:"v2"
2310
+
~secure:false ~http_only:false
2311
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2312
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2315
+
Cookeio.make ~domain:"example.com" ~path:"/foo/bar" ~name:"long" ~value:"v3"
2316
+
~secure:false ~http_only:false
2317
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2318
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2321
+
(* Add in random order *)
2322
+
add_cookie jar cookie_short;
2323
+
add_cookie jar cookie_long;
2324
+
add_cookie jar cookie_medium;
2326
+
(* Get cookies for path /foo/bar/baz - all three should match *)
2328
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/foo/bar/baz" ~is_secure:false
2331
+
Alcotest.(check int) "all 3 cookies match" 3 (List.length cookies);
2333
+
(* Verify order: longest path first *)
2334
+
let names = List.map Cookeio.name cookies in
2335
+
Alcotest.(check (list string))
2336
+
"cookies ordered by path length (longest first)"
2337
+
[ "long"; "medium"; "short" ]
2340
+
let test_cookie_ordering_by_creation_time () =
2341
+
Eio_mock.Backend.run @@ fun () ->
2342
+
let clock = Eio_mock.Clock.make () in
2343
+
Eio_mock.Clock.set_time clock 2000.0;
2345
+
let jar = create () in
2347
+
(* Add cookies with same path but different creation times *)
2349
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"new" ~value:"v1"
2350
+
~secure:false ~http_only:false
2351
+
~creation_time:(Ptime.of_float_s 1500.0 |> Option.get)
2352
+
~last_access:(Ptime.of_float_s 1500.0 |> Option.get) ()
2355
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"old" ~value:"v2"
2356
+
~secure:false ~http_only:false
2357
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2358
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2360
+
let cookie_middle =
2361
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"middle" ~value:"v3"
2362
+
~secure:false ~http_only:false
2363
+
~creation_time:(Ptime.of_float_s 1200.0 |> Option.get)
2364
+
~last_access:(Ptime.of_float_s 1200.0 |> Option.get) ()
2367
+
(* Add in random order *)
2368
+
add_cookie jar cookie_new;
2369
+
add_cookie jar cookie_old;
2370
+
add_cookie jar cookie_middle;
2373
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
2376
+
Alcotest.(check int) "all 3 cookies match" 3 (List.length cookies);
2378
+
(* Verify order: earlier creation time first (for same path length) *)
2379
+
let names = List.map Cookeio.name cookies in
2380
+
Alcotest.(check (list string))
2381
+
"cookies ordered by creation time (earliest first)"
2382
+
[ "old"; "middle"; "new" ]
2385
+
let test_cookie_ordering_combined () =
2386
+
Eio_mock.Backend.run @@ fun () ->
2387
+
let clock = Eio_mock.Clock.make () in
2388
+
Eio_mock.Clock.set_time clock 2000.0;
2390
+
let jar = create () in
2392
+
(* Mix of different paths and creation times *)
2394
+
Cookeio.make ~domain:"example.com" ~path:"/foo" ~name:"a" ~value:"v1"
2395
+
~secure:false ~http_only:false
2396
+
~creation_time:(Ptime.of_float_s 1500.0 |> Option.get)
2397
+
~last_access:(Ptime.of_float_s 1500.0 |> Option.get) ()
2400
+
Cookeio.make ~domain:"example.com" ~path:"/foo" ~name:"b" ~value:"v2"
2401
+
~secure:false ~http_only:false
2402
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2403
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2406
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"c" ~value:"v3"
2407
+
~secure:false ~http_only:false
2408
+
~creation_time:(Ptime.of_float_s 500.0 |> Option.get)
2409
+
~last_access:(Ptime.of_float_s 500.0 |> Option.get) ()
2412
+
add_cookie jar cookie_a;
2413
+
add_cookie jar cookie_c;
2414
+
add_cookie jar cookie_b;
2417
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/foo/bar" ~is_secure:false
2420
+
Alcotest.(check int) "all 3 cookies match" 3 (List.length cookies);
2422
+
(* /foo cookies (length 4) should come before / cookie (length 1)
2423
+
Within /foo, earlier creation time (b=1000) should come before (a=1500) *)
2424
+
let names = List.map Cookeio.name cookies in
2425
+
Alcotest.(check (list string))
2426
+
"cookies ordered by path length then creation time"
2430
+
(* ============================================================================ *)
2431
+
(* Creation Time Preservation Tests (RFC 6265 Section 5.3, Step 11.3) *)
2432
+
(* ============================================================================ *)
2434
+
let test_creation_time_preserved_on_update () =
2435
+
Eio_mock.Backend.run @@ fun () ->
2436
+
let clock = Eio_mock.Clock.make () in
2437
+
Eio_mock.Clock.set_time clock 1000.0;
2439
+
let jar = create () in
2441
+
(* Add initial cookie with creation_time=500 *)
2442
+
let original_creation = Ptime.of_float_s 500.0 |> Option.get in
2444
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"session" ~value:"v1"
2445
+
~secure:false ~http_only:false
2446
+
~creation_time:original_creation
2447
+
~last_access:(Ptime.of_float_s 500.0 |> Option.get) ()
2449
+
add_cookie jar cookie_v1;
2451
+
(* Update the cookie with a new value (creation_time=1000) *)
2452
+
Eio_mock.Clock.set_time clock 1500.0;
2454
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"session" ~value:"v2"
2455
+
~secure:false ~http_only:false
2456
+
~creation_time:(Ptime.of_float_s 1500.0 |> Option.get)
2457
+
~last_access:(Ptime.of_float_s 1500.0 |> Option.get) ()
2459
+
add_cookie jar cookie_v2;
2461
+
(* Get the cookie and verify creation_time was preserved *)
2463
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
2465
+
Alcotest.(check int) "still one cookie" 1 (List.length cookies);
2467
+
let cookie = List.hd cookies in
2468
+
Alcotest.(check string) "value was updated" "v2" (Cookeio.value cookie);
2470
+
(* Creation time should be preserved from original cookie *)
2471
+
let creation_float =
2472
+
Ptime.to_float_s (Cookeio.creation_time cookie)
2474
+
Alcotest.(check (float 0.001))
2475
+
"creation_time preserved from original"
2476
+
500.0 creation_float
2478
+
let test_creation_time_preserved_add_original () =
2479
+
Eio_mock.Backend.run @@ fun () ->
2480
+
let clock = Eio_mock.Clock.make () in
2481
+
Eio_mock.Clock.set_time clock 1000.0;
2483
+
let jar = create () in
2485
+
(* Add initial original cookie *)
2486
+
let original_creation = Ptime.of_float_s 100.0 |> Option.get in
2488
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"v1"
2489
+
~secure:false ~http_only:false
2490
+
~creation_time:original_creation
2491
+
~last_access:(Ptime.of_float_s 100.0 |> Option.get) ()
2493
+
add_original jar cookie_v1;
2495
+
(* Replace with new original cookie *)
2497
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"v2"
2498
+
~secure:false ~http_only:false
2499
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2500
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2502
+
add_original jar cookie_v2;
2504
+
let cookies = get_all_cookies jar in
2505
+
Alcotest.(check int) "still one cookie" 1 (List.length cookies);
2507
+
let cookie = List.hd cookies in
2508
+
Alcotest.(check string) "value was updated" "v2" (Cookeio.value cookie);
2510
+
(* Creation time should be preserved *)
2511
+
let creation_float =
2512
+
Ptime.to_float_s (Cookeio.creation_time cookie)
2514
+
Alcotest.(check (float 0.001))
2515
+
"creation_time preserved in add_original"
2516
+
100.0 creation_float
2518
+
let test_creation_time_new_cookie () =
2519
+
Eio_mock.Backend.run @@ fun () ->
2520
+
let clock = Eio_mock.Clock.make () in
2521
+
Eio_mock.Clock.set_time clock 1000.0;
2523
+
let jar = create () in
2525
+
(* Add a new cookie (no existing cookie to preserve from) *)
2527
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"new" ~value:"v1"
2528
+
~secure:false ~http_only:false
2529
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2530
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2532
+
add_cookie jar cookie;
2535
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
2537
+
let cookie = List.hd cookies in
2539
+
(* New cookie should keep its own creation time *)
2540
+
let creation_float =
2541
+
Ptime.to_float_s (Cookeio.creation_time cookie)
2543
+
Alcotest.(check (float 0.001))
2544
+
"new cookie keeps its creation_time"
2545
+
1000.0 creation_float
2547
+
(* ============================================================================ *)
(* IP Address Domain Matching Tests (RFC 6265 Section 5.1.3) *)
(* ============================================================================ *)
···
Alcotest.(check int) "IP matches IP cookie" 1 (List.length cookies3);
Alcotest.(check string) "IP cookie is returned" "ip" (Cookeio.name (List.hd cookies3))
2702
+
(* ============================================================================ *)
2703
+
(* RFC 6265 Validation Tests *)
2704
+
(* ============================================================================ *)
2706
+
let test_validate_cookie_name_valid () =
2707
+
(* Valid token characters per RFC 2616 *)
2708
+
let valid_names = ["session"; "SID"; "my-cookie"; "COOKIE_123"; "abc.def"] in
2709
+
List.iter (fun name ->
2710
+
match Cookeio.Validate.cookie_name name with
2713
+
Alcotest.fail (Printf.sprintf "Name %S should be valid: %s" name msg))
2716
+
let test_validate_cookie_name_invalid () =
2717
+
(* Invalid: control chars, separators, spaces *)
2718
+
let invalid_names =
2721
+
("my cookie", "space");
2722
+
("cookie=value", "equals");
2723
+
("my;cookie", "semicolon");
2724
+
("name\t", "tab");
2725
+
("(cookie)", "parens");
2726
+
("name,val", "comma");
2729
+
List.iter (fun (name, reason) ->
2730
+
match Cookeio.Validate.cookie_name name with
2731
+
| Error _ -> () (* Expected *)
2734
+
(Printf.sprintf "Name %S (%s) should be invalid" name reason))
2737
+
let test_validate_cookie_value_valid () =
2738
+
(* Valid cookie-octets or quoted values *)
2739
+
let valid_values = ["abc123"; "value!#$%&'()*+-./"; "\"quoted\""; ""] in
2740
+
List.iter (fun value ->
2741
+
match Cookeio.Validate.cookie_value value with
2744
+
Alcotest.fail (Printf.sprintf "Value %S should be valid: %s" value msg))
2747
+
let test_validate_cookie_value_invalid () =
2748
+
(* Invalid: space, comma, semicolon, backslash, unmatched quotes *)
2749
+
let invalid_values =
2751
+
("with space", "space");
2752
+
("with,comma", "comma");
2753
+
("with;semi", "semicolon");
2754
+
("back\\slash", "backslash");
2755
+
("\"unmatched", "unmatched opening quote");
2756
+
("unmatched\"", "unmatched closing quote");
2759
+
List.iter (fun (value, reason) ->
2760
+
match Cookeio.Validate.cookie_value value with
2761
+
| Error _ -> () (* Expected *)
2764
+
(Printf.sprintf "Value %S (%s) should be invalid" value reason))
2767
+
let test_validate_domain_valid () =
2768
+
(* Valid domain names and IP addresses *)
2769
+
let valid_domains =
2770
+
["example.com"; "sub.example.com"; ".example.com"; "192.168.1.1"; "::1"]
2772
+
List.iter (fun domain ->
2773
+
match Cookeio.Validate.domain_value domain with
2776
+
Alcotest.fail (Printf.sprintf "Domain %S should be valid: %s" domain msg))
2779
+
let test_validate_domain_invalid () =
2780
+
(* Invalid domain names - only test cases that domain-name library rejects.
2781
+
Note: domain-name library has specific rules that may differ from what
2782
+
we might expect from the RFC. *)
2783
+
let invalid_domains =
2786
+
(* Note: "-invalid.com" and "invalid-.com" are valid per domain-name library *)
2789
+
List.iter (fun (domain, reason) ->
2790
+
match Cookeio.Validate.domain_value domain with
2791
+
| Error _ -> () (* Expected *)
2794
+
(Printf.sprintf "Domain %S (%s) should be invalid" domain reason))
2797
+
let test_validate_path_valid () =
2798
+
let valid_paths = ["/"; "/path"; "/path/to/resource"; "/path?query"] in
2799
+
List.iter (fun path ->
2800
+
match Cookeio.Validate.path_value path with
2803
+
Alcotest.fail (Printf.sprintf "Path %S should be valid: %s" path msg))
2806
+
let test_validate_path_invalid () =
2807
+
let invalid_paths =
2809
+
("/path;bad", "semicolon");
2810
+
("/path\x00bad", "control char");
2813
+
List.iter (fun (path, reason) ->
2814
+
match Cookeio.Validate.path_value path with
2815
+
| Error _ -> () (* Expected *)
2818
+
(Printf.sprintf "Path %S (%s) should be invalid" path reason))
2821
+
let test_duplicate_cookie_detection () =
2822
+
Eio_mock.Backend.run @@ fun () ->
2823
+
let clock = Eio_mock.Clock.make () in
2824
+
Eio_mock.Clock.set_time clock 1000.0;
2826
+
(* Duplicate cookie names should be rejected *)
2830
+
Ptime.of_float_s (Eio.Time.now clock)
2831
+
|> Option.value ~default:Ptime.epoch)
2832
+
~domain:"example.com" ~path:"/" "session=abc; theme=dark; session=xyz"
2836
+
(* Should mention duplicate *)
2837
+
let contains_dup = String.lowercase_ascii msg |> fun s ->
2838
+
try let _ = Str.search_forward (Str.regexp_string "duplicate") s 0 in true
2839
+
with Not_found -> false
2841
+
Alcotest.(check bool) "error mentions duplicate" true contains_dup
2842
+
| Ok _ -> Alcotest.fail "Should reject duplicate cookie names"
2844
+
let test_validation_error_messages () =
2845
+
Eio_mock.Backend.run @@ fun () ->
2846
+
let clock = Eio_mock.Clock.make () in
2847
+
Eio_mock.Clock.set_time clock 1000.0;
2849
+
(* Test that error messages are descriptive *)
2852
+
("=noname", "Cookie name is empty");
2853
+
("bad cookie=value", "invalid characters");
2854
+
("name=val ue", "invalid characters");
2857
+
List.iter (fun (header, expected_substring) ->
2859
+
of_set_cookie_header
2861
+
Ptime.of_float_s (Eio.Time.now clock)
2862
+
|> Option.value ~default:Ptime.epoch)
2863
+
~domain:"example.com" ~path:"/" header
2866
+
let has_substring =
2868
+
let _ = Str.search_forward
2869
+
(Str.regexp_string expected_substring) msg 0 in
2871
+
with Not_found -> false
2873
+
Alcotest.(check bool)
2874
+
(Printf.sprintf "error for %S mentions %S" header expected_substring)
2875
+
true has_substring
2877
+
Alcotest.fail (Printf.sprintf "Should reject %S" header))
2880
+
(* ============================================================================ *)
2881
+
(* Public Suffix Validation Tests (RFC 6265 Section 5.3, Step 5) *)
2882
+
(* ============================================================================ *)
2884
+
let test_public_suffix_rejection () =
2885
+
Eio_mock.Backend.run @@ fun () ->
2886
+
let clock = Eio_mock.Clock.make () in
2887
+
Eio_mock.Clock.set_time clock 1000.0;
2889
+
(* Setting a cookie for a public suffix (TLD) should be rejected *)
2892
+
(* (request_domain, cookie_domain, description) *)
2893
+
("www.example.com", "com", "TLD .com");
2894
+
("www.example.co.uk", "co.uk", "ccTLD .co.uk");
2895
+
("foo.bar.github.io", "github.io", "private domain github.io");
2900
+
(fun (request_domain, cookie_domain, description) ->
2901
+
let header = Printf.sprintf "session=abc; Domain=.%s" cookie_domain in
2903
+
of_set_cookie_header
2905
+
Ptime.of_float_s (Eio.Time.now clock)
2906
+
|> Option.value ~default:Ptime.epoch)
2907
+
~domain:request_domain ~path:"/" header
2911
+
(* Should mention public suffix *)
2913
+
String.lowercase_ascii msg |> fun s ->
2915
+
let _ = Str.search_forward (Str.regexp_string "public suffix") s 0 in
2917
+
with Not_found -> false
2919
+
Alcotest.(check bool)
2920
+
(Printf.sprintf "%s: error mentions public suffix" description)
2924
+
(Printf.sprintf "Should reject cookie for %s" description))
2927
+
let test_public_suffix_allowed_when_exact_match () =
2928
+
Eio_mock.Backend.run @@ fun () ->
2929
+
let clock = Eio_mock.Clock.make () in
2930
+
Eio_mock.Clock.set_time clock 1000.0;
2932
+
(* If request host exactly matches the public suffix domain, allow it.
2933
+
This is rare but possible for private domains like blogspot.com *)
2934
+
let header = "session=abc; Domain=.blogspot.com" in
2936
+
of_set_cookie_header
2938
+
Ptime.of_float_s (Eio.Time.now clock)
2939
+
|> Option.value ~default:Ptime.epoch)
2940
+
~domain:"blogspot.com" ~path:"/" header
2942
+
Alcotest.(check bool)
2943
+
"exact match allows public suffix" true
2944
+
(Result.is_ok result)
2946
+
let test_non_public_suffix_allowed () =
2947
+
Eio_mock.Backend.run @@ fun () ->
2948
+
let clock = Eio_mock.Clock.make () in
2949
+
Eio_mock.Clock.set_time clock 1000.0;
2951
+
(* Normal domain (not a public suffix) should be allowed *)
2954
+
("www.example.com", "example.com", "registrable domain");
2955
+
("sub.example.com", "example.com", "parent of subdomain");
2956
+
("www.example.co.uk", "example.co.uk", "registrable domain under ccTLD");
2961
+
(fun (request_domain, cookie_domain, description) ->
2962
+
let header = Printf.sprintf "session=abc; Domain=.%s" cookie_domain in
2964
+
of_set_cookie_header
2966
+
Ptime.of_float_s (Eio.Time.now clock)
2967
+
|> Option.value ~default:Ptime.epoch)
2968
+
~domain:request_domain ~path:"/" header
2972
+
Alcotest.(check string)
2973
+
(Printf.sprintf "%s: domain correct" description)
2974
+
cookie_domain (Cookeio.domain cookie)
2977
+
(Printf.sprintf "%s should be allowed: %s" description msg))
2980
+
let test_public_suffix_no_domain_attribute () =
2981
+
Eio_mock.Backend.run @@ fun () ->
2982
+
let clock = Eio_mock.Clock.make () in
2983
+
Eio_mock.Clock.set_time clock 1000.0;
2985
+
(* Cookie without Domain attribute should always be allowed (host-only) *)
2986
+
let header = "session=abc; Secure; HttpOnly" in
2988
+
of_set_cookie_header
2990
+
Ptime.of_float_s (Eio.Time.now clock)
2991
+
|> Option.value ~default:Ptime.epoch)
2992
+
~domain:"www.example.com" ~path:"/" header
2996
+
Alcotest.(check bool) "host_only is true" true (Cookeio.host_only cookie);
2997
+
Alcotest.(check string)
2998
+
"domain is request domain" "www.example.com"
2999
+
(Cookeio.domain cookie)
3000
+
| Error msg -> Alcotest.fail ("Should allow host-only cookie: " ^ msg)
3002
+
let test_public_suffix_ip_address_bypass () =
3003
+
Eio_mock.Backend.run @@ fun () ->
3004
+
let clock = Eio_mock.Clock.make () in
3005
+
Eio_mock.Clock.set_time clock 1000.0;
3007
+
(* IP addresses should bypass PSL check *)
3008
+
let header = "session=abc; Domain=192.168.1.1" in
3010
+
of_set_cookie_header
3012
+
Ptime.of_float_s (Eio.Time.now clock)
3013
+
|> Option.value ~default:Ptime.epoch)
3014
+
~domain:"192.168.1.1" ~path:"/" header
3016
+
Alcotest.(check bool)
3017
+
"IP address bypasses PSL" true
3018
+
(Result.is_ok result)
3020
+
let test_public_suffix_case_insensitive () =
3021
+
Eio_mock.Backend.run @@ fun () ->
3022
+
let clock = Eio_mock.Clock.make () in
3023
+
Eio_mock.Clock.set_time clock 1000.0;
3025
+
(* Public suffix check should be case-insensitive *)
3026
+
let header = "session=abc; Domain=.COM" in
3028
+
of_set_cookie_header
3030
+
Ptime.of_float_s (Eio.Time.now clock)
3031
+
|> Option.value ~default:Ptime.epoch)
3032
+
~domain:"www.example.COM" ~path:"/" header
3034
+
Alcotest.(check bool)
3035
+
"uppercase TLD still rejected" true
3036
+
(Result.is_error result)
Eio_main.run @@ fun env ->
···
test_case "Cookie expiry with mock clock" `Quick
test_cookie_expiry_with_mock_clock;
3065
+
test_case "get_cookies filters expired cookies" `Quick
3066
+
test_get_cookies_filters_expired;
test_case "Max-Age parsing with mock clock" `Quick
test_max_age_parsing_with_mock_clock;
test_case "Last access time with mock clock" `Quick
···
test_case "IPv6 exact match" `Quick test_ipv6_exact_match;
test_case "IPv6 full format" `Quick test_ipv6_full_format;
test_case "IP vs hostname behavior" `Quick test_ip_vs_hostname;
3210
+
( "rfc6265_validation",
3212
+
test_case "valid cookie names" `Quick test_validate_cookie_name_valid;
3213
+
test_case "invalid cookie names" `Quick test_validate_cookie_name_invalid;
3214
+
test_case "valid cookie values" `Quick test_validate_cookie_value_valid;
3215
+
test_case "invalid cookie values" `Quick test_validate_cookie_value_invalid;
3216
+
test_case "valid domain values" `Quick test_validate_domain_valid;
3217
+
test_case "invalid domain values" `Quick test_validate_domain_invalid;
3218
+
test_case "valid path values" `Quick test_validate_path_valid;
3219
+
test_case "invalid path values" `Quick test_validate_path_invalid;
3220
+
test_case "duplicate cookie detection" `Quick test_duplicate_cookie_detection;
3221
+
test_case "validation error messages" `Quick test_validation_error_messages;
3223
+
( "cookie_ordering",
3225
+
test_case "ordering by path length" `Quick
3226
+
test_cookie_ordering_by_path_length;
3227
+
test_case "ordering by creation time" `Quick
3228
+
test_cookie_ordering_by_creation_time;
3229
+
test_case "ordering combined" `Quick test_cookie_ordering_combined;
3231
+
( "creation_time_preservation",
3233
+
test_case "preserved on update" `Quick
3234
+
test_creation_time_preserved_on_update;
3235
+
test_case "preserved in add_original" `Quick
3236
+
test_creation_time_preserved_add_original;
3237
+
test_case "new cookie keeps time" `Quick test_creation_time_new_cookie;
3239
+
( "public_suffix_validation",
3241
+
test_case "reject public suffix domains" `Quick
3242
+
test_public_suffix_rejection;
3243
+
test_case "allow exact match on public suffix" `Quick
3244
+
test_public_suffix_allowed_when_exact_match;
3245
+
test_case "allow non-public-suffix domains" `Quick
3246
+
test_non_public_suffix_allowed;
3247
+
test_case "no Domain attribute bypasses PSL" `Quick
3248
+
test_public_suffix_no_domain_attribute;
3249
+
test_case "IP address bypasses PSL" `Quick
3250
+
test_public_suffix_ip_address_bypass;
3251
+
test_case "case insensitive check" `Quick
3252
+
test_public_suffix_case_insensitive;