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

fmt

Changed files
+389 -193
lib
test
+16 -12
lib/core/cookeio.ml
···
| "domain" -> attrs.domain <- Some (normalize_domain attr_value)
| "path" -> attrs.path <- Some attr_value
| "expires" -> (
-
(* Special case: Expires=0 means session cookie *)
-
if attr_value = "0" then attrs.expires <- Some `Session
+
if
+
(* Special case: Expires=0 means session cookie *)
+
attr_value = "0"
+
then attrs.expires <- Some `Session
else
match Ptime.of_rfc3339 attr_value with
| Ok (time, _, _) -> attrs.expires <- Some (`DateTime time)
···
(* Store the max-age as a Ptime.Span *)
attrs.max_age <- Some (Ptime.Span.of_int_s seconds);
(* Also compute and store expires as DateTime *)
-
let expires = Ptime.add_span current_time (Ptime.Span.of_int_s seconds) in
+
let expires =
+
Ptime.add_span current_time (Ptime.Span.of_int_s seconds)
+
in
(match expires with
| Some time -> attrs.expires <- Some (`DateTime time)
| None -> ());
···
if attrs.partitioned && not attrs.secure then (
Log.warn (fun m ->
m
-
"Cookie has Partitioned attribute but Secure flag is not set; \
-
this violates CHIPS requirements");
+
"Cookie has Partitioned attribute but Secure flag is not set; this \
+
violates CHIPS requirements");
false)
else true
in
···
let path = Option.value attrs.path ~default:request_path in
make ~domain ~path ~name ~value ~secure:attrs.secure
~http_only:attrs.http_only ?expires:attrs.expires ?max_age:attrs.max_age
-
?same_site:attrs.same_site ~partitioned:attrs.partitioned
-
~creation_time:now ~last_access:now ()
+
?same_site:attrs.same_site ~partitioned:attrs.partitioned ~creation_time:now
+
~last_access:now ()
(** {1 Pretty Printing} *)
let pp ppf cookie =
Format.fprintf ppf
"@[<hov 2>{ name=%S;@ value=%S;@ domain=%S;@ path=%S;@ secure=%b;@ \
-
http_only=%b;@ partitioned=%b;@ expires=%a;@ max_age=%a;@ same_site=%a }@]"
+
http_only=%b;@ partitioned=%b;@ expires=%a;@ max_age=%a;@ same_site=%a \
+
}@]"
(name cookie) (value cookie) (domain cookie) (path cookie) (secure cookie)
(http_only cookie) (partitioned cookie)
(Format.pp_print_option Expiration.pp)
···
Error (Printf.sprintf "Cookie missing '=' separator: %s" name_value)
| Some eq_pos ->
let cookie_name = String.sub name_value 0 eq_pos |> String.trim in
-
if String.length cookie_name = 0 then
-
Error "Cookie has empty name"
+
if String.length cookie_name = 0 then Error "Cookie has empty name"
else
let cookie_value =
String.sub name_value (eq_pos + 1)
···
(* Create cookie with defaults from Cookie header context *)
let cookie =
make ~domain ~path ~name:cookie_name ~value:cookie_value
-
~secure:false ~http_only:false ~partitioned:false ~creation_time:current_time
-
~last_access:current_time ()
+
~secure:false ~http_only:false ~partitioned:false
+
~creation_time:current_time ~last_access:current_time ()
in
Ok cookie)
parts
+6 -5
lib/core/cookeio.mli
···
protection
- [`Lax]: Cookie sent for same-site requests and top-level navigation
(default for modern browsers)
-
- [`None]: Cookie sent for all cross-site requests (requires [secure] flag) *)
+
- [`None]: Cookie sent for all cross-site requests (requires [secure]
+
flag) *)
val equal : t -> t -> bool
(** Equality function for same-site values *)
···
- [Partitioned] requires the [Secure] flag to be set
Example:
-
[of_set_cookie_header ~now:(fun () -> Ptime_clock.now ()) ~domain:"example.com" ~path:"/" "session=abc123;
-
Secure; HttpOnly"] *)
+
[of_set_cookie_header ~now:(fun () -> Ptime_clock.now ())
+
~domain:"example.com" ~path:"/" "session=abc123; Secure; HttpOnly"] *)
val of_cookie_header :
now:(unit -> Ptime.t) ->
···
values and excess whitespace are ignored.
Example:
-
[of_cookie_header ~now:(fun () -> Ptime_clock.now ()) ~domain:"example.com" ~path:"/"
-
"session=abc; theme=dark"] *)
+
[of_cookie_header ~now:(fun () -> Ptime_clock.now ()) ~domain:"example.com"
+
~path:"/" "session=abc; theme=dark"] *)
val make_cookie_header : t list -> string
(** Create cookie header value from cookies.
+23 -44
lib/jar/cookeio_jar.ml
···
let is_expired cookie clock =
match Cookeio.expires cookie with
| None -> false (* No expiration *)
-
| Some `Session -> false (* Session cookie - not expired until browser closes *)
+
| Some `Session ->
+
false (* Session cookie - not expired until browser closes *)
| Some (`DateTime exp_time) ->
let now =
Ptime.of_float_s (Eio.Time.now clock)
···
let add_cookie jar cookie =
Log.debug (fun m ->
-
m "Adding cookie to delta: %s=%s for domain %s"
-
(Cookeio.name cookie)
-
(Cookeio.value cookie)
-
(Cookeio.domain cookie));
+
m "Adding cookie to delta: %s=%s for domain %s" (Cookeio.name cookie)
+
(Cookeio.value cookie) (Cookeio.domain cookie));
Eio.Mutex.lock jar.mutex;
(* Remove existing cookie with same identity from delta *)
···
let add_original jar cookie =
Log.debug (fun m ->
-
m "Adding original cookie: %s=%s for domain %s"
-
(Cookeio.name cookie)
-
(Cookeio.value cookie)
-
(Cookeio.domain cookie));
+
m "Adding original cookie: %s=%s for domain %s" (Cookeio.name cookie)
+
(Cookeio.value cookie) (Cookeio.domain cookie));
Eio.Mutex.lock jar.mutex;
(* Remove existing cookie with same identity from original *)
···
Ptime.sub_span now (Ptime.Span.of_int_s (365 * 24 * 60 * 60))
|> Option.value ~default:Ptime.epoch
in
-
Cookeio.make
-
~domain:(Cookeio.domain cookie)
-
~path:(Cookeio.path cookie)
-
~name:(Cookeio.name cookie)
-
~value:""
-
~secure:(Cookeio.secure cookie)
-
~http_only:(Cookeio.http_only cookie)
-
~expires:(`DateTime past_expiry)
-
~max_age:(Ptime.Span.of_int_s 0)
-
?same_site:(Cookeio.same_site cookie)
+
Cookeio.make ~domain:(Cookeio.domain cookie) ~path:(Cookeio.path cookie)
+
~name:(Cookeio.name cookie) ~value:"" ~secure:(Cookeio.secure cookie)
+
~http_only:(Cookeio.http_only cookie) ~expires:(`DateTime past_expiry)
+
~max_age:(Ptime.Span.of_int_s 0) ?same_site:(Cookeio.same_site cookie)
~partitioned:(Cookeio.partitioned cookie)
~creation_time:now ~last_access:now ()
let remove jar ~clock cookie =
Log.debug (fun m ->
-
m "Removing cookie: %s=%s for domain %s"
-
(Cookeio.name cookie)
-
(Cookeio.value cookie)
-
(Cookeio.domain cookie));
+
m "Removing cookie: %s=%s for domain %s" (Cookeio.name cookie)
+
(Cookeio.value cookie) (Cookeio.domain cookie));
Eio.Mutex.lock jar.mutex;
(* Check if this cookie exists in original_cookies *)
···
List.map
(fun c ->
if List.exists (fun a -> cookie_identity_matches a c) applicable then
-
Cookeio.make
-
~domain:(Cookeio.domain c)
-
~path:(Cookeio.path c)
-
~name:(Cookeio.name c)
-
~value:(Cookeio.value c)
-
~secure:(Cookeio.secure c)
-
~http_only:(Cookeio.http_only c)
-
?expires:(Cookeio.expires c)
-
?max_age:(Cookeio.max_age c)
+
Cookeio.make ~domain:(Cookeio.domain c) ~path:(Cookeio.path c)
+
~name:(Cookeio.name c) ~value:(Cookeio.value c)
+
~secure:(Cookeio.secure c) ~http_only:(Cookeio.http_only c)
+
?expires:(Cookeio.expires c) ?max_age:(Cookeio.max_age c)
?same_site:(Cookeio.same_site c)
~partitioned:(Cookeio.partitioned c)
-
~creation_time:(Cookeio.creation_time c)
-
~last_access:now ()
+
~creation_time:(Cookeio.creation_time c) ~last_access:now ()
else c)
cookies
in
···
in
Buffer.add_string buffer
-
(Printf.sprintf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
-
(Cookeio.domain cookie)
-
include_subdomains
-
(Cookeio.path cookie)
-
secure_flag expires_str
-
(Cookeio.name cookie)
-
(Cookeio.value cookie)))
+
(Printf.sprintf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" (Cookeio.domain cookie)
+
include_subdomains (Cookeio.path cookie) secure_flag expires_str
+
(Cookeio.name cookie) (Cookeio.value cookie)))
unique;
Buffer.contents buffer
···
let cookie =
Cookeio.make ~domain:(normalize_domain domain) ~path ~name ~value
-
~secure:(secure = "TRUE") ~http_only:false ?expires ?max_age:None
-
?same_site:None ~partitioned:false ~creation_time:now
-
~last_access:now ()
+
~secure:(secure = "TRUE") ~http_only:false ?expires
+
?max_age:None ?same_site:None ~partitioned:false
+
~creation_time:now ~last_access:now ()
in
add_original jar cookie;
Log.debug (fun m -> m "Loaded cookie: %s=%s" name value)
+10 -1
test/dune
···
(test
(name test_cookeio)
-
(libraries cookeio cookeio_jar alcotest eio eio.unix eio_main eio.mock ptime str)
+
(libraries
+
cookeio
+
cookeio_jar
+
alcotest
+
eio
+
eio.unix
+
eio_main
+
eio.mock
+
ptime
+
str)
(deps cookies.txt))
+334 -131
test/test_cookeio.ml
···
partitioned=%b; expires=%a; max_age=%a; same_site=%a }"
(Cookeio.name c) (Cookeio.value c) (Cookeio.domain c) (Cookeio.path c)
(Cookeio.secure c) (Cookeio.http_only c) (Cookeio.partitioned c)
-
(Format.pp_print_option
-
(fun ppf e ->
+
(Format.pp_print_option (fun ppf e ->
match e with
| `Session -> Format.pp_print_string ppf "Session"
| `DateTime t -> Format.fprintf ppf "DateTime(%a)" Ptime.pp t))
(Cookeio.expires c)
(Format.pp_print_option Ptime.Span.pp)
(Cookeio.max_age c)
-
(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"))
+
(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"))
(Cookeio.same_site c))
(fun c1 c2 ->
let expires_equal e1 e2 =
···
begin match expected_expiry with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"cookie-3 expires" (Some (`DateTime t)) (Cookeio.expires cookie3)
+
"cookie-3 expires"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie3)
| None -> Alcotest.fail "Expected expiry time for cookie-3"
end;
···
begin match expected_expiry with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"cookie-4 expires" (Some (`DateTime t)) (Cookeio.expires cookie4)
+
"cookie-4 expires"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie4)
| None -> Alcotest.fail "Expected expiry time for cookie-4"
end;
···
begin match expected_expiry with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"cookie-5 expires" (Some (`DateTime t)) (Cookeio.expires cookie5)
+
"cookie-5 expires"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie5)
| None -> Alcotest.fail "Expected expiry time for cookie-5"
end
···
begin match expected_expiry with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"file cookie-5 expires" (Some (`DateTime t)) (Cookeio.expires cookie5)
+
"file cookie-5 expires"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie5)
| None -> Alcotest.fail "Expected expiry time for cookie-5"
end;
···
begin match Ptime.of_float_s 1257894000.0 with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"round trip expires" (Some (`DateTime t)) (Cookeio.expires cookie2)
+
"round trip expires"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie2)
| None -> Alcotest.fail "Expected expiry time"
end
···
(* Parse a Set-Cookie header with Max-Age *)
let header = "session=abc123; Max-Age=3600; Secure; HttpOnly" in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
···
begin match expected_expiry with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"expires set from max-age" (Some (`DateTime t)) (Cookeio.expires cookie)
+
"expires set from max-age"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie)
| None -> Alcotest.fail "Expected expiry time"
end;
···
"id=xyz789; Expires=2025-10-21T07:28:00Z; Path=/; Domain=.example.com"
in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
···
match expected_expiry with
| Ok (time, _, _) ->
Alcotest.(check (option expiration_testable))
-
"expires matches parsed value" (Some (`DateTime time))
+
"expires matches parsed value"
+
(Some (`DateTime time))
(Cookeio.expires cookie)
| Error _ -> Alcotest.fail "Failed to parse expected expiry time"
···
(* This should be rejected: SameSite=None without Secure *)
let invalid_header = "token=abc; SameSite=None" in
let cookie_opt =
-
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
+
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
in
Alcotest.(check bool)
···
(* This should be accepted: SameSite=None with Secure *)
let valid_header = "token=abc; SameSite=None; Secure" in
let cookie_opt2 =
-
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
+
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
in
Alcotest.(check bool)
···
(* Test parsing ".example.com" stores as "example.com" *)
let header = "test=value; Domain=.example.com" in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
let cookie = Option.get cookie_opt in
···
(* Parse a Set-Cookie header with Max-Age *)
let header = "session=abc123; Max-Age=3600" in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
···
begin match expected_expiry with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"expires computed from max-age" (Some (`DateTime t))
+
"expires computed from max-age"
+
(Some (`DateTime t))
(Cookeio.expires cookie)
| None -> Alcotest.fail "Expected expiry time"
end
···
(* Parse a Set-Cookie header with negative Max-Age *)
let header = "session=abc123; Max-Age=-100" in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
···
begin match expected_expiry with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"expires computed with 0 seconds" (Some (`DateTime t))
+
"expires computed with 0 seconds"
+
(Some (`DateTime t))
(Cookeio.expires cookie)
| None -> Alcotest.fail "Expected expiry time"
end
···
let expires_time = Ptime.of_float_s 8600.0 |> Option.get in
let cookie =
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"session" ~value:"abc123"
-
~secure:true ~http_only:true ?expires:(Some (`DateTime expires_time))
+
~secure:true ~http_only:true
+
?expires:(Some (`DateTime expires_time))
?max_age:(Some max_age_span) ?same_site:(Some `Strict)
~creation_time:(Ptime.of_float_s 5000.0 |> Option.get)
~last_access:(Ptime.of_float_s 5000.0 |> Option.get)
···
(* Parse a cookie with Max-Age *)
let header = "session=xyz; Max-Age=7200; Secure; HttpOnly" in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
let cookie = Option.get cookie_opt in
···
Eio_mock.Clock.set_time clock 5000.0;
(* Reset clock to same time *)
let cookie2_opt =
-
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
+
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
in
Alcotest.(check bool) "cookie re-parsed" true (Option.is_some cookie2_opt);
let cookie2 = Option.get cookie2_opt in
···
(* Test FMT1: "Wed, 21 Oct 2015 07:28:00 GMT" (RFC 1123) *)
let header = "session=abc; Expires=Wed, 21 Oct 2015 07:28:00 GMT" in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
Alcotest.(check bool) "FMT1 cookie parsed" true (Option.is_some cookie_opt);
···
begin match expected with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"FMT1 expiry correct" (Some (`DateTime t)) (Cookeio.expires cookie)
+
"FMT1 expiry correct"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie)
| None -> Alcotest.fail "Expected expiry time for FMT1"
end
···
(* Test FMT2: "Wednesday, 21-Oct-15 07:28:00 GMT" (RFC 850 with abbreviated year) *)
let header = "session=abc; Expires=Wednesday, 21-Oct-15 07:28:00 GMT" in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
Alcotest.(check bool) "FMT2 cookie parsed" true (Option.is_some cookie_opt);
···
begin match expected with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"FMT2 expiry correct with year normalization" (Some (`DateTime t))
+
"FMT2 expiry correct with year normalization"
+
(Some (`DateTime t))
(Cookeio.expires cookie)
| None -> Alcotest.fail "Expected expiry time for FMT2"
end
···
(* Test FMT3: "Wed Oct 21 07:28:00 2015" (asctime) *)
let header = "session=abc; Expires=Wed Oct 21 07:28:00 2015" in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
Alcotest.(check bool) "FMT3 cookie parsed" true (Option.is_some cookie_opt);
···
begin match expected with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"FMT3 expiry correct" (Some (`DateTime t)) (Cookeio.expires cookie)
+
"FMT3 expiry correct"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie)
| None -> Alcotest.fail "Expected expiry time for FMT3"
end
···
(* Test FMT4: "Wed, 21-Oct-2015 07:28:00 GMT" (variant) *)
let header = "session=abc; Expires=Wed, 21-Oct-2015 07:28:00 GMT" in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
Alcotest.(check bool) "FMT4 cookie parsed" true (Option.is_some cookie_opt);
···
begin match expected with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"FMT4 expiry correct" (Some (`DateTime t)) (Cookeio.expires cookie)
+
"FMT4 expiry correct"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie)
| None -> Alcotest.fail "Expected expiry time for FMT4"
end
···
(* Year 95 should become 1995 *)
let header = "session=abc; Expires=Wed, 21-Oct-95 07:28:00 GMT" in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
let cookie = Option.get cookie_opt in
let expected = Ptime.of_date_time ((1995, 10, 21), ((07, 28, 00), 0)) in
begin match expected with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"year 95 becomes 1995" (Some (`DateTime t)) (Cookeio.expires cookie)
+
"year 95 becomes 1995"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie)
| None -> Alcotest.fail "Expected expiry time for year 95"
end;
(* Year 69 should become 1969 *)
let header2 = "session=abc; Expires=Wed, 10-Sep-69 20:00:00 GMT" in
let cookie_opt2 =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header2
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header2
in
let cookie2 = Option.get cookie_opt2 in
let expected2 = Ptime.of_date_time ((1969, 9, 10), ((20, 0, 0), 0)) in
begin match expected2 with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"year 69 becomes 1969" (Some (`DateTime t)) (Cookeio.expires cookie2)
+
"year 69 becomes 1969"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie2)
| None -> Alcotest.fail "Expected expiry time for year 69"
end;
(* Year 99 should become 1999 *)
let header3 = "session=abc; Expires=Thu, 10-Sep-99 20:00:00 GMT" in
let cookie_opt3 =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header3
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header3
in
let cookie3 = Option.get cookie_opt3 in
let expected3 = Ptime.of_date_time ((1999, 9, 10), ((20, 0, 0), 0)) in
begin match expected3 with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"year 99 becomes 1999" (Some (`DateTime t)) (Cookeio.expires cookie3)
+
"year 99 becomes 1999"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie3)
| None -> Alcotest.fail "Expected expiry time for year 99"
end
···
(* Year 25 should become 2025 *)
let header = "session=abc; Expires=Wed, 21-Oct-25 07:28:00 GMT" in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
let cookie = Option.get cookie_opt in
let expected = Ptime.of_date_time ((2025, 10, 21), ((07, 28, 00), 0)) in
begin match expected with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"year 25 becomes 2025" (Some (`DateTime t)) (Cookeio.expires cookie)
+
"year 25 becomes 2025"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie)
| None -> Alcotest.fail "Expected expiry time for year 25"
end;
(* Year 0 should become 2000 *)
let header2 = "session=abc; Expires=Fri, 01-Jan-00 00:00:00 GMT" in
let cookie_opt2 =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header2
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header2
in
let cookie2 = Option.get cookie_opt2 in
let expected2 = Ptime.of_date_time ((2000, 1, 1), ((0, 0, 0), 0)) in
begin match expected2 with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"year 0 becomes 2000" (Some (`DateTime t)) (Cookeio.expires cookie2)
+
"year 0 becomes 2000"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie2)
| None -> Alcotest.fail "Expected expiry time for year 0"
end;
(* Year 68 should become 2068 *)
let header3 = "session=abc; Expires=Thu, 10-Sep-68 20:00:00 GMT" in
let cookie_opt3 =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header3
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header3
in
let cookie3 = Option.get cookie_opt3 in
let expected3 = Ptime.of_date_time ((2068, 9, 10), ((20, 0, 0), 0)) in
begin match expected3 with
| Some t ->
Alcotest.(check (option expiration_testable))
-
"year 68 becomes 2068" (Some (`DateTime t)) (Cookeio.expires cookie3)
+
"year 68 becomes 2068"
+
(Some (`DateTime t))
+
(Cookeio.expires cookie3)
| None -> Alcotest.fail "Expected expiry time for year 68"
end
···
(* Ensure RFC 3339 format still works for backward compatibility *)
let header = "session=abc; Expires=2025-10-21T07:28:00Z" in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
Alcotest.(check bool)
"RFC 3339 cookie parsed" true
···
match expected with
| Ok (time, _, _) ->
Alcotest.(check (option expiration_testable))
-
"RFC 3339 expiry correct" (Some (`DateTime time)) (Cookeio.expires cookie)
+
"RFC 3339 expiry correct"
+
(Some (`DateTime time))
+
(Cookeio.expires cookie)
| Error _ -> Alcotest.fail "Failed to parse expected RFC 3339 time"
let test_invalid_date_format_logs_warning () =
···
(* Invalid date format should log a warning but still parse the cookie *)
let header = "session=abc; Expires=InvalidDate" in
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
(* Cookie should still be parsed, just without expires *)
···
List.iter
(fun (header, description) ->
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
Alcotest.(check bool)
(description ^ " parsed") true
···
List.iter
(fun (header, description) ->
let cookie_opt =
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/" header
in
Alcotest.(check bool)
(description ^ " parsed") true
···
let test_partitioned_parsing env =
let clock = Eio.Stdenv.clock env in
-
match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"widget.com" ~path:"/"
-
"id=123; Partitioned; Secure" with
+
match
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"widget.com" ~path:"/" "id=123; Partitioned; Secure"
+
with
| Some c ->
Alcotest.(check bool) "partitioned flag" true (partitioned c);
Alcotest.(check bool) "secure flag" true (secure c)
···
let test_partitioned_serialization env =
let clock = Eio.Stdenv.clock env in
-
let now = Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch in
+
let now =
+
Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch
+
in
-
let cookie = make ~domain:"widget.com" ~path:"/" ~name:"id" ~value:"123"
-
~secure:true ~partitioned:true
-
~creation_time:now ~last_access:now () in
+
let cookie =
+
make ~domain:"widget.com" ~path:"/" ~name:"id" ~value:"123" ~secure:true
+
~partitioned:true ~creation_time:now ~last_access:now ()
+
in
let header = make_set_cookie_header cookie in
let contains_substring s sub =
···
let clock = Eio.Stdenv.clock env in
(* Partitioned without Secure should be rejected *)
-
match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"widget.com" ~path:"/"
-
"id=123; Partitioned" with
+
match
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"widget.com" ~path:"/" "id=123; Partitioned"
+
with
| None -> () (* Expected *)
| Some _ -> Alcotest.fail "Should reject Partitioned without Secure"
···
let test_expiration_variants env =
let clock = Eio.Stdenv.clock env in
-
let now = Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch in
+
let now =
+
Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch
+
in
let make_base ~name ?expires () =
-
make ~domain:"ex.com" ~path:"/" ~name ~value:"v"
-
?expires ~creation_time:now ~last_access:now ()
+
make ~domain:"ex.com" ~path:"/" ~name ~value:"v" ?expires ~creation_time:now
+
~last_access:now ()
in
(* No expiration *)
let c1 = make_base ~name:"no_expiry" () in
-
Alcotest.(check (option expiration_testable)) "no expiration"
-
None (expires c1);
+
Alcotest.(check (option expiration_testable))
+
"no expiration" None (expires c1);
(* Session cookie *)
let c2 = make_base ~name:"session" ~expires:`Session () in
-
Alcotest.(check (option expiration_testable)) "session cookie"
-
(Some `Session) (expires c2);
+
Alcotest.(check (option expiration_testable))
+
"session cookie" (Some `Session) (expires c2);
(* Explicit expiration *)
let future = Ptime.add_span now (Ptime.Span.of_int_s 3600) |> Option.get in
···
let clock = Eio.Stdenv.clock env in
(* Expires=0 should parse as Session *)
-
match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/"
-
"id=123; Expires=0" with
+
match
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"ex.com" ~path:"/" "id=123; Expires=0"
+
with
| Some c ->
-
Alcotest.(check (option expiration_testable)) "expires=0 is session"
-
(Some `Session) (expires c)
+
Alcotest.(check (option expiration_testable))
+
"expires=0 is session" (Some `Session) (expires c)
| None -> Alcotest.fail "Should parse Expires=0"
let test_serialize_expiration_variants env =
let clock = Eio.Stdenv.clock env in
-
let now = Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch in
+
let now =
+
Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch
+
in
let contains_substring s sub =
try
let _ = Str.search_forward (Str.regexp_string sub) s 0 in
···
in
(* Session cookie serialization *)
-
let c1 = make ~domain:"ex.com" ~path:"/" ~name:"s" ~value:"v"
-
~expires:`Session ~creation_time:now ~last_access:now () in
+
let c1 =
+
make ~domain:"ex.com" ~path:"/" ~name:"s" ~value:"v" ~expires:`Session
+
~creation_time:now ~last_access:now ()
+
in
let h1 = make_set_cookie_header c1 in
let has_expires = contains_substring h1 "Expires=" in
Alcotest.(check bool) "session has Expires" true has_expires;
(* DateTime serialization *)
let future = Ptime.add_span now (Ptime.Span.of_int_s 3600) |> Option.get in
-
let c2 = make ~domain:"ex.com" ~path:"/" ~name:"p" ~value:"v"
-
~expires:(`DateTime future) ~creation_time:now ~last_access:now () in
+
let c2 =
+
make ~domain:"ex.com" ~path:"/" ~name:"p" ~value:"v"
+
~expires:(`DateTime future) ~creation_time:now ~last_access:now ()
+
in
let h2 = make_set_cookie_header c2 in
let has_expires2 = contains_substring h2 "Expires=" in
Alcotest.(check bool) "datetime has Expires" true has_expires2
···
let test_quoted_cookie_values env =
let clock = Eio.Stdenv.clock env in
-
let test_cases = [
-
("name=value", "value", "value");
-
("name=\"value\"", "\"value\"", "value");
-
("name=\"partial", "\"partial", "\"partial");
-
("name=\"val\"\"", "\"val\"\"", "val\"");
-
("name=val\"", "val\"", "val\"");
-
("name=\"\"", "\"\"", "");
-
] in
+
let test_cases =
+
[
+
("name=value", "value", "value");
+
("name=\"value\"", "\"value\"", "value");
+
("name=\"partial", "\"partial", "\"partial");
+
("name=\"val\"\"", "\"val\"\"", "val\"");
+
("name=val\"", "val\"", "val\"");
+
("name=\"\"", "\"\"", "");
+
]
+
in
-
List.iter (fun (input, expected_raw, expected_trimmed) ->
-
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
-
| Some c ->
-
Alcotest.(check string)
-
(Printf.sprintf "raw value for %s" input) expected_raw (value c);
-
Alcotest.(check string)
-
(Printf.sprintf "trimmed value for %s" input) expected_trimmed
-
(value_trimmed c)
-
| None -> Alcotest.fail ("Parse failed: " ^ input)
-
) test_cases
+
List.iter
+
(fun (input, expected_raw, expected_trimmed) ->
+
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
+
| Some c ->
+
Alcotest.(check string)
+
(Printf.sprintf "raw value for %s" input)
+
expected_raw (value c);
+
Alcotest.(check string)
+
(Printf.sprintf "trimmed value for %s" input)
+
expected_trimmed (value_trimmed c)
+
| None -> Alcotest.fail ("Parse failed: " ^ input))
+
test_cases
let test_trimmed_value_not_used_for_equality env =
let clock = Eio.Stdenv.clock env in
-
match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/"
-
"name=\"value\"" with
-
| Some c1 ->
-
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:"/"
-
"name=value" with
+
match
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"ex.com" ~path:"/" "name=\"value\""
+
with
+
| Some c1 -> 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:"/" "name=value"
+
with
| Some c2 ->
(* Different raw values *)
-
Alcotest.(check bool) "different raw values" false
+
Alcotest.(check bool)
+
"different raw values" false
(value c1 = value c2);
(* Same trimmed values *)
-
Alcotest.(check string) "same trimmed values"
-
(value_trimmed c1) (value_trimmed c2)
+
Alcotest.(check string)
+
"same trimmed values" (value_trimmed c1) (value_trimmed c2)
| None -> Alcotest.fail "Parse failed for unquoted"
-
end
+
end
| None -> Alcotest.fail "Parse failed for quoted"
(* Priority 2.4: Cookie Header Parsing *)
let test_cookie_header_parsing_basic env =
let clock = Eio.Stdenv.clock env in
-
let results = of_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/"
-
"session=abc123; theme=dark; lang=en" in
+
let results =
+
of_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"ex.com" ~path:"/" "session=abc123; theme=dark; lang=en"
+
in
let cookies = List.filter_map Result.to_option results in
Alcotest.(check int) "parsed 3 cookies" 3 (List.length cookies);
···
let test_cookie_header_defaults env =
let clock = Eio.Stdenv.clock env in
-
match of_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/app"
-
"session=xyz" with
-
| [Ok c] ->
+
match
+
of_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"example.com" ~path:"/app" "session=xyz"
+
with
+
| [ Ok c ] ->
(* 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 bool) "partitioned default" false (partitioned c);
(* Optional attributes default to None *)
-
Alcotest.(check (option expiration_testable)) "no expiration"
-
None (expires 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)
+
Alcotest.(check (option expiration_testable))
+
"no expiration" None (expires 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)
| _ -> Alcotest.fail "Should parse single cookie"
let test_cookie_header_edge_cases env =
let clock = Eio.Stdenv.clock env in
let test input expected_count description =
-
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
+
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
let cookies = List.filter_map Result.to_option results in
Alcotest.(check int) description expected_count (List.length cookies)
in
···
let clock = Eio.Stdenv.clock env in
(* Mix of valid and invalid cookies *)
-
let results = of_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/"
-
"valid=1;=noname;valid2=2" in
+
let results =
+
of_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"ex.com" ~path:"/" "valid=1;=noname;valid2=2"
+
in
Alcotest.(check int) "total results" 3 (List.length results);
···
| Error msg ->
let has_name = contains_substring msg "name" in
let has_empty = contains_substring msg "empty" in
-
Alcotest.(check bool) "error mentions name or empty" true
-
(has_name || has_empty)
+
Alcotest.(check bool)
+
"error mentions name or empty" true (has_name || has_empty)
| Ok _ -> Alcotest.fail "Expected error"
end
···
let test_max_age_and_expires_both_present env =
let clock = Eio.Stdenv.clock env in
-
let now = Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch in
+
let now =
+
Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch
+
in
let future = Ptime.add_span now (Ptime.Span.of_int_s 7200) |> Option.get in
(* Create cookie with both *)
-
let cookie = make ~domain:"ex.com" ~path:"/" ~name:"dual" ~value:"val"
-
~max_age:(Ptime.Span.of_int_s 3600)
-
~expires:(`DateTime future)
-
~creation_time:now ~last_access:now () in
+
let cookie =
+
make ~domain:"ex.com" ~path:"/" ~name:"dual" ~value:"val"
+
~max_age:(Ptime.Span.of_int_s 3600) ~expires:(`DateTime future)
+
~creation_time:now ~last_access:now ()
+
in
(* Both should be present *)
begin match max_age cookie with
-
| Some span ->
-
begin match Ptime.Span.to_int_s span with
+
| Some span -> begin
+
match Ptime.Span.to_int_s span with
| Some s ->
Alcotest.(check int64) "max_age present" 3600L (Int64.of_int s)
| None -> Alcotest.fail "max_age span could not be converted to int"
-
end
+
end
| None -> Alcotest.fail "max_age should be present"
end;
···
let clock = Eio.Stdenv.clock env in
(* Parse Set-Cookie with both attributes *)
-
match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/"
-
"id=123; Max-Age=3600; Expires=Wed, 21 Oct 2025 07:28:00 GMT" with
+
match
+
of_set_cookie_header
+
~now:(fun () ->
+
Ptime.of_float_s (Eio.Time.now clock)
+
|> Option.value ~default:Ptime.epoch)
+
~domain:"ex.com" ~path:"/"
+
"id=123; Max-Age=3600; Expires=Wed, 21 Oct 2025 07:28:00 GMT"
+
with
| Some c ->
(* Both should be stored *)
begin match max_age c with
-
| Some span ->
-
begin match Ptime.Span.to_int_s span with
+
| Some span -> begin
+
match Ptime.Span.to_int_s span with
| Some s ->
Alcotest.(check int64) "max_age parsed" 3600L (Int64.of_int s)
| None -> Alcotest.fail "max_age span could not be converted to int"
-
end
+
end
| None -> Alcotest.fail "max_age should be parsed"
end;