OCaml HTTP cookie handling library with support for Eio-based storage jars

fmt

+389 -193
+16 -12
lib/core/cookeio.ml
··· 226 226 | "domain" -> attrs.domain <- Some (normalize_domain attr_value) 227 227 | "path" -> attrs.path <- Some attr_value 228 228 | "expires" -> ( 229 - (* Special case: Expires=0 means session cookie *) 230 - if attr_value = "0" then attrs.expires <- Some `Session 229 + if 230 + (* Special case: Expires=0 means session cookie *) 231 + attr_value = "0" 232 + then attrs.expires <- Some `Session 231 233 else 232 234 match Ptime.of_rfc3339 attr_value with 233 235 | Ok (time, _, _) -> attrs.expires <- Some (`DateTime time) ··· 248 250 (* Store the max-age as a Ptime.Span *) 249 251 attrs.max_age <- Some (Ptime.Span.of_int_s seconds); 250 252 (* Also compute and store expires as DateTime *) 251 - let expires = Ptime.add_span current_time (Ptime.Span.of_int_s seconds) in 253 + let expires = 254 + Ptime.add_span current_time (Ptime.Span.of_int_s seconds) 255 + in 252 256 (match expires with 253 257 | Some time -> attrs.expires <- Some (`DateTime time) 254 258 | None -> ()); ··· 288 292 if attrs.partitioned && not attrs.secure then ( 289 293 Log.warn (fun m -> 290 294 m 291 - "Cookie has Partitioned attribute but Secure flag is not set; \ 292 - this violates CHIPS requirements"); 295 + "Cookie has Partitioned attribute but Secure flag is not set; this \ 296 + violates CHIPS requirements"); 293 297 false) 294 298 else true 295 299 in ··· 303 307 let path = Option.value attrs.path ~default:request_path in 304 308 make ~domain ~path ~name ~value ~secure:attrs.secure 305 309 ~http_only:attrs.http_only ?expires:attrs.expires ?max_age:attrs.max_age 306 - ?same_site:attrs.same_site ~partitioned:attrs.partitioned 307 - ~creation_time:now ~last_access:now () 310 + ?same_site:attrs.same_site ~partitioned:attrs.partitioned ~creation_time:now 311 + ~last_access:now () 308 312 309 313 (** {1 Pretty Printing} *) 310 314 311 315 let pp ppf cookie = 312 316 Format.fprintf ppf 313 317 "@[<hov 2>{ name=%S;@ value=%S;@ domain=%S;@ path=%S;@ secure=%b;@ \ 314 - http_only=%b;@ partitioned=%b;@ expires=%a;@ max_age=%a;@ same_site=%a }@]" 318 + http_only=%b;@ partitioned=%b;@ expires=%a;@ max_age=%a;@ same_site=%a \ 319 + }@]" 315 320 (name cookie) (value cookie) (domain cookie) (path cookie) (secure cookie) 316 321 (http_only cookie) (partitioned cookie) 317 322 (Format.pp_print_option Expiration.pp) ··· 392 397 Error (Printf.sprintf "Cookie missing '=' separator: %s" name_value) 393 398 | Some eq_pos -> 394 399 let cookie_name = String.sub name_value 0 eq_pos |> String.trim in 395 - if String.length cookie_name = 0 then 396 - Error "Cookie has empty name" 400 + if String.length cookie_name = 0 then Error "Cookie has empty name" 397 401 else 398 402 let cookie_value = 399 403 String.sub name_value (eq_pos + 1) ··· 404 408 (* Create cookie with defaults from Cookie header context *) 405 409 let cookie = 406 410 make ~domain ~path ~name:cookie_name ~value:cookie_value 407 - ~secure:false ~http_only:false ~partitioned:false ~creation_time:current_time 408 - ~last_access:current_time () 411 + ~secure:false ~http_only:false ~partitioned:false 412 + ~creation_time:current_time ~last_access:current_time () 409 413 in 410 414 Ok cookie) 411 415 parts
+6 -5
lib/core/cookeio.mli
··· 37 37 protection 38 38 - [`Lax]: Cookie sent for same-site requests and top-level navigation 39 39 (default for modern browsers) 40 - - [`None]: Cookie sent for all cross-site requests (requires [secure] flag) *) 40 + - [`None]: Cookie sent for all cross-site requests (requires [secure] 41 + flag) *) 41 42 42 43 val equal : t -> t -> bool 43 44 (** Equality function for same-site values *) ··· 179 180 - [Partitioned] requires the [Secure] flag to be set 180 181 181 182 Example: 182 - [of_set_cookie_header ~now:(fun () -> Ptime_clock.now ()) ~domain:"example.com" ~path:"/" "session=abc123; 183 - Secure; HttpOnly"] *) 183 + [of_set_cookie_header ~now:(fun () -> Ptime_clock.now ()) 184 + ~domain:"example.com" ~path:"/" "session=abc123; Secure; HttpOnly"] *) 184 185 185 186 val of_cookie_header : 186 187 now:(unit -> Ptime.t) -> ··· 204 205 values and excess whitespace are ignored. 205 206 206 207 Example: 207 - [of_cookie_header ~now:(fun () -> Ptime_clock.now ()) ~domain:"example.com" ~path:"/" 208 - "session=abc; theme=dark"] *) 208 + [of_cookie_header ~now:(fun () -> Ptime_clock.now ()) ~domain:"example.com" 209 + ~path:"/" "session=abc; theme=dark"] *) 209 210 210 211 val make_cookie_header : t list -> string 211 212 (** Create cookie header value from cookies.
+23 -44
lib/jar/cookeio_jar.ml
··· 44 44 let is_expired cookie clock = 45 45 match Cookeio.expires cookie with 46 46 | None -> false (* No expiration *) 47 - | Some `Session -> false (* Session cookie - not expired until browser closes *) 47 + | Some `Session -> 48 + false (* Session cookie - not expired until browser closes *) 48 49 | Some (`DateTime exp_time) -> 49 50 let now = 50 51 Ptime.of_float_s (Eio.Time.now clock) ··· 70 71 71 72 let add_cookie jar cookie = 72 73 Log.debug (fun m -> 73 - m "Adding cookie to delta: %s=%s for domain %s" 74 - (Cookeio.name cookie) 75 - (Cookeio.value cookie) 76 - (Cookeio.domain cookie)); 74 + m "Adding cookie to delta: %s=%s for domain %s" (Cookeio.name cookie) 75 + (Cookeio.value cookie) (Cookeio.domain cookie)); 77 76 78 77 Eio.Mutex.lock jar.mutex; 79 78 (* Remove existing cookie with same identity from delta *) ··· 86 85 87 86 let add_original jar cookie = 88 87 Log.debug (fun m -> 89 - m "Adding original cookie: %s=%s for domain %s" 90 - (Cookeio.name cookie) 91 - (Cookeio.value cookie) 92 - (Cookeio.domain cookie)); 88 + m "Adding original cookie: %s=%s for domain %s" (Cookeio.name cookie) 89 + (Cookeio.value cookie) (Cookeio.domain cookie)); 93 90 94 91 Eio.Mutex.lock jar.mutex; 95 92 (* Remove existing cookie with same identity from original *) ··· 116 113 Ptime.sub_span now (Ptime.Span.of_int_s (365 * 24 * 60 * 60)) 117 114 |> Option.value ~default:Ptime.epoch 118 115 in 119 - Cookeio.make 120 - ~domain:(Cookeio.domain cookie) 121 - ~path:(Cookeio.path cookie) 122 - ~name:(Cookeio.name cookie) 123 - ~value:"" 124 - ~secure:(Cookeio.secure cookie) 125 - ~http_only:(Cookeio.http_only cookie) 126 - ~expires:(`DateTime past_expiry) 127 - ~max_age:(Ptime.Span.of_int_s 0) 128 - ?same_site:(Cookeio.same_site cookie) 116 + Cookeio.make ~domain:(Cookeio.domain cookie) ~path:(Cookeio.path cookie) 117 + ~name:(Cookeio.name cookie) ~value:"" ~secure:(Cookeio.secure cookie) 118 + ~http_only:(Cookeio.http_only cookie) ~expires:(`DateTime past_expiry) 119 + ~max_age:(Ptime.Span.of_int_s 0) ?same_site:(Cookeio.same_site cookie) 129 120 ~partitioned:(Cookeio.partitioned cookie) 130 121 ~creation_time:now ~last_access:now () 131 122 132 123 let remove jar ~clock cookie = 133 124 Log.debug (fun m -> 134 - m "Removing cookie: %s=%s for domain %s" 135 - (Cookeio.name cookie) 136 - (Cookeio.value cookie) 137 - (Cookeio.domain cookie)); 125 + m "Removing cookie: %s=%s for domain %s" (Cookeio.name cookie) 126 + (Cookeio.value cookie) (Cookeio.domain cookie)); 138 127 139 128 Eio.Mutex.lock jar.mutex; 140 129 (* Check if this cookie exists in original_cookies *) ··· 204 193 List.map 205 194 (fun c -> 206 195 if List.exists (fun a -> cookie_identity_matches a c) applicable then 207 - Cookeio.make 208 - ~domain:(Cookeio.domain c) 209 - ~path:(Cookeio.path c) 210 - ~name:(Cookeio.name c) 211 - ~value:(Cookeio.value c) 212 - ~secure:(Cookeio.secure c) 213 - ~http_only:(Cookeio.http_only c) 214 - ?expires:(Cookeio.expires c) 215 - ?max_age:(Cookeio.max_age c) 196 + Cookeio.make ~domain:(Cookeio.domain c) ~path:(Cookeio.path c) 197 + ~name:(Cookeio.name c) ~value:(Cookeio.value c) 198 + ~secure:(Cookeio.secure c) ~http_only:(Cookeio.http_only c) 199 + ?expires:(Cookeio.expires c) ?max_age:(Cookeio.max_age c) 216 200 ?same_site:(Cookeio.same_site c) 217 201 ~partitioned:(Cookeio.partitioned c) 218 - ~creation_time:(Cookeio.creation_time c) 219 - ~last_access:now () 202 + ~creation_time:(Cookeio.creation_time c) ~last_access:now () 220 203 else c) 221 204 cookies 222 205 in ··· 346 329 in 347 330 348 331 Buffer.add_string buffer 349 - (Printf.sprintf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" 350 - (Cookeio.domain cookie) 351 - include_subdomains 352 - (Cookeio.path cookie) 353 - secure_flag expires_str 354 - (Cookeio.name cookie) 355 - (Cookeio.value cookie))) 332 + (Printf.sprintf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" (Cookeio.domain cookie) 333 + include_subdomains (Cookeio.path cookie) secure_flag expires_str 334 + (Cookeio.name cookie) (Cookeio.value cookie))) 356 335 unique; 357 336 358 337 Buffer.contents buffer ··· 389 368 390 369 let cookie = 391 370 Cookeio.make ~domain:(normalize_domain domain) ~path ~name ~value 392 - ~secure:(secure = "TRUE") ~http_only:false ?expires ?max_age:None 393 - ?same_site:None ~partitioned:false ~creation_time:now 394 - ~last_access:now () 371 + ~secure:(secure = "TRUE") ~http_only:false ?expires 372 + ?max_age:None ?same_site:None ~partitioned:false 373 + ~creation_time:now ~last_access:now () 395 374 in 396 375 add_original jar cookie; 397 376 Log.debug (fun m -> m "Loaded cookie: %s=%s" name value)
+10 -1
test/dune
··· 1 1 (test 2 2 (name test_cookeio) 3 - (libraries cookeio cookeio_jar alcotest eio eio.unix eio_main eio.mock ptime str) 3 + (libraries 4 + cookeio 5 + cookeio_jar 6 + alcotest 7 + eio 8 + eio.unix 9 + eio_main 10 + eio.mock 11 + ptime 12 + str) 4 13 (deps cookies.txt))
+334 -131
test/test_cookeio.ml
··· 19 19 partitioned=%b; expires=%a; max_age=%a; same_site=%a }" 20 20 (Cookeio.name c) (Cookeio.value c) (Cookeio.domain c) (Cookeio.path c) 21 21 (Cookeio.secure c) (Cookeio.http_only c) (Cookeio.partitioned c) 22 - (Format.pp_print_option 23 - (fun ppf e -> 22 + (Format.pp_print_option (fun ppf e -> 24 23 match e with 25 24 | `Session -> Format.pp_print_string ppf "Session" 26 25 | `DateTime t -> Format.fprintf ppf "DateTime(%a)" Ptime.pp t)) 27 26 (Cookeio.expires c) 28 27 (Format.pp_print_option Ptime.Span.pp) 29 28 (Cookeio.max_age c) 30 - (Format.pp_print_option 31 - (fun ppf -> function 32 - | `Strict -> Format.pp_print_string ppf "Strict" 33 - | `Lax -> Format.pp_print_string ppf "Lax" 34 - | `None -> Format.pp_print_string ppf "None")) 29 + (Format.pp_print_option (fun ppf -> function 30 + | `Strict -> Format.pp_print_string ppf "Strict" 31 + | `Lax -> Format.pp_print_string ppf "Lax" 32 + | `None -> Format.pp_print_string ppf "None")) 35 33 (Cookeio.same_site c)) 36 34 (fun c1 c2 -> 37 35 let expires_equal e1 e2 = ··· 126 124 begin match expected_expiry with 127 125 | Some t -> 128 126 Alcotest.(check (option expiration_testable)) 129 - "cookie-3 expires" (Some (`DateTime t)) (Cookeio.expires cookie3) 127 + "cookie-3 expires" 128 + (Some (`DateTime t)) 129 + (Cookeio.expires cookie3) 130 130 | None -> Alcotest.fail "Expected expiry time for cookie-3" 131 131 end; 132 132 ··· 142 142 begin match expected_expiry with 143 143 | Some t -> 144 144 Alcotest.(check (option expiration_testable)) 145 - "cookie-4 expires" (Some (`DateTime t)) (Cookeio.expires cookie4) 145 + "cookie-4 expires" 146 + (Some (`DateTime t)) 147 + (Cookeio.expires cookie4) 146 148 | None -> Alcotest.fail "Expected expiry time for cookie-4" 147 149 end; 148 150 ··· 158 160 begin match expected_expiry with 159 161 | Some t -> 160 162 Alcotest.(check (option expiration_testable)) 161 - "cookie-5 expires" (Some (`DateTime t)) (Cookeio.expires cookie5) 163 + "cookie-5 expires" 164 + (Some (`DateTime t)) 165 + (Cookeio.expires cookie5) 162 166 | None -> Alcotest.fail "Expected expiry time for cookie-5" 163 167 end 164 168 ··· 191 195 begin match expected_expiry with 192 196 | Some t -> 193 197 Alcotest.(check (option expiration_testable)) 194 - "file cookie-5 expires" (Some (`DateTime t)) (Cookeio.expires cookie5) 198 + "file cookie-5 expires" 199 + (Some (`DateTime t)) 200 + (Cookeio.expires cookie5) 195 201 | None -> Alcotest.fail "Expected expiry time for cookie-5" 196 202 end; 197 203 ··· 292 298 begin match Ptime.of_float_s 1257894000.0 with 293 299 | Some t -> 294 300 Alcotest.(check (option expiration_testable)) 295 - "round trip expires" (Some (`DateTime t)) (Cookeio.expires cookie2) 301 + "round trip expires" 302 + (Some (`DateTime t)) 303 + (Cookeio.expires cookie2) 296 304 | None -> Alcotest.fail "Expected expiry time" 297 305 end 298 306 ··· 376 384 (* Parse a Set-Cookie header with Max-Age *) 377 385 let header = "session=abc123; Max-Age=3600; Secure; HttpOnly" in 378 386 let cookie_opt = 379 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 387 + of_set_cookie_header 388 + ~now:(fun () -> 389 + Ptime.of_float_s (Eio.Time.now clock) 390 + |> Option.value ~default:Ptime.epoch) 391 + ~domain:"example.com" ~path:"/" header 380 392 in 381 393 382 394 Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt); ··· 392 404 begin match expected_expiry with 393 405 | Some t -> 394 406 Alcotest.(check (option expiration_testable)) 395 - "expires set from max-age" (Some (`DateTime t)) (Cookeio.expires cookie) 407 + "expires set from max-age" 408 + (Some (`DateTime t)) 409 + (Cookeio.expires cookie) 396 410 | None -> Alcotest.fail "Expected expiry time" 397 411 end; 398 412 ··· 455 469 "id=xyz789; Expires=2025-10-21T07:28:00Z; Path=/; Domain=.example.com" 456 470 in 457 471 let cookie_opt = 458 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 472 + of_set_cookie_header 473 + ~now:(fun () -> 474 + Ptime.of_float_s (Eio.Time.now clock) 475 + |> Option.value ~default:Ptime.epoch) 476 + ~domain:"example.com" ~path:"/" header 459 477 in 460 478 461 479 Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt); ··· 476 494 match expected_expiry with 477 495 | Ok (time, _, _) -> 478 496 Alcotest.(check (option expiration_testable)) 479 - "expires matches parsed value" (Some (`DateTime time)) 497 + "expires matches parsed value" 498 + (Some (`DateTime time)) 480 499 (Cookeio.expires cookie) 481 500 | Error _ -> Alcotest.fail "Failed to parse expected expiry time" 482 501 ··· 490 509 (* This should be rejected: SameSite=None without Secure *) 491 510 let invalid_header = "token=abc; SameSite=None" in 492 511 let cookie_opt = 493 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" invalid_header 512 + of_set_cookie_header 513 + ~now:(fun () -> 514 + Ptime.of_float_s (Eio.Time.now clock) 515 + |> Option.value ~default:Ptime.epoch) 516 + ~domain:"example.com" ~path:"/" invalid_header 494 517 in 495 518 496 519 Alcotest.(check bool) ··· 500 523 (* This should be accepted: SameSite=None with Secure *) 501 524 let valid_header = "token=abc; SameSite=None; Secure" in 502 525 let cookie_opt2 = 503 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" valid_header 526 + of_set_cookie_header 527 + ~now:(fun () -> 528 + Ptime.of_float_s (Eio.Time.now clock) 529 + |> Option.value ~default:Ptime.epoch) 530 + ~domain:"example.com" ~path:"/" valid_header 504 531 in 505 532 506 533 Alcotest.(check bool) ··· 528 555 (* Test parsing ".example.com" stores as "example.com" *) 529 556 let header = "test=value; Domain=.example.com" in 530 557 let cookie_opt = 531 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 558 + of_set_cookie_header 559 + ~now:(fun () -> 560 + Ptime.of_float_s (Eio.Time.now clock) 561 + |> Option.value ~default:Ptime.epoch) 562 + ~domain:"example.com" ~path:"/" header 532 563 in 533 564 Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt); 534 565 let cookie = Option.get cookie_opt in ··· 562 593 (* Parse a Set-Cookie header with Max-Age *) 563 594 let header = "session=abc123; Max-Age=3600" in 564 595 let cookie_opt = 565 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 596 + of_set_cookie_header 597 + ~now:(fun () -> 598 + Ptime.of_float_s (Eio.Time.now clock) 599 + |> Option.value ~default:Ptime.epoch) 600 + ~domain:"example.com" ~path:"/" header 566 601 in 567 602 Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt); 568 603 ··· 582 617 begin match expected_expiry with 583 618 | Some t -> 584 619 Alcotest.(check (option expiration_testable)) 585 - "expires computed from max-age" (Some (`DateTime t)) 620 + "expires computed from max-age" 621 + (Some (`DateTime t)) 586 622 (Cookeio.expires cookie) 587 623 | None -> Alcotest.fail "Expected expiry time" 588 624 end ··· 595 631 (* Parse a Set-Cookie header with negative Max-Age *) 596 632 let header = "session=abc123; Max-Age=-100" in 597 633 let cookie_opt = 598 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 634 + of_set_cookie_header 635 + ~now:(fun () -> 636 + Ptime.of_float_s (Eio.Time.now clock) 637 + |> Option.value ~default:Ptime.epoch) 638 + ~domain:"example.com" ~path:"/" header 599 639 in 600 640 Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt); 601 641 ··· 615 655 begin match expected_expiry with 616 656 | Some t -> 617 657 Alcotest.(check (option expiration_testable)) 618 - "expires computed with 0 seconds" (Some (`DateTime t)) 658 + "expires computed with 0 seconds" 659 + (Some (`DateTime t)) 619 660 (Cookeio.expires cookie) 620 661 | None -> Alcotest.fail "Expected expiry time" 621 662 end ··· 641 682 let expires_time = Ptime.of_float_s 8600.0 |> Option.get in 642 683 let cookie = 643 684 Cookeio.make ~domain:"example.com" ~path:"/" ~name:"session" ~value:"abc123" 644 - ~secure:true ~http_only:true ?expires:(Some (`DateTime expires_time)) 685 + ~secure:true ~http_only:true 686 + ?expires:(Some (`DateTime expires_time)) 645 687 ?max_age:(Some max_age_span) ?same_site:(Some `Strict) 646 688 ~creation_time:(Ptime.of_float_s 5000.0 |> Option.get) 647 689 ~last_access:(Ptime.of_float_s 5000.0 |> Option.get) ··· 679 721 (* Parse a cookie with Max-Age *) 680 722 let header = "session=xyz; Max-Age=7200; Secure; HttpOnly" in 681 723 let cookie_opt = 682 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 724 + of_set_cookie_header 725 + ~now:(fun () -> 726 + Ptime.of_float_s (Eio.Time.now clock) 727 + |> Option.value ~default:Ptime.epoch) 728 + ~domain:"example.com" ~path:"/" header 683 729 in 684 730 Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt); 685 731 let cookie = Option.get cookie_opt in ··· 691 737 Eio_mock.Clock.set_time clock 5000.0; 692 738 (* Reset clock to same time *) 693 739 let cookie2_opt = 694 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" set_cookie_header 740 + of_set_cookie_header 741 + ~now:(fun () -> 742 + Ptime.of_float_s (Eio.Time.now clock) 743 + |> Option.value ~default:Ptime.epoch) 744 + ~domain:"example.com" ~path:"/" set_cookie_header 695 745 in 696 746 Alcotest.(check bool) "cookie re-parsed" true (Option.is_some cookie2_opt); 697 747 let cookie2 = Option.get cookie2_opt in ··· 760 810 (* Test FMT1: "Wed, 21 Oct 2015 07:28:00 GMT" (RFC 1123) *) 761 811 let header = "session=abc; Expires=Wed, 21 Oct 2015 07:28:00 GMT" in 762 812 let cookie_opt = 763 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 813 + of_set_cookie_header 814 + ~now:(fun () -> 815 + Ptime.of_float_s (Eio.Time.now clock) 816 + |> Option.value ~default:Ptime.epoch) 817 + ~domain:"example.com" ~path:"/" header 764 818 in 765 819 Alcotest.(check bool) "FMT1 cookie parsed" true (Option.is_some cookie_opt); 766 820 ··· 774 828 begin match expected with 775 829 | Some t -> 776 830 Alcotest.(check (option expiration_testable)) 777 - "FMT1 expiry correct" (Some (`DateTime t)) (Cookeio.expires cookie) 831 + "FMT1 expiry correct" 832 + (Some (`DateTime t)) 833 + (Cookeio.expires cookie) 778 834 | None -> Alcotest.fail "Expected expiry time for FMT1" 779 835 end 780 836 ··· 786 842 (* Test FMT2: "Wednesday, 21-Oct-15 07:28:00 GMT" (RFC 850 with abbreviated year) *) 787 843 let header = "session=abc; Expires=Wednesday, 21-Oct-15 07:28:00 GMT" in 788 844 let cookie_opt = 789 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 845 + of_set_cookie_header 846 + ~now:(fun () -> 847 + Ptime.of_float_s (Eio.Time.now clock) 848 + |> Option.value ~default:Ptime.epoch) 849 + ~domain:"example.com" ~path:"/" header 790 850 in 791 851 Alcotest.(check bool) "FMT2 cookie parsed" true (Option.is_some cookie_opt); 792 852 ··· 800 860 begin match expected with 801 861 | Some t -> 802 862 Alcotest.(check (option expiration_testable)) 803 - "FMT2 expiry correct with year normalization" (Some (`DateTime t)) 863 + "FMT2 expiry correct with year normalization" 864 + (Some (`DateTime t)) 804 865 (Cookeio.expires cookie) 805 866 | None -> Alcotest.fail "Expected expiry time for FMT2" 806 867 end ··· 813 874 (* Test FMT3: "Wed Oct 21 07:28:00 2015" (asctime) *) 814 875 let header = "session=abc; Expires=Wed Oct 21 07:28:00 2015" in 815 876 let cookie_opt = 816 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 877 + of_set_cookie_header 878 + ~now:(fun () -> 879 + Ptime.of_float_s (Eio.Time.now clock) 880 + |> Option.value ~default:Ptime.epoch) 881 + ~domain:"example.com" ~path:"/" header 817 882 in 818 883 Alcotest.(check bool) "FMT3 cookie parsed" true (Option.is_some cookie_opt); 819 884 ··· 826 891 begin match expected with 827 892 | Some t -> 828 893 Alcotest.(check (option expiration_testable)) 829 - "FMT3 expiry correct" (Some (`DateTime t)) (Cookeio.expires cookie) 894 + "FMT3 expiry correct" 895 + (Some (`DateTime t)) 896 + (Cookeio.expires cookie) 830 897 | None -> Alcotest.fail "Expected expiry time for FMT3" 831 898 end 832 899 ··· 838 905 (* Test FMT4: "Wed, 21-Oct-2015 07:28:00 GMT" (variant) *) 839 906 let header = "session=abc; Expires=Wed, 21-Oct-2015 07:28:00 GMT" in 840 907 let cookie_opt = 841 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 908 + of_set_cookie_header 909 + ~now:(fun () -> 910 + Ptime.of_float_s (Eio.Time.now clock) 911 + |> Option.value ~default:Ptime.epoch) 912 + ~domain:"example.com" ~path:"/" header 842 913 in 843 914 Alcotest.(check bool) "FMT4 cookie parsed" true (Option.is_some cookie_opt); 844 915 ··· 851 922 begin match expected with 852 923 | Some t -> 853 924 Alcotest.(check (option expiration_testable)) 854 - "FMT4 expiry correct" (Some (`DateTime t)) (Cookeio.expires cookie) 925 + "FMT4 expiry correct" 926 + (Some (`DateTime t)) 927 + (Cookeio.expires cookie) 855 928 | None -> Alcotest.fail "Expected expiry time for FMT4" 856 929 end 857 930 ··· 863 936 (* Year 95 should become 1995 *) 864 937 let header = "session=abc; Expires=Wed, 21-Oct-95 07:28:00 GMT" in 865 938 let cookie_opt = 866 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 939 + of_set_cookie_header 940 + ~now:(fun () -> 941 + Ptime.of_float_s (Eio.Time.now clock) 942 + |> Option.value ~default:Ptime.epoch) 943 + ~domain:"example.com" ~path:"/" header 867 944 in 868 945 let cookie = Option.get cookie_opt in 869 946 let expected = Ptime.of_date_time ((1995, 10, 21), ((07, 28, 00), 0)) in 870 947 begin match expected with 871 948 | Some t -> 872 949 Alcotest.(check (option expiration_testable)) 873 - "year 95 becomes 1995" (Some (`DateTime t)) (Cookeio.expires cookie) 950 + "year 95 becomes 1995" 951 + (Some (`DateTime t)) 952 + (Cookeio.expires cookie) 874 953 | None -> Alcotest.fail "Expected expiry time for year 95" 875 954 end; 876 955 877 956 (* Year 69 should become 1969 *) 878 957 let header2 = "session=abc; Expires=Wed, 10-Sep-69 20:00:00 GMT" in 879 958 let cookie_opt2 = 880 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header2 959 + of_set_cookie_header 960 + ~now:(fun () -> 961 + Ptime.of_float_s (Eio.Time.now clock) 962 + |> Option.value ~default:Ptime.epoch) 963 + ~domain:"example.com" ~path:"/" header2 881 964 in 882 965 let cookie2 = Option.get cookie_opt2 in 883 966 let expected2 = Ptime.of_date_time ((1969, 9, 10), ((20, 0, 0), 0)) in 884 967 begin match expected2 with 885 968 | Some t -> 886 969 Alcotest.(check (option expiration_testable)) 887 - "year 69 becomes 1969" (Some (`DateTime t)) (Cookeio.expires cookie2) 970 + "year 69 becomes 1969" 971 + (Some (`DateTime t)) 972 + (Cookeio.expires cookie2) 888 973 | None -> Alcotest.fail "Expected expiry time for year 69" 889 974 end; 890 975 891 976 (* Year 99 should become 1999 *) 892 977 let header3 = "session=abc; Expires=Thu, 10-Sep-99 20:00:00 GMT" in 893 978 let cookie_opt3 = 894 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header3 979 + of_set_cookie_header 980 + ~now:(fun () -> 981 + Ptime.of_float_s (Eio.Time.now clock) 982 + |> Option.value ~default:Ptime.epoch) 983 + ~domain:"example.com" ~path:"/" header3 895 984 in 896 985 let cookie3 = Option.get cookie_opt3 in 897 986 let expected3 = Ptime.of_date_time ((1999, 9, 10), ((20, 0, 0), 0)) in 898 987 begin match expected3 with 899 988 | Some t -> 900 989 Alcotest.(check (option expiration_testable)) 901 - "year 99 becomes 1999" (Some (`DateTime t)) (Cookeio.expires cookie3) 990 + "year 99 becomes 1999" 991 + (Some (`DateTime t)) 992 + (Cookeio.expires cookie3) 902 993 | None -> Alcotest.fail "Expected expiry time for year 99" 903 994 end 904 995 ··· 910 1001 (* Year 25 should become 2025 *) 911 1002 let header = "session=abc; Expires=Wed, 21-Oct-25 07:28:00 GMT" in 912 1003 let cookie_opt = 913 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 1004 + of_set_cookie_header 1005 + ~now:(fun () -> 1006 + Ptime.of_float_s (Eio.Time.now clock) 1007 + |> Option.value ~default:Ptime.epoch) 1008 + ~domain:"example.com" ~path:"/" header 914 1009 in 915 1010 let cookie = Option.get cookie_opt in 916 1011 let expected = Ptime.of_date_time ((2025, 10, 21), ((07, 28, 00), 0)) in 917 1012 begin match expected with 918 1013 | Some t -> 919 1014 Alcotest.(check (option expiration_testable)) 920 - "year 25 becomes 2025" (Some (`DateTime t)) (Cookeio.expires cookie) 1015 + "year 25 becomes 2025" 1016 + (Some (`DateTime t)) 1017 + (Cookeio.expires cookie) 921 1018 | None -> Alcotest.fail "Expected expiry time for year 25" 922 1019 end; 923 1020 924 1021 (* Year 0 should become 2000 *) 925 1022 let header2 = "session=abc; Expires=Fri, 01-Jan-00 00:00:00 GMT" in 926 1023 let cookie_opt2 = 927 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header2 1024 + of_set_cookie_header 1025 + ~now:(fun () -> 1026 + Ptime.of_float_s (Eio.Time.now clock) 1027 + |> Option.value ~default:Ptime.epoch) 1028 + ~domain:"example.com" ~path:"/" header2 928 1029 in 929 1030 let cookie2 = Option.get cookie_opt2 in 930 1031 let expected2 = Ptime.of_date_time ((2000, 1, 1), ((0, 0, 0), 0)) in 931 1032 begin match expected2 with 932 1033 | Some t -> 933 1034 Alcotest.(check (option expiration_testable)) 934 - "year 0 becomes 2000" (Some (`DateTime t)) (Cookeio.expires cookie2) 1035 + "year 0 becomes 2000" 1036 + (Some (`DateTime t)) 1037 + (Cookeio.expires cookie2) 935 1038 | None -> Alcotest.fail "Expected expiry time for year 0" 936 1039 end; 937 1040 938 1041 (* Year 68 should become 2068 *) 939 1042 let header3 = "session=abc; Expires=Thu, 10-Sep-68 20:00:00 GMT" in 940 1043 let cookie_opt3 = 941 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header3 1044 + of_set_cookie_header 1045 + ~now:(fun () -> 1046 + Ptime.of_float_s (Eio.Time.now clock) 1047 + |> Option.value ~default:Ptime.epoch) 1048 + ~domain:"example.com" ~path:"/" header3 942 1049 in 943 1050 let cookie3 = Option.get cookie_opt3 in 944 1051 let expected3 = Ptime.of_date_time ((2068, 9, 10), ((20, 0, 0), 0)) in 945 1052 begin match expected3 with 946 1053 | Some t -> 947 1054 Alcotest.(check (option expiration_testable)) 948 - "year 68 becomes 2068" (Some (`DateTime t)) (Cookeio.expires cookie3) 1055 + "year 68 becomes 2068" 1056 + (Some (`DateTime t)) 1057 + (Cookeio.expires cookie3) 949 1058 | None -> Alcotest.fail "Expected expiry time for year 68" 950 1059 end 951 1060 ··· 957 1066 (* Ensure RFC 3339 format still works for backward compatibility *) 958 1067 let header = "session=abc; Expires=2025-10-21T07:28:00Z" in 959 1068 let cookie_opt = 960 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 1069 + of_set_cookie_header 1070 + ~now:(fun () -> 1071 + Ptime.of_float_s (Eio.Time.now clock) 1072 + |> Option.value ~default:Ptime.epoch) 1073 + ~domain:"example.com" ~path:"/" header 961 1074 in 962 1075 Alcotest.(check bool) 963 1076 "RFC 3339 cookie parsed" true ··· 973 1086 match expected with 974 1087 | Ok (time, _, _) -> 975 1088 Alcotest.(check (option expiration_testable)) 976 - "RFC 3339 expiry correct" (Some (`DateTime time)) (Cookeio.expires cookie) 1089 + "RFC 3339 expiry correct" 1090 + (Some (`DateTime time)) 1091 + (Cookeio.expires cookie) 977 1092 | Error _ -> Alcotest.fail "Failed to parse expected RFC 3339 time" 978 1093 979 1094 let test_invalid_date_format_logs_warning () = ··· 984 1099 (* Invalid date format should log a warning but still parse the cookie *) 985 1100 let header = "session=abc; Expires=InvalidDate" in 986 1101 let cookie_opt = 987 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 1102 + of_set_cookie_header 1103 + ~now:(fun () -> 1104 + Ptime.of_float_s (Eio.Time.now clock) 1105 + |> Option.value ~default:Ptime.epoch) 1106 + ~domain:"example.com" ~path:"/" header 988 1107 in 989 1108 990 1109 (* Cookie should still be parsed, just without expires *) ··· 1016 1135 List.iter 1017 1136 (fun (header, description) -> 1018 1137 let cookie_opt = 1019 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 1138 + of_set_cookie_header 1139 + ~now:(fun () -> 1140 + Ptime.of_float_s (Eio.Time.now clock) 1141 + |> Option.value ~default:Ptime.epoch) 1142 + ~domain:"example.com" ~path:"/" header 1020 1143 in 1021 1144 Alcotest.(check bool) 1022 1145 (description ^ " parsed") true ··· 1058 1181 List.iter 1059 1182 (fun (header, description) -> 1060 1183 let cookie_opt = 1061 - of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header 1184 + of_set_cookie_header 1185 + ~now:(fun () -> 1186 + Ptime.of_float_s (Eio.Time.now clock) 1187 + |> Option.value ~default:Ptime.epoch) 1188 + ~domain:"example.com" ~path:"/" header 1062 1189 in 1063 1190 Alcotest.(check bool) 1064 1191 (description ^ " parsed") true ··· 1384 1511 let test_partitioned_parsing env = 1385 1512 let clock = Eio.Stdenv.clock env in 1386 1513 1387 - match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"widget.com" ~path:"/" 1388 - "id=123; Partitioned; Secure" with 1514 + match 1515 + of_set_cookie_header 1516 + ~now:(fun () -> 1517 + Ptime.of_float_s (Eio.Time.now clock) 1518 + |> Option.value ~default:Ptime.epoch) 1519 + ~domain:"widget.com" ~path:"/" "id=123; Partitioned; Secure" 1520 + with 1389 1521 | Some c -> 1390 1522 Alcotest.(check bool) "partitioned flag" true (partitioned c); 1391 1523 Alcotest.(check bool) "secure flag" true (secure c) ··· 1393 1525 1394 1526 let test_partitioned_serialization env = 1395 1527 let clock = Eio.Stdenv.clock env in 1396 - let now = Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch in 1528 + let now = 1529 + Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch 1530 + in 1397 1531 1398 - let cookie = make ~domain:"widget.com" ~path:"/" ~name:"id" ~value:"123" 1399 - ~secure:true ~partitioned:true 1400 - ~creation_time:now ~last_access:now () in 1532 + let cookie = 1533 + make ~domain:"widget.com" ~path:"/" ~name:"id" ~value:"123" ~secure:true 1534 + ~partitioned:true ~creation_time:now ~last_access:now () 1535 + in 1401 1536 1402 1537 let header = make_set_cookie_header cookie in 1403 1538 let contains_substring s sub = ··· 1415 1550 let clock = Eio.Stdenv.clock env in 1416 1551 1417 1552 (* Partitioned without Secure should be rejected *) 1418 - match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"widget.com" ~path:"/" 1419 - "id=123; Partitioned" with 1553 + match 1554 + of_set_cookie_header 1555 + ~now:(fun () -> 1556 + Ptime.of_float_s (Eio.Time.now clock) 1557 + |> Option.value ~default:Ptime.epoch) 1558 + ~domain:"widget.com" ~path:"/" "id=123; Partitioned" 1559 + with 1420 1560 | None -> () (* Expected *) 1421 1561 | Some _ -> Alcotest.fail "Should reject Partitioned without Secure" 1422 1562 ··· 1424 1564 1425 1565 let test_expiration_variants env = 1426 1566 let clock = Eio.Stdenv.clock env in 1427 - let now = Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch in 1567 + let now = 1568 + Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch 1569 + in 1428 1570 let make_base ~name ?expires () = 1429 - make ~domain:"ex.com" ~path:"/" ~name ~value:"v" 1430 - ?expires ~creation_time:now ~last_access:now () 1571 + make ~domain:"ex.com" ~path:"/" ~name ~value:"v" ?expires ~creation_time:now 1572 + ~last_access:now () 1431 1573 in 1432 1574 1433 1575 (* No expiration *) 1434 1576 let c1 = make_base ~name:"no_expiry" () in 1435 - Alcotest.(check (option expiration_testable)) "no expiration" 1436 - None (expires c1); 1577 + Alcotest.(check (option expiration_testable)) 1578 + "no expiration" None (expires c1); 1437 1579 1438 1580 (* Session cookie *) 1439 1581 let c2 = make_base ~name:"session" ~expires:`Session () in 1440 - Alcotest.(check (option expiration_testable)) "session cookie" 1441 - (Some `Session) (expires c2); 1582 + Alcotest.(check (option expiration_testable)) 1583 + "session cookie" (Some `Session) (expires c2); 1442 1584 1443 1585 (* Explicit expiration *) 1444 1586 let future = Ptime.add_span now (Ptime.Span.of_int_s 3600) |> Option.get in ··· 1451 1593 let clock = Eio.Stdenv.clock env in 1452 1594 1453 1595 (* Expires=0 should parse as Session *) 1454 - match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/" 1455 - "id=123; Expires=0" with 1596 + match 1597 + of_set_cookie_header 1598 + ~now:(fun () -> 1599 + Ptime.of_float_s (Eio.Time.now clock) 1600 + |> Option.value ~default:Ptime.epoch) 1601 + ~domain:"ex.com" ~path:"/" "id=123; Expires=0" 1602 + with 1456 1603 | Some c -> 1457 - Alcotest.(check (option expiration_testable)) "expires=0 is session" 1458 - (Some `Session) (expires c) 1604 + Alcotest.(check (option expiration_testable)) 1605 + "expires=0 is session" (Some `Session) (expires c) 1459 1606 | None -> Alcotest.fail "Should parse Expires=0" 1460 1607 1461 1608 let test_serialize_expiration_variants env = 1462 1609 let clock = Eio.Stdenv.clock env in 1463 - let now = Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch in 1610 + let now = 1611 + Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch 1612 + in 1464 1613 let contains_substring s sub = 1465 1614 try 1466 1615 let _ = Str.search_forward (Str.regexp_string sub) s 0 in ··· 1469 1618 in 1470 1619 1471 1620 (* Session cookie serialization *) 1472 - let c1 = make ~domain:"ex.com" ~path:"/" ~name:"s" ~value:"v" 1473 - ~expires:`Session ~creation_time:now ~last_access:now () in 1621 + let c1 = 1622 + make ~domain:"ex.com" ~path:"/" ~name:"s" ~value:"v" ~expires:`Session 1623 + ~creation_time:now ~last_access:now () 1624 + in 1474 1625 let h1 = make_set_cookie_header c1 in 1475 1626 let has_expires = contains_substring h1 "Expires=" in 1476 1627 Alcotest.(check bool) "session has Expires" true has_expires; 1477 1628 1478 1629 (* DateTime serialization *) 1479 1630 let future = Ptime.add_span now (Ptime.Span.of_int_s 3600) |> Option.get in 1480 - let c2 = make ~domain:"ex.com" ~path:"/" ~name:"p" ~value:"v" 1481 - ~expires:(`DateTime future) ~creation_time:now ~last_access:now () in 1631 + let c2 = 1632 + make ~domain:"ex.com" ~path:"/" ~name:"p" ~value:"v" 1633 + ~expires:(`DateTime future) ~creation_time:now ~last_access:now () 1634 + in 1482 1635 let h2 = make_set_cookie_header c2 in 1483 1636 let has_expires2 = contains_substring h2 "Expires=" in 1484 1637 Alcotest.(check bool) "datetime has Expires" true has_expires2 ··· 1487 1640 1488 1641 let test_quoted_cookie_values env = 1489 1642 let clock = Eio.Stdenv.clock env in 1490 - let test_cases = [ 1491 - ("name=value", "value", "value"); 1492 - ("name=\"value\"", "\"value\"", "value"); 1493 - ("name=\"partial", "\"partial", "\"partial"); 1494 - ("name=\"val\"\"", "\"val\"\"", "val\""); 1495 - ("name=val\"", "val\"", "val\""); 1496 - ("name=\"\"", "\"\"", ""); 1497 - ] in 1643 + let test_cases = 1644 + [ 1645 + ("name=value", "value", "value"); 1646 + ("name=\"value\"", "\"value\"", "value"); 1647 + ("name=\"partial", "\"partial", "\"partial"); 1648 + ("name=\"val\"\"", "\"val\"\"", "val\""); 1649 + ("name=val\"", "val\"", "val\""); 1650 + ("name=\"\"", "\"\"", ""); 1651 + ] 1652 + in 1498 1653 1499 - List.iter (fun (input, expected_raw, expected_trimmed) -> 1500 - match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/" input with 1501 - | Some c -> 1502 - Alcotest.(check string) 1503 - (Printf.sprintf "raw value for %s" input) expected_raw (value c); 1504 - Alcotest.(check string) 1505 - (Printf.sprintf "trimmed value for %s" input) expected_trimmed 1506 - (value_trimmed c) 1507 - | None -> Alcotest.fail ("Parse failed: " ^ input) 1508 - ) test_cases 1654 + List.iter 1655 + (fun (input, expected_raw, expected_trimmed) -> 1656 + match 1657 + of_set_cookie_header 1658 + ~now:(fun () -> 1659 + Ptime.of_float_s (Eio.Time.now clock) 1660 + |> Option.value ~default:Ptime.epoch) 1661 + ~domain:"ex.com" ~path:"/" input 1662 + with 1663 + | Some c -> 1664 + Alcotest.(check string) 1665 + (Printf.sprintf "raw value for %s" input) 1666 + expected_raw (value c); 1667 + Alcotest.(check string) 1668 + (Printf.sprintf "trimmed value for %s" input) 1669 + expected_trimmed (value_trimmed c) 1670 + | None -> Alcotest.fail ("Parse failed: " ^ input)) 1671 + test_cases 1509 1672 1510 1673 let test_trimmed_value_not_used_for_equality env = 1511 1674 let clock = Eio.Stdenv.clock env in 1512 1675 1513 - match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/" 1514 - "name=\"value\"" with 1515 - | Some c1 -> 1516 - begin match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/" 1517 - "name=value" with 1676 + match 1677 + of_set_cookie_header 1678 + ~now:(fun () -> 1679 + Ptime.of_float_s (Eio.Time.now clock) 1680 + |> Option.value ~default:Ptime.epoch) 1681 + ~domain:"ex.com" ~path:"/" "name=\"value\"" 1682 + with 1683 + | Some c1 -> begin 1684 + match 1685 + of_set_cookie_header 1686 + ~now:(fun () -> 1687 + Ptime.of_float_s (Eio.Time.now clock) 1688 + |> Option.value ~default:Ptime.epoch) 1689 + ~domain:"ex.com" ~path:"/" "name=value" 1690 + with 1518 1691 | Some c2 -> 1519 1692 (* Different raw values *) 1520 - Alcotest.(check bool) "different raw values" false 1693 + Alcotest.(check bool) 1694 + "different raw values" false 1521 1695 (value c1 = value c2); 1522 1696 (* Same trimmed values *) 1523 - Alcotest.(check string) "same trimmed values" 1524 - (value_trimmed c1) (value_trimmed c2) 1697 + Alcotest.(check string) 1698 + "same trimmed values" (value_trimmed c1) (value_trimmed c2) 1525 1699 | None -> Alcotest.fail "Parse failed for unquoted" 1526 - end 1700 + end 1527 1701 | None -> Alcotest.fail "Parse failed for quoted" 1528 1702 1529 1703 (* Priority 2.4: Cookie Header Parsing *) 1530 1704 1531 1705 let test_cookie_header_parsing_basic env = 1532 1706 let clock = Eio.Stdenv.clock env in 1533 - let results = of_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/" 1534 - "session=abc123; theme=dark; lang=en" in 1707 + let results = 1708 + of_cookie_header 1709 + ~now:(fun () -> 1710 + Ptime.of_float_s (Eio.Time.now clock) 1711 + |> Option.value ~default:Ptime.epoch) 1712 + ~domain:"ex.com" ~path:"/" "session=abc123; theme=dark; lang=en" 1713 + in 1535 1714 1536 1715 let cookies = List.filter_map Result.to_option results in 1537 1716 Alcotest.(check int) "parsed 3 cookies" 3 (List.length cookies); ··· 1544 1723 let test_cookie_header_defaults env = 1545 1724 let clock = Eio.Stdenv.clock env in 1546 1725 1547 - match of_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/app" 1548 - "session=xyz" with 1549 - | [Ok c] -> 1726 + match 1727 + of_cookie_header 1728 + ~now:(fun () -> 1729 + Ptime.of_float_s (Eio.Time.now clock) 1730 + |> Option.value ~default:Ptime.epoch) 1731 + ~domain:"example.com" ~path:"/app" "session=xyz" 1732 + with 1733 + | [ Ok c ] -> 1550 1734 (* Domain and path from request context *) 1551 1735 Alcotest.(check string) "domain from context" "example.com" (domain c); 1552 1736 Alcotest.(check string) "path from context" "/app" (path c); ··· 1557 1741 Alcotest.(check bool) "partitioned default" false (partitioned c); 1558 1742 1559 1743 (* Optional attributes default to None *) 1560 - Alcotest.(check (option expiration_testable)) "no expiration" 1561 - None (expires c); 1562 - Alcotest.(check (option span_testable)) "no max_age" 1563 - None (max_age c); 1564 - Alcotest.(check (option same_site_testable)) "no same_site" 1565 - None (same_site c) 1744 + Alcotest.(check (option expiration_testable)) 1745 + "no expiration" None (expires c); 1746 + Alcotest.(check (option span_testable)) "no max_age" None (max_age c); 1747 + Alcotest.(check (option same_site_testable)) 1748 + "no same_site" None (same_site c) 1566 1749 | _ -> Alcotest.fail "Should parse single cookie" 1567 1750 1568 1751 let test_cookie_header_edge_cases env = 1569 1752 let clock = Eio.Stdenv.clock env in 1570 1753 1571 1754 let test input expected_count description = 1572 - let results = of_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/" input in 1755 + let results = 1756 + of_cookie_header 1757 + ~now:(fun () -> 1758 + Ptime.of_float_s (Eio.Time.now clock) 1759 + |> Option.value ~default:Ptime.epoch) 1760 + ~domain:"ex.com" ~path:"/" input 1761 + in 1573 1762 let cookies = List.filter_map Result.to_option results in 1574 1763 Alcotest.(check int) description expected_count (List.length cookies) 1575 1764 in ··· 1584 1773 let clock = Eio.Stdenv.clock env in 1585 1774 1586 1775 (* Mix of valid and invalid cookies *) 1587 - let results = of_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/" 1588 - "valid=1;=noname;valid2=2" in 1776 + let results = 1777 + of_cookie_header 1778 + ~now:(fun () -> 1779 + Ptime.of_float_s (Eio.Time.now clock) 1780 + |> Option.value ~default:Ptime.epoch) 1781 + ~domain:"ex.com" ~path:"/" "valid=1;=noname;valid2=2" 1782 + in 1589 1783 1590 1784 Alcotest.(check int) "total results" 3 (List.length results); 1591 1785 ··· 1606 1800 | Error msg -> 1607 1801 let has_name = contains_substring msg "name" in 1608 1802 let has_empty = contains_substring msg "empty" in 1609 - Alcotest.(check bool) "error mentions name or empty" true 1610 - (has_name || has_empty) 1803 + Alcotest.(check bool) 1804 + "error mentions name or empty" true (has_name || has_empty) 1611 1805 | Ok _ -> Alcotest.fail "Expected error" 1612 1806 end 1613 1807 ··· 1615 1809 1616 1810 let test_max_age_and_expires_both_present env = 1617 1811 let clock = Eio.Stdenv.clock env in 1618 - let now = Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch in 1812 + let now = 1813 + Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch 1814 + in 1619 1815 let future = Ptime.add_span now (Ptime.Span.of_int_s 7200) |> Option.get in 1620 1816 1621 1817 (* Create cookie with both *) 1622 - let cookie = make ~domain:"ex.com" ~path:"/" ~name:"dual" ~value:"val" 1623 - ~max_age:(Ptime.Span.of_int_s 3600) 1624 - ~expires:(`DateTime future) 1625 - ~creation_time:now ~last_access:now () in 1818 + let cookie = 1819 + make ~domain:"ex.com" ~path:"/" ~name:"dual" ~value:"val" 1820 + ~max_age:(Ptime.Span.of_int_s 3600) ~expires:(`DateTime future) 1821 + ~creation_time:now ~last_access:now () 1822 + in 1626 1823 1627 1824 (* Both should be present *) 1628 1825 begin match max_age cookie with 1629 - | Some span -> 1630 - begin match Ptime.Span.to_int_s span with 1826 + | Some span -> begin 1827 + match Ptime.Span.to_int_s span with 1631 1828 | Some s -> 1632 1829 Alcotest.(check int64) "max_age present" 3600L (Int64.of_int s) 1633 1830 | None -> Alcotest.fail "max_age span could not be converted to int" 1634 - end 1831 + end 1635 1832 | None -> Alcotest.fail "max_age should be present" 1636 1833 end; 1637 1834 ··· 1657 1854 let clock = Eio.Stdenv.clock env in 1658 1855 1659 1856 (* Parse Set-Cookie with both attributes *) 1660 - match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/" 1661 - "id=123; Max-Age=3600; Expires=Wed, 21 Oct 2025 07:28:00 GMT" with 1857 + match 1858 + of_set_cookie_header 1859 + ~now:(fun () -> 1860 + Ptime.of_float_s (Eio.Time.now clock) 1861 + |> Option.value ~default:Ptime.epoch) 1862 + ~domain:"ex.com" ~path:"/" 1863 + "id=123; Max-Age=3600; Expires=Wed, 21 Oct 2025 07:28:00 GMT" 1864 + with 1662 1865 | Some c -> 1663 1866 (* Both should be stored *) 1664 1867 begin match max_age c with 1665 - | Some span -> 1666 - begin match Ptime.Span.to_int_s span with 1868 + | Some span -> begin 1869 + match Ptime.Span.to_int_s span with 1667 1870 | Some s -> 1668 1871 Alcotest.(check int64) "max_age parsed" 3600L (Int64.of_int s) 1669 1872 | None -> Alcotest.fail "max_age span could not be converted to int" 1670 - end 1873 + end 1671 1874 | None -> Alcotest.fail "max_age should be parsed" 1672 1875 end; 1673 1876