···
| None -> Alcotest.fail "Should parse cookie with both attributes"
1888
+
(* ============================================================================ *)
1889
+
(* Host-Only Flag Tests (RFC 6265 Section 5.3) *)
1890
+
(* ============================================================================ *)
1892
+
let test_host_only_without_domain_attribute () =
1893
+
Eio_mock.Backend.run @@ fun () ->
1894
+
let clock = Eio_mock.Clock.make () in
1895
+
Eio_mock.Clock.set_time clock 1000.0;
1897
+
(* Cookie without Domain attribute should have host_only=true *)
1898
+
let header = "session=abc123; Secure; HttpOnly" in
1900
+
of_set_cookie_header
1902
+
Ptime.of_float_s (Eio.Time.now clock)
1903
+
|> Option.value ~default:Ptime.epoch)
1904
+
~domain:"example.com" ~path:"/" header
1906
+
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
1907
+
let cookie = Option.get cookie_opt in
1908
+
Alcotest.(check bool) "host_only is true" true (Cookeio.host_only cookie);
1909
+
Alcotest.(check string) "domain is request host" "example.com" (Cookeio.domain cookie)
1911
+
let test_host_only_with_domain_attribute () =
1912
+
Eio_mock.Backend.run @@ fun () ->
1913
+
let clock = Eio_mock.Clock.make () in
1914
+
Eio_mock.Clock.set_time clock 1000.0;
1916
+
(* Cookie with Domain attribute should have host_only=false *)
1917
+
let header = "session=abc123; Domain=example.com; Secure" in
1919
+
of_set_cookie_header
1921
+
Ptime.of_float_s (Eio.Time.now clock)
1922
+
|> Option.value ~default:Ptime.epoch)
1923
+
~domain:"example.com" ~path:"/" header
1925
+
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
1926
+
let cookie = Option.get cookie_opt in
1927
+
Alcotest.(check bool) "host_only is false" false (Cookeio.host_only cookie);
1928
+
Alcotest.(check string) "domain is attribute value" "example.com" (Cookeio.domain cookie)
1930
+
let test_host_only_with_dotted_domain_attribute () =
1931
+
Eio_mock.Backend.run @@ fun () ->
1932
+
let clock = Eio_mock.Clock.make () in
1933
+
Eio_mock.Clock.set_time clock 1000.0;
1935
+
(* Cookie with .domain should have host_only=false and normalized domain *)
1936
+
let header = "session=abc123; Domain=.example.com" in
1938
+
of_set_cookie_header
1940
+
Ptime.of_float_s (Eio.Time.now clock)
1941
+
|> Option.value ~default:Ptime.epoch)
1942
+
~domain:"example.com" ~path:"/" header
1944
+
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
1945
+
let cookie = Option.get cookie_opt in
1946
+
Alcotest.(check bool) "host_only is false" false (Cookeio.host_only cookie);
1947
+
Alcotest.(check string) "domain normalized" "example.com" (Cookeio.domain cookie)
1949
+
let test_host_only_domain_matching () =
1950
+
Eio_mock.Backend.run @@ fun () ->
1951
+
let clock = Eio_mock.Clock.make () in
1952
+
Eio_mock.Clock.set_time clock 1000.0;
1954
+
let jar = create () in
1956
+
(* Add a host-only cookie (no Domain attribute) *)
1957
+
let host_only_cookie =
1958
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"host_only" ~value:"val1"
1959
+
~secure:false ~http_only:false ~host_only:true
1960
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1961
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
1963
+
add_cookie jar host_only_cookie;
1965
+
(* Add a domain cookie (with Domain attribute) *)
1966
+
let domain_cookie =
1967
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"domain" ~value:"val2"
1968
+
~secure:false ~http_only:false ~host_only:false
1969
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
1970
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
1972
+
add_cookie jar domain_cookie;
1974
+
(* Both cookies should match exact domain *)
1975
+
let cookies_exact =
1976
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
1978
+
Alcotest.(check int) "both match exact domain" 2 (List.length cookies_exact);
1980
+
(* Only domain cookie should match subdomain *)
1982
+
get_cookies jar ~clock ~domain:"sub.example.com" ~path:"/" ~is_secure:false
1984
+
Alcotest.(check int) "only domain cookie matches subdomain" 1 (List.length cookies_sub);
1985
+
let sub_cookie = List.hd cookies_sub in
1986
+
Alcotest.(check string) "subdomain match is domain cookie" "domain" (Cookeio.name sub_cookie)
1988
+
let test_host_only_cookie_header_parsing () =
1989
+
Eio_mock.Backend.run @@ fun () ->
1990
+
let clock = Eio_mock.Clock.make () in
1991
+
Eio_mock.Clock.set_time clock 1000.0;
1993
+
(* Cookies from Cookie header should have host_only=true *)
1997
+
Ptime.of_float_s (Eio.Time.now clock)
1998
+
|> Option.value ~default:Ptime.epoch)
1999
+
~domain:"example.com" ~path:"/" "session=abc; theme=dark"
2001
+
let cookies = List.filter_map Result.to_option results in
2002
+
Alcotest.(check int) "parsed 2 cookies" 2 (List.length cookies);
2003
+
List.iter (fun c ->
2004
+
Alcotest.(check bool)
2005
+
("host_only is true for " ^ Cookeio.name c)
2006
+
true (Cookeio.host_only c)
2009
+
let test_host_only_mozilla_format_round_trip () =
2010
+
Eio_mock.Backend.run @@ fun () ->
2011
+
let clock = Eio_mock.Clock.make () in
2012
+
Eio_mock.Clock.set_time clock 1000.0;
2014
+
let jar = create () in
2016
+
(* Add host-only cookie *)
2018
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"hostonly" ~value:"v1"
2019
+
~secure:false ~http_only:false ~host_only:true
2020
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2021
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2023
+
add_cookie jar host_only;
2025
+
(* Add domain cookie *)
2026
+
let domain_cookie =
2027
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"domain" ~value:"v2"
2028
+
~secure:false ~http_only:false ~host_only:false
2029
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2030
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2032
+
add_cookie jar domain_cookie;
2034
+
(* Round trip through Mozilla format *)
2035
+
let mozilla = to_mozilla_format jar in
2036
+
let jar2 = from_mozilla_format ~clock mozilla in
2037
+
let cookies = get_all_cookies jar2 in
2039
+
Alcotest.(check int) "2 cookies after round trip" 2 (List.length cookies);
2041
+
let find name_val = List.find (fun c -> Cookeio.name c = name_val) cookies in
2042
+
Alcotest.(check bool) "hostonly preserved" true (Cookeio.host_only (find "hostonly"));
2043
+
Alcotest.(check bool) "domain preserved" false (Cookeio.host_only (find "domain"))
2045
+
(* ============================================================================ *)
2046
+
(* Path Matching Tests (RFC 6265 Section 5.1.4) *)
2047
+
(* ============================================================================ *)
2049
+
let test_path_matching_identical () =
2050
+
Eio_mock.Backend.run @@ fun () ->
2051
+
let clock = Eio_mock.Clock.make () in
2052
+
Eio_mock.Clock.set_time clock 1000.0;
2054
+
let jar = create () in
2056
+
Cookeio.make ~domain:"example.com" ~path:"/foo" ~name:"test" ~value:"val"
2057
+
~secure:false ~http_only:false
2058
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2059
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2061
+
add_cookie jar cookie;
2063
+
(* Identical path should match *)
2065
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/foo" ~is_secure:false
2067
+
Alcotest.(check int) "identical path matches" 1 (List.length cookies)
2069
+
let test_path_matching_with_trailing_slash () =
2070
+
Eio_mock.Backend.run @@ fun () ->
2071
+
let clock = Eio_mock.Clock.make () in
2072
+
Eio_mock.Clock.set_time clock 1000.0;
2074
+
let jar = create () in
2076
+
Cookeio.make ~domain:"example.com" ~path:"/foo/" ~name:"test" ~value:"val"
2077
+
~secure:false ~http_only:false
2078
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2079
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2081
+
add_cookie jar cookie;
2083
+
(* Cookie path /foo/ should match /foo/bar *)
2085
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/foo/bar" ~is_secure:false
2087
+
Alcotest.(check int) "/foo/ matches /foo/bar" 1 (List.length cookies);
2089
+
(* Cookie path /foo/ should match /foo/ *)
2091
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/foo/" ~is_secure:false
2093
+
Alcotest.(check int) "/foo/ matches /foo/" 1 (List.length cookies2)
2095
+
let test_path_matching_prefix_with_slash () =
2096
+
Eio_mock.Backend.run @@ fun () ->
2097
+
let clock = Eio_mock.Clock.make () in
2098
+
Eio_mock.Clock.set_time clock 1000.0;
2100
+
let jar = create () in
2102
+
Cookeio.make ~domain:"example.com" ~path:"/foo" ~name:"test" ~value:"val"
2103
+
~secure:false ~http_only:false
2104
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2105
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2107
+
add_cookie jar cookie;
2109
+
(* Cookie path /foo should match /foo/bar (next char is /) *)
2111
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/foo/bar" ~is_secure:false
2113
+
Alcotest.(check int) "/foo matches /foo/bar" 1 (List.length cookies);
2115
+
(* Cookie path /foo should match /foo/ *)
2117
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/foo/" ~is_secure:false
2119
+
Alcotest.(check int) "/foo matches /foo/" 1 (List.length cookies2)
2121
+
let test_path_matching_no_false_prefix () =
2122
+
Eio_mock.Backend.run @@ fun () ->
2123
+
let clock = Eio_mock.Clock.make () in
2124
+
Eio_mock.Clock.set_time clock 1000.0;
2126
+
let jar = create () in
2128
+
Cookeio.make ~domain:"example.com" ~path:"/foo" ~name:"test" ~value:"val"
2129
+
~secure:false ~http_only:false
2130
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2131
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2133
+
add_cookie jar cookie;
2135
+
(* Cookie path /foo should NOT match /foobar (no / separator) *)
2137
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/foobar" ~is_secure:false
2139
+
Alcotest.(check int) "/foo does NOT match /foobar" 0 (List.length cookies);
2141
+
(* Cookie path /foo should NOT match /foob *)
2143
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/foob" ~is_secure:false
2145
+
Alcotest.(check int) "/foo does NOT match /foob" 0 (List.length cookies2)
2147
+
let test_path_matching_root () =
2148
+
Eio_mock.Backend.run @@ fun () ->
2149
+
let clock = Eio_mock.Clock.make () in
2150
+
Eio_mock.Clock.set_time clock 1000.0;
2152
+
let jar = create () in
2154
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"val"
2155
+
~secure:false ~http_only:false
2156
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2157
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2159
+
add_cookie jar cookie;
2161
+
(* Root path should match everything *)
2163
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
2165
+
Alcotest.(check int) "/ matches /" 1 (List.length cookies1);
2168
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/foo" ~is_secure:false
2170
+
Alcotest.(check int) "/ matches /foo" 1 (List.length cookies2);
2173
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/foo/bar/baz" ~is_secure:false
2175
+
Alcotest.(check int) "/ matches /foo/bar/baz" 1 (List.length cookies3)
2177
+
let test_path_matching_no_match () =
2178
+
Eio_mock.Backend.run @@ fun () ->
2179
+
let clock = Eio_mock.Clock.make () in
2180
+
Eio_mock.Clock.set_time clock 1000.0;
2182
+
let jar = create () in
2184
+
Cookeio.make ~domain:"example.com" ~path:"/foo/bar" ~name:"test" ~value:"val"
2185
+
~secure:false ~http_only:false
2186
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
2187
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
2189
+
add_cookie jar cookie;
2191
+
(* Cookie path /foo/bar should NOT match /foo *)
2193
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/foo" ~is_secure:false
2195
+
Alcotest.(check int) "/foo/bar does NOT match /foo" 0 (List.length cookies);
2197
+
(* Cookie path /foo/bar should NOT match / *)
2199
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
2201
+
Alcotest.(check int) "/foo/bar does NOT match /" 0 (List.length cookies2);
2203
+
(* Cookie path /foo/bar should NOT match /baz *)
2205
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/baz" ~is_secure:false
2207
+
Alcotest.(check int) "/foo/bar does NOT match /baz" 0 (List.length cookies3)
Eio_main.run @@ fun env ->
···
test_max_age_and_expires_both_present env);
test_case "parse both" `Quick (fun () ->
test_parse_max_age_and_expires env);
2343
+
( "host_only_flag",
2345
+
test_case "host_only without Domain attribute" `Quick
2346
+
test_host_only_without_domain_attribute;
2347
+
test_case "host_only with Domain attribute" `Quick
2348
+
test_host_only_with_domain_attribute;
2349
+
test_case "host_only with dotted Domain attribute" `Quick
2350
+
test_host_only_with_dotted_domain_attribute;
2351
+
test_case "host_only domain matching" `Quick
2352
+
test_host_only_domain_matching;
2353
+
test_case "host_only Cookie header parsing" `Quick
2354
+
test_host_only_cookie_header_parsing;
2355
+
test_case "host_only Mozilla format round trip" `Quick
2356
+
test_host_only_mozilla_format_round_trip;
2358
+
( "path_matching",
2360
+
test_case "identical path" `Quick test_path_matching_identical;
2361
+
test_case "path with trailing slash" `Quick
2362
+
test_path_matching_with_trailing_slash;
2363
+
test_case "prefix with slash separator" `Quick
2364
+
test_path_matching_prefix_with_slash;
2365
+
test_case "no false prefix match" `Quick
2366
+
test_path_matching_no_false_prefix;
2367
+
test_case "root path matches all" `Quick test_path_matching_root;
2368
+
test_case "path no match" `Quick test_path_matching_no_match;