···
"{ name=%S; value=%S; domain=%S; path=%S; secure=%b; http_only=%b; \
8
-
expires=%a; same_site=%a }"
9
-
(Cookeio.name c) (Cookeio.value c) (Cookeio.domain c) (Cookeio.path c) (Cookeio.secure c) (Cookeio.http_only c)
8
+
expires=%a; max_age=%a; same_site=%a }"
9
+
(Cookeio.name c) (Cookeio.value c) (Cookeio.domain c) (Cookeio.path c)
10
+
(Cookeio.secure c) (Cookeio.http_only c)
(Format.pp_print_option Ptime.pp)
13
+
(Format.pp_print_option Ptime.Span.pp)
(Format.pp_print_option (fun ppf -> function
| `Strict -> Format.pp_print_string ppf "Strict"
| `Lax -> Format.pp_print_string ppf "Lax"
| `None -> Format.pp_print_string ppf "None"))
18
-
Cookeio.name c1 = Cookeio.name c2 && Cookeio.value c1 = Cookeio.value c2 && Cookeio.domain c1 = Cookeio.domain c2
19
-
&& Cookeio.path c1 = Cookeio.path c2 && Cookeio.secure c1 = Cookeio.secure c2
21
+
Cookeio.name c1 = Cookeio.name c2
22
+
&& Cookeio.value c1 = Cookeio.value c2
23
+
&& Cookeio.domain c1 = Cookeio.domain c2
24
+
&& Cookeio.path c1 = Cookeio.path c2
25
+
&& Cookeio.secure c1 = Cookeio.secure c2
&& Cookeio.http_only c1 = Cookeio.http_only c2
&& Option.equal Ptime.equal (Cookeio.expires c1) (Cookeio.expires c2)
28
+
&& Option.equal Ptime.Span.equal (Cookeio.max_age c1) (Cookeio.max_age c2)
&& Option.equal ( = ) (Cookeio.same_site c1) (Cookeio.same_site c2))
let test_load_mozilla_cookies env =
···
(* Test cookie-1: session cookie on exact domain *)
let cookie1 = find_cookie "cookie-1" in
52
-
Alcotest.(check string) "cookie-1 domain" "example.com" (Cookeio.domain cookie1);
59
+
Alcotest.(check string)
60
+
"cookie-1 domain" "example.com" (Cookeio.domain cookie1);
Alcotest.(check string) "cookie-1 path" "/foo/" (Cookeio.path cookie1);
Alcotest.(check string) "cookie-1 name" "cookie-1" (Cookeio.name cookie1);
Alcotest.(check string) "cookie-1 value" "v$1" (Cookeio.value cookie1);
···
| `Lax -> Format.pp_print_string ppf "Lax"
| `None -> Format.pp_print_string ppf "None")
69
-
"cookie-1 same_site" None (Cookeio.same_site cookie1);
77
+
"cookie-1 same_site" None
78
+
(Cookeio.same_site cookie1);
(* Test cookie-2: session cookie on subdomain pattern *)
let cookie2 = find_cookie "cookie-2" in
73
-
Alcotest.(check string) "cookie-2 domain" ".example.com" (Cookeio.domain cookie2);
82
+
Alcotest.(check string)
83
+
"cookie-2 domain" "example.com" (Cookeio.domain cookie2);
Alcotest.(check string) "cookie-2 path" "/foo/" (Cookeio.path cookie2);
Alcotest.(check string) "cookie-2 name" "cookie-2" (Cookeio.name cookie2);
Alcotest.(check string) "cookie-2 value" "v$2" (Cookeio.value cookie2);
···
(* Test cookie-3: non-session cookie with expiry *)
let cookie3 = find_cookie "cookie-3" in
let expected_expiry = Ptime.of_float_s 1257894000.0 in
85
-
Alcotest.(check string) "cookie-3 domain" "example.com" (Cookeio.domain cookie3);
95
+
Alcotest.(check string)
96
+
"cookie-3 domain" "example.com" (Cookeio.domain cookie3);
Alcotest.(check string) "cookie-3 path" "/foo/" (Cookeio.path cookie3);
Alcotest.(check string) "cookie-3 name" "cookie-3" (Cookeio.name cookie3);
Alcotest.(check string) "cookie-3 value" "v$3" (Cookeio.value cookie3);
···
(* Test cookie-4: another non-session cookie *)
let cookie4 = find_cookie "cookie-4" in
96
-
Alcotest.(check string) "cookie-4 domain" "example.com" (Cookeio.domain cookie4);
107
+
Alcotest.(check string)
108
+
"cookie-4 domain" "example.com" (Cookeio.domain cookie4);
Alcotest.(check string) "cookie-4 path" "/foo/" (Cookeio.path cookie4);
Alcotest.(check string) "cookie-4 name" "cookie-4" (Cookeio.name cookie4);
Alcotest.(check string) "cookie-4 value" "v$4" (Cookeio.value cookie4);
···
(* Test cookie-5: secure cookie *)
let cookie5 = find_cookie "cookie-5" in
107
-
Alcotest.(check string) "cookie-5 domain" "example.com" (Cookeio.domain cookie5);
119
+
Alcotest.(check string)
120
+
"cookie-5 domain" "example.com" (Cookeio.domain cookie5);
Alcotest.(check string) "cookie-5 path" "/foo/" (Cookeio.path cookie5);
Alcotest.(check string) "cookie-5 name" "cookie-5" (Cookeio.name cookie5);
Alcotest.(check string) "cookie-5 value" "v$5" (Cookeio.value cookie5);
···
(* Verify a few key cookies are loaded correctly *)
let cookie1 = find_cookie "cookie-1" in
Alcotest.(check string) "file cookie-1 value" "v$1" (Cookeio.value cookie1);
132
-
Alcotest.(check string) "file cookie-1 domain" "example.com" (Cookeio.domain cookie1);
145
+
Alcotest.(check string)
146
+
"file cookie-1 domain" "example.com" (Cookeio.domain cookie1);
Alcotest.(check bool) "file cookie-1 secure" false (Cookeio.secure cookie1);
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
"file cookie-1 expires" None (Cookeio.expires cookie1);
···
(* Verify subdomain cookie *)
let cookie2 = find_cookie "cookie-2" in
146
-
Alcotest.(check string) "file cookie-2 domain" ".example.com" (Cookeio.domain cookie2);
160
+
Alcotest.(check string)
161
+
"file cookie-2 domain" "example.com" (Cookeio.domain cookie2);
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
"file cookie-2 expires" None (Cookeio.expires cookie2)
···
(* Add test cookies with different domain patterns *)
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"exact" ~value:"test1"
157
-
~secure:false ~http_only:false ?expires:None ?same_site:None
172
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
~creation_time:Ptime.epoch ~last_access:Ptime.epoch ()
161
-
Cookeio.make ~domain:".example.com" ~path:"/" ~name:"subdomain" ~value:"test2"
162
-
~secure:false ~http_only:false ?expires:None ?same_site:None
163
-
~creation_time:Ptime.epoch ~last_access:Ptime.epoch ()
176
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"subdomain"
177
+
~value:"test2" ~secure:false ~http_only:false ?expires:None
178
+
?same_site:None ?max_age:None ~creation_time:Ptime.epoch
179
+
~last_access:Ptime.epoch ()
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"secure" ~value:"test3"
167
-
~secure:true ~http_only:false ?expires:None ?same_site:None
183
+
~secure:true ~http_only:false ?expires:None ?same_site:None ?max_age:None
~creation_time:Ptime.epoch ~last_access:Ptime.epoch ()
···
add_cookie jar subdomain_cookie;
add_cookie jar secure_cookie;
175
-
(* Test exact domain matching *)
191
+
(* Test exact domain matching - all three cookies should match example.com *)
get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
···
Alcotest.(check int) "https cookies count" 3 (List.length cookies_https);
186
-
(* Test subdomain matching *)
202
+
(* Test subdomain matching - all cookies should match subdomains now *)
get_cookies jar ~clock ~domain:"sub.example.com" ~path:"/" ~is_secure:false
190
-
Alcotest.(check int) "subdomain cookies count" 1 (List.length cookies_sub);
191
-
let sub_cookie = List.hd cookies_sub in
192
-
Alcotest.(check string) "subdomain cookie name" "subdomain" (Cookeio.name sub_cookie)
206
+
Alcotest.(check int) "subdomain cookies count" 2 (List.length cookies_sub)
let clock = Eio.Stdenv.clock env in
···
212
-
Cookeio.make ~domain:"example.com" ~path:"/test/" ~name:"test" ~value:"value"
213
-
~secure:true ~http_only:false ?expires:(Ptime.of_float_s 1257894000.0)
214
-
~same_site:`Strict ~creation_time:Ptime.epoch ~last_access:Ptime.epoch ()
226
+
Cookeio.make ~domain:"example.com" ~path:"/test/" ~name:"test"
227
+
~value:"value" ~secure:true ~http_only:false
228
+
?expires:(Ptime.of_float_s 1257894000.0)
229
+
~same_site:`Strict ?max_age:None ~creation_time:Ptime.epoch
230
+
~last_access:Ptime.epoch ()
add_cookie jar test_cookie;
···
let cookie2 = List.hd cookies2 in
Alcotest.(check string) "round trip name" "test" (Cookeio.name cookie2);
Alcotest.(check string) "round trip value" "value" (Cookeio.value cookie2);
228
-
Alcotest.(check string) "round trip domain" "example.com" (Cookeio.domain cookie2);
244
+
Alcotest.(check string)
245
+
"round trip domain" "example.com" (Cookeio.domain cookie2);
Alcotest.(check string) "round trip path" "/test/" (Cookeio.path cookie2);
Alcotest.(check bool) "round trip secure" true (Cookeio.secure cookie2);
(* Note: http_only and same_site are lost in Mozilla format *)
···
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"expires_soon"
~value:"value1" ~secure:false ~http_only:false ~expires:expires_soon
268
+
?same_site:None ?max_age:None
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
···
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"expires_later"
~value:"value2" ~secure:false ~http_only:false ~expires:expires_later
279
+
?same_site:None ?max_age:None
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
···
(* Add a session cookie (no expiry) *)
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"session" ~value:"value3"
271
-
~secure:false ~http_only:false ?expires:None ?same_site:None
288
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
···
let cookies = get_all_cookies jar in
let names = List.map Cookeio.name cookies |> List.sort String.compare in
Alcotest.(check (list string))
292
-
"remaining cookies after 1600s" [ "expires_later"; "session" ] names;
309
+
"remaining cookies after 1600s"
310
+
[ "expires_later"; "session" ]
(* Advance time to 2100.0 - second cookie should expire *)
Eio_mock.Clock.set_time clock 2100.0;
···
Alcotest.(check int) "after second expiry" 1 (count jar);
let remaining = get_all_cookies jar in
301
-
Alcotest.(check string) "only session cookie remains" "session"
320
+
Alcotest.(check string)
321
+
"only session cookie remains" "session"
(Cookeio.name (List.hd remaining))
let test_max_age_parsing_with_mock_clock () =
···
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"value"
348
-
~secure:false ~http_only:false ?expires:None ?same_site:None
368
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
~creation_time:(Ptime.of_float_s 3000.0 |> Option.get)
~last_access:(Ptime.of_float_s 3000.0 |> Option.get)
···
let cookie = Option.get cookie_opt in
Alcotest.(check string) "cookie name" "id" (Cookeio.name cookie);
Alcotest.(check string) "cookie value" "xyz789" (Cookeio.value cookie);
397
-
Alcotest.(check string) "cookie domain" ".example.com" (Cookeio.domain cookie);
417
+
Alcotest.(check string) "cookie domain" "example.com" (Cookeio.domain cookie);
Alcotest.(check string) "cookie path" "/" (Cookeio.path cookie);
(* Verify expires is parsed correctly *)
401
-
Alcotest.(check bool) "has expiry" true
421
+
Alcotest.(check bool)
(Option.is_some (Cookeio.expires cookie));
(* Verify the specific expiry time parsed from the RFC3339 date *)
···
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" invalid_header
425
-
Alcotest.(check bool) "invalid cookie rejected" true (Option.is_none cookie_opt);
446
+
Alcotest.(check bool)
447
+
"invalid cookie rejected" true
448
+
(Option.is_none cookie_opt);
(* This should be accepted: SameSite=None with Secure *)
let valid_header = "token=abc; SameSite=None; Secure" in
···
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" valid_header
433
-
Alcotest.(check bool) "valid cookie accepted" true (Option.is_some cookie_opt2);
456
+
Alcotest.(check bool)
457
+
"valid cookie accepted" true
458
+
(Option.is_some cookie_opt2);
let cookie = Option.get cookie_opt2 in
Alcotest.(check bool) "cookie is secure" true (Cookeio.secure cookie);
···
"samesite is None" (Some `None) (Cookeio.same_site cookie)
473
+
let test_domain_normalization () =
474
+
Eio_mock.Backend.run @@ fun () ->
475
+
let clock = Eio_mock.Clock.make () in
476
+
Eio_mock.Clock.set_time clock 1000.0;
478
+
(* Test parsing ".example.com" stores as "example.com" *)
479
+
let header = "test=value; Domain=.example.com" in
481
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
483
+
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
484
+
let cookie = Option.get cookie_opt in
485
+
Alcotest.(check string)
486
+
"domain normalized" "example.com" (Cookeio.domain cookie);
488
+
(* Test round-trip through Mozilla format normalizes domains *)
489
+
let jar = create () in
491
+
Cookeio.make ~domain:".example.com" ~path:"/" ~name:"test" ~value:"val"
492
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
493
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
494
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
497
+
add_cookie jar test_cookie;
499
+
let mozilla_format = to_mozilla_format jar in
500
+
let jar2 = from_mozilla_format ~clock mozilla_format in
501
+
let cookies2 = get_all_cookies jar2 in
502
+
Alcotest.(check int) "one cookie" 1 (List.length cookies2);
503
+
Alcotest.(check string)
504
+
"domain normalized after round-trip" "example.com"
505
+
(Cookeio.domain (List.hd cookies2))
507
+
let test_max_age_stored_separately () =
508
+
Eio_mock.Backend.run @@ fun () ->
509
+
let clock = Eio_mock.Clock.make () in
510
+
Eio_mock.Clock.set_time clock 5000.0;
512
+
(* Parse a Set-Cookie header with Max-Age *)
513
+
let header = "session=abc123; Max-Age=3600" in
515
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
517
+
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
519
+
let cookie = Option.get cookie_opt in
521
+
(* Verify max_age is stored as a Ptime.Span *)
522
+
Alcotest.(check bool)
523
+
"max_age is set" true
524
+
(Option.is_some (Cookeio.max_age cookie));
525
+
let max_age_span = Option.get (Cookeio.max_age cookie) in
526
+
Alcotest.(check (option int))
527
+
"max_age is 3600 seconds" (Some 3600)
528
+
(Ptime.Span.to_int_s max_age_span);
530
+
(* Verify expires is also computed correctly *)
531
+
let expected_expiry = Ptime.of_float_s 8600.0 in
532
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
533
+
"expires computed from max-age" expected_expiry (Cookeio.expires cookie)
535
+
let test_max_age_negative_becomes_zero () =
536
+
Eio_mock.Backend.run @@ fun () ->
537
+
let clock = Eio_mock.Clock.make () in
538
+
Eio_mock.Clock.set_time clock 5000.0;
540
+
(* Parse a Set-Cookie header with negative Max-Age *)
541
+
let header = "session=abc123; Max-Age=-100" in
543
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
545
+
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
547
+
let cookie = Option.get cookie_opt in
549
+
(* Verify max_age is stored as 0 per RFC 6265 *)
550
+
Alcotest.(check bool)
551
+
"max_age is set" true
552
+
(Option.is_some (Cookeio.max_age cookie));
553
+
let max_age_span = Option.get (Cookeio.max_age cookie) in
554
+
Alcotest.(check (option int))
555
+
"negative max_age becomes 0" (Some 0)
556
+
(Ptime.Span.to_int_s max_age_span);
558
+
(* Verify expires is computed with 0 seconds *)
559
+
let expected_expiry = Ptime.of_float_s 5000.0 in
560
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
561
+
"expires computed with 0 seconds" expected_expiry (Cookeio.expires cookie)
563
+
let string_contains_substring s sub =
565
+
let len = String.length sub in
567
+
if i + len > String.length s then false
568
+
else if String.sub s i len = sub then true
569
+
else search (i + 1)
574
+
let test_make_set_cookie_header_includes_max_age () =
575
+
Eio_mock.Backend.run @@ fun () ->
576
+
let clock = Eio_mock.Clock.make () in
577
+
Eio_mock.Clock.set_time clock 5000.0;
579
+
(* Create a cookie with max_age *)
580
+
let max_age_span = Ptime.Span.of_int_s 3600 in
581
+
let expires_time = Ptime.of_float_s 8600.0 |> Option.get in
583
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"session" ~value:"abc123"
584
+
~secure:true ~http_only:true ?expires:(Some expires_time)
585
+
?max_age:(Some max_age_span) ?same_site:(Some `Strict)
586
+
~creation_time:(Ptime.of_float_s 5000.0 |> Option.get)
587
+
~last_access:(Ptime.of_float_s 5000.0 |> Option.get)
591
+
let header = make_set_cookie_header cookie in
593
+
(* Verify the header includes Max-Age *)
594
+
Alcotest.(check bool)
595
+
"header includes Max-Age" true
596
+
(string_contains_substring header "Max-Age=3600");
598
+
(* Verify the header includes Expires *)
599
+
Alcotest.(check bool)
600
+
"header includes Expires" true
601
+
(string_contains_substring header "Expires=");
603
+
(* Verify the header includes other attributes *)
604
+
Alcotest.(check bool)
605
+
"header includes Secure" true
606
+
(string_contains_substring header "Secure");
607
+
Alcotest.(check bool)
608
+
"header includes HttpOnly" true
609
+
(string_contains_substring header "HttpOnly");
610
+
Alcotest.(check bool)
611
+
"header includes SameSite" true
612
+
(string_contains_substring header "SameSite=Strict")
614
+
let test_max_age_round_trip () =
615
+
Eio_mock.Backend.run @@ fun () ->
616
+
let clock = Eio_mock.Clock.make () in
617
+
Eio_mock.Clock.set_time clock 5000.0;
619
+
(* Parse a cookie with Max-Age *)
620
+
let header = "session=xyz; Max-Age=7200; Secure; HttpOnly" in
622
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
624
+
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
625
+
let cookie = Option.get cookie_opt in
627
+
(* Generate Set-Cookie header from the cookie *)
628
+
let set_cookie_header = make_set_cookie_header cookie in
630
+
(* Parse it back *)
631
+
Eio_mock.Clock.set_time clock 5000.0;
632
+
(* Reset clock to same time *)
634
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" set_cookie_header
636
+
Alcotest.(check bool) "cookie re-parsed" true (Option.is_some cookie2_opt);
637
+
let cookie2 = Option.get cookie2_opt in
639
+
(* Verify max_age is preserved *)
640
+
Alcotest.(check (option int))
641
+
"max_age preserved"
642
+
(Ptime.Span.to_int_s (Option.get (Cookeio.max_age cookie)))
643
+
(Ptime.Span.to_int_s (Option.get (Cookeio.max_age cookie2)))
645
+
let test_domain_matching () =
646
+
Eio_mock.Backend.run @@ fun () ->
647
+
let clock = Eio_mock.Clock.make () in
648
+
Eio_mock.Clock.set_time clock 2000.0;
650
+
let jar = create () in
652
+
(* Create a cookie with domain "example.com" *)
654
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"value"
655
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
656
+
~creation_time:(Ptime.of_float_s 2000.0 |> Option.get)
657
+
~last_access:(Ptime.of_float_s 2000.0 |> Option.get)
660
+
add_cookie jar cookie;
662
+
(* Test "example.com" cookie matches "example.com" request *)
664
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
666
+
Alcotest.(check int) "matches exact domain" 1 (List.length cookies1);
668
+
(* Test "example.com" cookie matches "sub.example.com" request *)
670
+
get_cookies jar ~clock ~domain:"sub.example.com" ~path:"/" ~is_secure:false
672
+
Alcotest.(check int) "matches subdomain" 1 (List.length cookies2);
674
+
(* Test "example.com" cookie matches "deep.sub.example.com" request *)
676
+
get_cookies jar ~clock ~domain:"deep.sub.example.com" ~path:"/"
679
+
Alcotest.(check int) "matches deep subdomain" 1 (List.length cookies3);
681
+
(* Test "example.com" cookie doesn't match "notexample.com" *)
683
+
get_cookies jar ~clock ~domain:"notexample.com" ~path:"/" ~is_secure:false
685
+
Alcotest.(check int) "doesn't match different domain" 0 (List.length cookies4);
687
+
(* Test "example.com" cookie doesn't match "fakeexample.com" *)
689
+
get_cookies jar ~clock ~domain:"fakeexample.com" ~path:"/" ~is_secure:false
691
+
Alcotest.(check int) "doesn't match prefix domain" 0 (List.length cookies5)
693
+
(** {1 HTTP Date Parsing Tests} *)
695
+
let test_http_date_fmt1 () =
696
+
Eio_mock.Backend.run @@ fun () ->
697
+
let clock = Eio_mock.Clock.make () in
698
+
Eio_mock.Clock.set_time clock 1000.0;
700
+
(* Test FMT1: "Wed, 21 Oct 2015 07:28:00 GMT" (RFC 1123) *)
701
+
let header = "session=abc; Expires=Wed, 21 Oct 2015 07:28:00 GMT" in
703
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
705
+
Alcotest.(check bool) "FMT1 cookie parsed" true (Option.is_some cookie_opt);
707
+
let cookie = Option.get cookie_opt in
708
+
Alcotest.(check bool)
709
+
"FMT1 has expiry" true
710
+
(Option.is_some (Cookeio.expires cookie));
712
+
(* Verify the parsed time matches expected value *)
713
+
let expected = Ptime.of_date_time ((2015, 10, 21), ((07, 28, 00), 0)) in
714
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
715
+
"FMT1 expiry correct" expected (Cookeio.expires cookie)
717
+
let test_http_date_fmt2 () =
718
+
Eio_mock.Backend.run @@ fun () ->
719
+
let clock = Eio_mock.Clock.make () in
720
+
Eio_mock.Clock.set_time clock 1000.0;
722
+
(* Test FMT2: "Wednesday, 21-Oct-15 07:28:00 GMT" (RFC 850 with abbreviated year) *)
723
+
let header = "session=abc; Expires=Wednesday, 21-Oct-15 07:28:00 GMT" in
725
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
727
+
Alcotest.(check bool) "FMT2 cookie parsed" true (Option.is_some cookie_opt);
729
+
let cookie = Option.get cookie_opt in
730
+
Alcotest.(check bool)
731
+
"FMT2 has expiry" true
732
+
(Option.is_some (Cookeio.expires cookie));
734
+
(* Year 15 should be normalized to 2015 *)
735
+
let expected = Ptime.of_date_time ((2015, 10, 21), ((07, 28, 00), 0)) in
736
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
737
+
"FMT2 expiry correct with year normalization" expected
738
+
(Cookeio.expires cookie)
740
+
let test_http_date_fmt3 () =
741
+
Eio_mock.Backend.run @@ fun () ->
742
+
let clock = Eio_mock.Clock.make () in
743
+
Eio_mock.Clock.set_time clock 1000.0;
745
+
(* Test FMT3: "Wed Oct 21 07:28:00 2015" (asctime) *)
746
+
let header = "session=abc; Expires=Wed Oct 21 07:28:00 2015" in
748
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
750
+
Alcotest.(check bool) "FMT3 cookie parsed" true (Option.is_some cookie_opt);
752
+
let cookie = Option.get cookie_opt in
753
+
Alcotest.(check bool)
754
+
"FMT3 has expiry" true
755
+
(Option.is_some (Cookeio.expires cookie));
757
+
let expected = Ptime.of_date_time ((2015, 10, 21), ((07, 28, 00), 0)) in
758
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
759
+
"FMT3 expiry correct" expected (Cookeio.expires cookie)
761
+
let test_http_date_fmt4 () =
762
+
Eio_mock.Backend.run @@ fun () ->
763
+
let clock = Eio_mock.Clock.make () in
764
+
Eio_mock.Clock.set_time clock 1000.0;
766
+
(* Test FMT4: "Wed, 21-Oct-2015 07:28:00 GMT" (variant) *)
767
+
let header = "session=abc; Expires=Wed, 21-Oct-2015 07:28:00 GMT" in
769
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
771
+
Alcotest.(check bool) "FMT4 cookie parsed" true (Option.is_some cookie_opt);
773
+
let cookie = Option.get cookie_opt in
774
+
Alcotest.(check bool)
775
+
"FMT4 has expiry" true
776
+
(Option.is_some (Cookeio.expires cookie));
778
+
let expected = Ptime.of_date_time ((2015, 10, 21), ((07, 28, 00), 0)) in
779
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
780
+
"FMT4 expiry correct" expected (Cookeio.expires cookie)
782
+
let test_abbreviated_year_69_to_99 () =
783
+
Eio_mock.Backend.run @@ fun () ->
784
+
let clock = Eio_mock.Clock.make () in
785
+
Eio_mock.Clock.set_time clock 1000.0;
787
+
(* Year 95 should become 1995 *)
788
+
let header = "session=abc; Expires=Wed, 21-Oct-95 07:28:00 GMT" in
790
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
792
+
let cookie = Option.get cookie_opt in
793
+
let expected = Ptime.of_date_time ((1995, 10, 21), ((07, 28, 00), 0)) in
794
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
795
+
"year 95 becomes 1995" expected (Cookeio.expires cookie);
797
+
(* Year 69 should become 1969 *)
798
+
let header2 = "session=abc; Expires=Wed, 10-Sep-69 20:00:00 GMT" in
800
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header2
802
+
let cookie2 = Option.get cookie_opt2 in
803
+
let expected2 = Ptime.of_date_time ((1969, 9, 10), ((20, 0, 0), 0)) in
804
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
805
+
"year 69 becomes 1969" expected2 (Cookeio.expires cookie2);
807
+
(* Year 99 should become 1999 *)
808
+
let header3 = "session=abc; Expires=Thu, 10-Sep-99 20:00:00 GMT" in
810
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header3
812
+
let cookie3 = Option.get cookie_opt3 in
813
+
let expected3 = Ptime.of_date_time ((1999, 9, 10), ((20, 0, 0), 0)) in
814
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
815
+
"year 99 becomes 1999" expected3 (Cookeio.expires cookie3)
817
+
let test_abbreviated_year_0_to_68 () =
818
+
Eio_mock.Backend.run @@ fun () ->
819
+
let clock = Eio_mock.Clock.make () in
820
+
Eio_mock.Clock.set_time clock 1000.0;
822
+
(* Year 25 should become 2025 *)
823
+
let header = "session=abc; Expires=Wed, 21-Oct-25 07:28:00 GMT" in
825
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
827
+
let cookie = Option.get cookie_opt in
828
+
let expected = Ptime.of_date_time ((2025, 10, 21), ((07, 28, 00), 0)) in
829
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
830
+
"year 25 becomes 2025" expected (Cookeio.expires cookie);
832
+
(* Year 0 should become 2000 *)
833
+
let header2 = "session=abc; Expires=Fri, 01-Jan-00 00:00:00 GMT" in
835
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header2
837
+
let cookie2 = Option.get cookie_opt2 in
838
+
let expected2 = Ptime.of_date_time ((2000, 1, 1), ((0, 0, 0), 0)) in
839
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
840
+
"year 0 becomes 2000" expected2 (Cookeio.expires cookie2);
842
+
(* Year 68 should become 2068 *)
843
+
let header3 = "session=abc; Expires=Thu, 10-Sep-68 20:00:00 GMT" in
845
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header3
847
+
let cookie3 = Option.get cookie_opt3 in
848
+
let expected3 = Ptime.of_date_time ((2068, 9, 10), ((20, 0, 0), 0)) in
849
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
850
+
"year 68 becomes 2068" expected3 (Cookeio.expires cookie3)
852
+
let test_rfc3339_still_works () =
853
+
Eio_mock.Backend.run @@ fun () ->
854
+
let clock = Eio_mock.Clock.make () in
855
+
Eio_mock.Clock.set_time clock 1000.0;
857
+
(* Ensure RFC 3339 format still works for backward compatibility *)
858
+
let header = "session=abc; Expires=2025-10-21T07:28:00Z" in
860
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
862
+
Alcotest.(check bool)
863
+
"RFC 3339 cookie parsed" true
864
+
(Option.is_some cookie_opt);
866
+
let cookie = Option.get cookie_opt in
867
+
Alcotest.(check bool)
868
+
"RFC 3339 has expiry" true
869
+
(Option.is_some (Cookeio.expires cookie));
871
+
(* Verify the time was parsed correctly *)
872
+
let expected = Ptime.of_rfc3339 "2025-10-21T07:28:00Z" in
873
+
match expected with
874
+
| Ok (time, _, _) ->
875
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
876
+
"RFC 3339 expiry correct" (Some time) (Cookeio.expires cookie)
877
+
| Error _ -> Alcotest.fail "Failed to parse expected RFC 3339 time"
879
+
let test_invalid_date_format_logs_warning () =
880
+
Eio_mock.Backend.run @@ fun () ->
881
+
let clock = Eio_mock.Clock.make () in
882
+
Eio_mock.Clock.set_time clock 1000.0;
884
+
(* Invalid date format should log a warning but still parse the cookie *)
885
+
let header = "session=abc; Expires=InvalidDate" in
887
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
890
+
(* Cookie should still be parsed, just without expires *)
891
+
Alcotest.(check bool)
892
+
"cookie parsed despite invalid date" true
893
+
(Option.is_some cookie_opt);
894
+
let cookie = Option.get cookie_opt in
895
+
Alcotest.(check string) "cookie name correct" "session" (Cookeio.name cookie);
896
+
Alcotest.(check string) "cookie value correct" "abc" (Cookeio.value cookie);
897
+
(* expires should be None since date was invalid *)
898
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
899
+
"expires is None for invalid date" None (Cookeio.expires cookie)
901
+
let test_case_insensitive_month_parsing () =
902
+
Eio_mock.Backend.run @@ fun () ->
903
+
let clock = Eio_mock.Clock.make () in
904
+
Eio_mock.Clock.set_time clock 1000.0;
906
+
(* Test various case combinations for month names *)
909
+
("session=abc; Expires=Wed, 21 oct 2015 07:28:00 GMT", "lowercase month");
910
+
("session=abc; Expires=Wed, 21 OCT 2015 07:28:00 GMT", "uppercase month");
911
+
("session=abc; Expires=Wed, 21 OcT 2015 07:28:00 GMT", "mixed case month");
912
+
("session=abc; Expires=Wed, 21 oCt 2015 07:28:00 GMT", "weird case month");
917
+
(fun (header, description) ->
919
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
921
+
Alcotest.(check bool)
922
+
(description ^ " parsed") true
923
+
(Option.is_some cookie_opt);
925
+
let cookie = Option.get cookie_opt in
926
+
Alcotest.(check bool)
927
+
(description ^ " has expiry")
929
+
(Option.is_some (Cookeio.expires cookie));
931
+
(* Verify the date was parsed correctly regardless of case *)
932
+
let expires = Option.get (Cookeio.expires cookie) in
933
+
let year, month, _ = Ptime.to_date expires in
934
+
Alcotest.(check int) (description ^ " year correct") 2015 year;
935
+
Alcotest.(check int)
936
+
(description ^ " month correct (October=10)")
940
+
let test_case_insensitive_gmt_parsing () =
941
+
Eio_mock.Backend.run @@ fun () ->
942
+
let clock = Eio_mock.Clock.make () in
943
+
Eio_mock.Clock.set_time clock 1000.0;
945
+
(* Test various case combinations for GMT timezone *)
948
+
("session=abc; Expires=Wed, 21 Oct 2015 07:28:00 GMT", "uppercase GMT");
949
+
("session=abc; Expires=Wed, 21 Oct 2015 07:28:00 gmt", "lowercase gmt");
950
+
("session=abc; Expires=Wed, 21 Oct 2015 07:28:00 Gmt", "mixed case Gmt");
951
+
("session=abc; Expires=Wed, 21 Oct 2015 07:28:00 GmT", "weird case GmT");
956
+
(fun (header, description) ->
958
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
960
+
Alcotest.(check bool)
961
+
(description ^ " parsed") true
962
+
(Option.is_some cookie_opt);
964
+
let cookie = Option.get cookie_opt in
965
+
Alcotest.(check bool)
966
+
(description ^ " has expiry")
968
+
(Option.is_some (Cookeio.expires cookie));
970
+
(* Verify the date was parsed correctly regardless of GMT case *)
971
+
let expires = Option.get (Cookeio.expires cookie) in
972
+
let year, month, day = Ptime.to_date expires in
973
+
Alcotest.(check int) (description ^ " year correct") 2015 year;
974
+
Alcotest.(check int)
975
+
(description ^ " month correct (October=10)")
977
+
Alcotest.(check int) (description ^ " day correct") 21 day)
980
+
(** {1 Delta Tracking Tests} *)
982
+
let test_add_original_not_in_delta () =
983
+
Eio_mock.Backend.run @@ fun () ->
984
+
let clock = Eio_mock.Clock.make () in
985
+
Eio_mock.Clock.set_time clock 1000.0;
987
+
let jar = create () in
989
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"value"
990
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
991
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
992
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
995
+
add_original jar cookie;
997
+
(* Delta should be empty *)
998
+
let delta = Cookeio.delta jar in
999
+
Alcotest.(check int) "delta is empty" 0 (List.length delta);
1001
+
(* But the cookie should be in the jar *)
1002
+
Alcotest.(check int) "jar count is 1" 1 (count jar)
1004
+
let test_add_cookie_appears_in_delta () =
1005
+
Eio_mock.Backend.run @@ fun () ->
1006
+
let clock = Eio_mock.Clock.make () in
1007
+
Eio_mock.Clock.set_time clock 1000.0;
1009
+
let jar = create () in
1011
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"value"
1012
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
1013
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1014
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
1017
+
add_cookie jar cookie;
1019
+
(* Delta should contain the cookie *)
1020
+
let delta = Cookeio.delta jar in
1021
+
Alcotest.(check int) "delta has 1 cookie" 1 (List.length delta);
1022
+
let delta_cookie = List.hd delta in
1023
+
Alcotest.(check string) "delta cookie name" "test" (Cookeio.name delta_cookie);
1024
+
Alcotest.(check string)
1025
+
"delta cookie value" "value"
1026
+
(Cookeio.value delta_cookie)
1028
+
let test_remove_original_creates_removal_cookie () =
1029
+
Eio_mock.Backend.run @@ fun () ->
1030
+
let clock = Eio_mock.Clock.make () in
1031
+
Eio_mock.Clock.set_time clock 1000.0;
1033
+
let jar = create () in
1035
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"value"
1036
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
1037
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1038
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
1041
+
add_original jar cookie;
1043
+
(* Remove the cookie *)
1044
+
Cookeio.remove jar ~clock cookie;
1046
+
(* Delta should contain a removal cookie *)
1047
+
let delta = Cookeio.delta jar in
1048
+
Alcotest.(check int) "delta has 1 removal cookie" 1 (List.length delta);
1049
+
let removal_cookie = List.hd delta in
1050
+
Alcotest.(check string)
1051
+
"removal cookie name" "test"
1052
+
(Cookeio.name removal_cookie);
1053
+
Alcotest.(check string)
1054
+
"removal cookie has empty value" ""
1055
+
(Cookeio.value removal_cookie);
1057
+
(* Check Max-Age is 0 *)
1058
+
match Cookeio.max_age removal_cookie with
1060
+
Alcotest.(check (option int))
1061
+
"removal cookie Max-Age is 0" (Some 0) (Ptime.Span.to_int_s span)
1062
+
| None -> Alcotest.fail "removal cookie should have Max-Age"
1064
+
let test_remove_delta_cookie_removes_it () =
1065
+
Eio_mock.Backend.run @@ fun () ->
1066
+
let clock = Eio_mock.Clock.make () in
1067
+
Eio_mock.Clock.set_time clock 1000.0;
1069
+
let jar = create () in
1071
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"value"
1072
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
1073
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1074
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
1077
+
add_cookie jar cookie;
1079
+
(* Remove the cookie *)
1080
+
Cookeio.remove jar ~clock cookie;
1082
+
(* Delta should be empty *)
1083
+
let delta = Cookeio.delta jar in
1084
+
Alcotest.(check int)
1085
+
"delta is empty after removing delta cookie" 0 (List.length delta)
1087
+
let test_get_cookies_combines_original_and_delta () =
1088
+
Eio_mock.Backend.run @@ fun () ->
1089
+
let clock = Eio_mock.Clock.make () in
1090
+
Eio_mock.Clock.set_time clock 1000.0;
1092
+
let jar = create () in
1094
+
(* Add an original cookie *)
1096
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"original"
1097
+
~value:"orig_val" ~secure:false ~http_only:false ?expires:None
1098
+
?same_site:None ?max_age:None
1099
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1100
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
1103
+
add_original jar original;
1105
+
(* Add a delta cookie *)
1106
+
let delta_cookie =
1107
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"delta"
1108
+
~value:"delta_val" ~secure:false ~http_only:false ?expires:None
1109
+
?same_site:None ?max_age:None
1110
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1111
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
1114
+
add_cookie jar delta_cookie;
1116
+
(* Get cookies should return both *)
1118
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
1120
+
Alcotest.(check int) "both cookies returned" 2 (List.length cookies);
1122
+
let names = List.map Cookeio.name cookies |> List.sort String.compare in
1123
+
Alcotest.(check (list string)) "cookie names" [ "delta"; "original" ] names
1125
+
let test_get_cookies_delta_takes_precedence () =
1126
+
Eio_mock.Backend.run @@ fun () ->
1127
+
let clock = Eio_mock.Clock.make () in
1128
+
Eio_mock.Clock.set_time clock 1000.0;
1130
+
let jar = create () in
1132
+
(* Add an original cookie *)
1134
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"orig_val"
1135
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
1136
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1137
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
1140
+
add_original jar original;
1142
+
(* Add a delta cookie with the same name/domain/path *)
1143
+
let delta_cookie =
1144
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"delta_val"
1145
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
1146
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1147
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
1150
+
add_cookie jar delta_cookie;
1152
+
(* Get cookies should return only the delta cookie *)
1154
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
1156
+
Alcotest.(check int) "only one cookie returned" 1 (List.length cookies);
1157
+
let cookie = List.hd cookies in
1158
+
Alcotest.(check string)
1159
+
"delta cookie value" "delta_val" (Cookeio.value cookie)
1161
+
let test_get_cookies_excludes_removal_cookies () =
1162
+
Eio_mock.Backend.run @@ fun () ->
1163
+
let clock = Eio_mock.Clock.make () in
1164
+
Eio_mock.Clock.set_time clock 1000.0;
1166
+
let jar = create () in
1168
+
(* Add an original cookie *)
1170
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"value"
1171
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
1172
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1173
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
1176
+
add_original jar original;
1179
+
Cookeio.remove jar ~clock original;
1181
+
(* Get cookies should return nothing *)
1183
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
1185
+
Alcotest.(check int) "no cookies returned" 0 (List.length cookies);
1187
+
(* But delta should have the removal cookie *)
1188
+
let delta = Cookeio.delta jar in
1189
+
Alcotest.(check int) "delta has removal cookie" 1 (List.length delta)
1191
+
let test_delta_returns_only_changed_cookies () =
1192
+
Eio_mock.Backend.run @@ fun () ->
1193
+
let clock = Eio_mock.Clock.make () in
1194
+
Eio_mock.Clock.set_time clock 1000.0;
1196
+
let jar = create () in
1198
+
(* Add original cookies *)
1200
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"orig1" ~value:"val1"
1201
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
1202
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1203
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
1206
+
add_original jar original1;
1209
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"orig2" ~value:"val2"
1210
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
1211
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1212
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
1215
+
add_original jar original2;
1217
+
(* Add a new delta cookie *)
1219
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"new" ~value:"new_val"
1220
+
~secure:false ~http_only:false ?expires:None ?same_site:None ?max_age:None
1221
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1222
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
1225
+
add_cookie jar new_cookie;
1227
+
(* Delta should only contain the new cookie *)
1228
+
let delta = Cookeio.delta jar in
1229
+
Alcotest.(check int) "delta has 1 cookie" 1 (List.length delta);
1230
+
let delta_cookie = List.hd delta in
1231
+
Alcotest.(check string) "delta cookie name" "new" (Cookeio.name delta_cookie)
1233
+
let test_removal_cookie_format () =
1234
+
Eio_mock.Backend.run @@ fun () ->
1235
+
let clock = Eio_mock.Clock.make () in
1236
+
Eio_mock.Clock.set_time clock 1000.0;
1238
+
let jar = create () in
1240
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"value"
1241
+
~secure:true ~http_only:true ?expires:None ~same_site:`Strict
1243
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1244
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
1247
+
add_original jar cookie;
1249
+
(* Remove the cookie *)
1250
+
Cookeio.remove jar ~clock cookie;
1252
+
(* Get the removal cookie *)
1253
+
let delta = Cookeio.delta jar in
1254
+
let removal = List.hd delta in
1256
+
(* Check all properties *)
1257
+
Alcotest.(check string)
1258
+
"removal cookie has empty value" "" (Cookeio.value removal);
1259
+
Alcotest.(check (option int))
1260
+
"removal cookie Max-Age is 0" (Some 0)
1261
+
(Option.bind (Cookeio.max_age removal) Ptime.Span.to_int_s);
1263
+
(* Check expires is in the past *)
1264
+
let now = Ptime.of_float_s 1000.0 |> Option.get in
1265
+
match Cookeio.expires removal with
1267
+
Alcotest.(check bool)
1268
+
"expires is in the past" true
1269
+
(Ptime.compare exp now < 0)
1270
+
| None -> Alcotest.fail "removal cookie should have expires"
Eio_main.run @@ fun env ->
···
test_cookie_matching env);
468
-
[ test_case "Empty jar operations" `Quick (fun () -> test_empty_jar env) ]
1293
+
test_case "Empty jar operations" `Quick (fun () -> test_empty_jar env);
test_case "Cookie expiry with mock clock" `Quick
···
test_parse_set_cookie_with_expires;
test_case "SameSite=None validation" `Quick
test_samesite_none_validation;
1308
+
( "domain_normalization",
1310
+
test_case "Domain normalization" `Quick test_domain_normalization;
1311
+
test_case "Domain matching with normalized domains" `Quick
1312
+
test_domain_matching;
1314
+
( "max_age_tracking",
1316
+
test_case "Max-Age stored separately from Expires" `Quick
1317
+
test_max_age_stored_separately;
1318
+
test_case "Negative Max-Age becomes 0" `Quick
1319
+
test_max_age_negative_becomes_zero;
1320
+
test_case "make_set_cookie_header includes Max-Age" `Quick
1321
+
test_make_set_cookie_header_includes_max_age;
1322
+
test_case "Max-Age round-trip parsing" `Quick test_max_age_round_trip;
1324
+
( "delta_tracking",
1326
+
test_case "add_original doesn't affect delta" `Quick
1327
+
test_add_original_not_in_delta;
1328
+
test_case "add_cookie appears in delta" `Quick
1329
+
test_add_cookie_appears_in_delta;
1330
+
test_case "remove original creates removal cookie" `Quick
1331
+
test_remove_original_creates_removal_cookie;
1332
+
test_case "remove delta cookie just removes it" `Quick
1333
+
test_remove_delta_cookie_removes_it;
1334
+
test_case "get_cookies combines original and delta" `Quick
1335
+
test_get_cookies_combines_original_and_delta;
1336
+
test_case "get_cookies delta takes precedence" `Quick
1337
+
test_get_cookies_delta_takes_precedence;
1338
+
test_case "get_cookies excludes removal cookies" `Quick
1339
+
test_get_cookies_excludes_removal_cookies;
1340
+
test_case "delta returns only changed cookies" `Quick
1341
+
test_delta_returns_only_changed_cookies;
1342
+
test_case "removal cookie format" `Quick test_removal_cookie_format;
1344
+
( "http_date_parsing",
1346
+
test_case "HTTP date FMT1 (RFC 1123)" `Quick test_http_date_fmt1;
1347
+
test_case "HTTP date FMT2 (RFC 850)" `Quick test_http_date_fmt2;
1348
+
test_case "HTTP date FMT3 (asctime)" `Quick test_http_date_fmt3;
1349
+
test_case "HTTP date FMT4 (variant)" `Quick test_http_date_fmt4;
1350
+
test_case "Abbreviated year 69-99 becomes 1900+" `Quick
1351
+
test_abbreviated_year_69_to_99;
1352
+
test_case "Abbreviated year 0-68 becomes 2000+" `Quick
1353
+
test_abbreviated_year_0_to_68;
1354
+
test_case "RFC 3339 backward compatibility" `Quick
1355
+
test_rfc3339_still_works;
1356
+
test_case "Invalid date format logs warning" `Quick
1357
+
test_invalid_date_format_logs_warning;
1358
+
test_case "Case-insensitive month parsing" `Quick
1359
+
test_case_insensitive_month_parsing;
1360
+
test_case "Case-insensitive GMT parsing" `Quick
1361
+
test_case_insensitive_gmt_parsing;