···
(Ptime.of_float_s 1257894000.0)
(Cookeio.expires cookie2)
237
+
let test_cookie_expiry_with_mock_clock () =
238
+
Eio_mock.Backend.run @@ fun () ->
239
+
let clock = Eio_mock.Clock.make () in
241
+
(* Start at time 1000.0 for convenience *)
242
+
Eio_mock.Clock.set_time clock 1000.0;
244
+
let jar = create () in
246
+
(* Add a cookie that expires at time 1500.0 (expires in 500 seconds) *)
247
+
let expires_soon = Ptime.of_float_s 1500.0 |> Option.get in
249
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"expires_soon"
250
+
~value:"value1" ~secure:false ~http_only:false ~expires:expires_soon
252
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
253
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
257
+
(* Add a cookie that expires at time 2000.0 (expires in 1000 seconds) *)
258
+
let expires_later = Ptime.of_float_s 2000.0 |> Option.get in
260
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"expires_later"
261
+
~value:"value2" ~secure:false ~http_only:false ~expires:expires_later
263
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
264
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
268
+
(* Add a session cookie (no expiry) *)
270
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"session" ~value:"value3"
271
+
~secure:false ~http_only:false ?expires:None ?same_site:None
272
+
~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
273
+
~last_access:(Ptime.of_float_s 1000.0 |> Option.get)
277
+
add_cookie jar cookie1;
278
+
add_cookie jar cookie2;
279
+
add_cookie jar cookie3;
281
+
Alcotest.(check int) "initial count" 3 (count jar);
283
+
(* Advance time to 1600.0 - first cookie should expire *)
284
+
Eio_mock.Clock.set_time clock 1600.0;
285
+
clear_expired jar ~clock;
287
+
Alcotest.(check int) "after first expiry" 2 (count jar);
289
+
let cookies = get_all_cookies jar in
290
+
let names = List.map Cookeio.name cookies |> List.sort String.compare in
291
+
Alcotest.(check (list string))
292
+
"remaining cookies after 1600s" [ "expires_later"; "session" ] names;
294
+
(* Advance time to 2100.0 - second cookie should expire *)
295
+
Eio_mock.Clock.set_time clock 2100.0;
296
+
clear_expired jar ~clock;
298
+
Alcotest.(check int) "after second expiry" 1 (count jar);
300
+
let remaining = get_all_cookies jar in
301
+
Alcotest.(check string) "only session cookie remains" "session"
302
+
(Cookeio.name (List.hd remaining))
304
+
let test_max_age_parsing_with_mock_clock () =
305
+
Eio_mock.Backend.run @@ fun () ->
306
+
let clock = Eio_mock.Clock.make () in
308
+
(* Start at a known time *)
309
+
Eio_mock.Clock.set_time clock 5000.0;
311
+
(* Parse a Set-Cookie header with Max-Age *)
312
+
let header = "session=abc123; Max-Age=3600; Secure; HttpOnly" in
314
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
317
+
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
319
+
let cookie = Option.get cookie_opt in
320
+
Alcotest.(check string) "cookie name" "session" (Cookeio.name cookie);
321
+
Alcotest.(check string) "cookie value" "abc123" (Cookeio.value cookie);
322
+
Alcotest.(check bool) "cookie secure" true (Cookeio.secure cookie);
323
+
Alcotest.(check bool) "cookie http_only" true (Cookeio.http_only cookie);
325
+
(* Verify the expiry time is set correctly (5000.0 + 3600 = 8600.0) *)
326
+
let expected_expiry = Ptime.of_float_s 8600.0 in
327
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
328
+
"expires set from max-age" expected_expiry (Cookeio.expires cookie);
330
+
(* Verify creation time matches clock time *)
331
+
let expected_creation = Ptime.of_float_s 5000.0 in
332
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
333
+
"creation time" expected_creation
334
+
(Some (Cookeio.creation_time cookie))
336
+
let test_last_access_time_with_mock_clock () =
337
+
Eio_mock.Backend.run @@ fun () ->
338
+
let clock = Eio_mock.Clock.make () in
340
+
(* Start at time 3000.0 *)
341
+
Eio_mock.Clock.set_time clock 3000.0;
343
+
let jar = create () in
347
+
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"value"
348
+
~secure:false ~http_only:false ?expires:None ?same_site:None
349
+
~creation_time:(Ptime.of_float_s 3000.0 |> Option.get)
350
+
~last_access:(Ptime.of_float_s 3000.0 |> Option.get)
353
+
add_cookie jar cookie;
355
+
(* Verify initial last access time *)
356
+
let cookies1 = get_all_cookies jar in
357
+
let cookie1 = List.hd cookies1 in
358
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
359
+
"initial last access" (Ptime.of_float_s 3000.0)
360
+
(Some (Cookeio.last_access cookie1));
362
+
(* Advance time to 4000.0 *)
363
+
Eio_mock.Clock.set_time clock 4000.0;
365
+
(* Get cookies, which should update last access time to current clock time *)
367
+
get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
370
+
(* Verify last access time was updated to the new clock time *)
371
+
let cookies2 = get_all_cookies jar in
372
+
let cookie2 = List.hd cookies2 in
373
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
374
+
"updated last access" (Ptime.of_float_s 4000.0)
375
+
(Some (Cookeio.last_access cookie2))
377
+
let test_parse_set_cookie_with_expires () =
378
+
Eio_mock.Backend.run @@ fun () ->
379
+
let clock = Eio_mock.Clock.make () in
381
+
(* Start at a known time *)
382
+
Eio_mock.Clock.set_time clock 6000.0;
384
+
(* Use RFC3339 format which is what Ptime.of_rfc3339 expects *)
386
+
"id=xyz789; Expires=2025-10-21T07:28:00Z; Path=/; Domain=.example.com"
389
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" header
392
+
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
394
+
let cookie = Option.get cookie_opt in
395
+
Alcotest.(check string) "cookie name" "id" (Cookeio.name cookie);
396
+
Alcotest.(check string) "cookie value" "xyz789" (Cookeio.value cookie);
397
+
Alcotest.(check string) "cookie domain" ".example.com" (Cookeio.domain cookie);
398
+
Alcotest.(check string) "cookie path" "/" (Cookeio.path cookie);
400
+
(* Verify expires is parsed correctly *)
401
+
Alcotest.(check bool) "has expiry" true
402
+
(Option.is_some (Cookeio.expires cookie));
404
+
(* Verify the specific expiry time parsed from the RFC3339 date *)
405
+
let expected_expiry = Ptime.of_rfc3339 "2025-10-21T07:28:00Z" in
406
+
match expected_expiry with
407
+
| Ok (time, _, _) ->
408
+
Alcotest.(check (option (Alcotest.testable Ptime.pp Ptime.equal)))
409
+
"expires matches parsed value" (Some time) (Cookeio.expires cookie)
410
+
| Error _ -> Alcotest.fail "Failed to parse expected expiry time"
412
+
let test_samesite_none_validation () =
413
+
Eio_mock.Backend.run @@ fun () ->
414
+
let clock = Eio_mock.Clock.make () in
416
+
(* Start at a known time *)
417
+
Eio_mock.Clock.set_time clock 7000.0;
419
+
(* This should be rejected: SameSite=None without Secure *)
420
+
let invalid_header = "token=abc; SameSite=None" in
422
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" invalid_header
425
+
Alcotest.(check bool) "invalid cookie rejected" true (Option.is_none cookie_opt);
427
+
(* This should be accepted: SameSite=None with Secure *)
428
+
let valid_header = "token=abc; SameSite=None; Secure" in
430
+
parse_set_cookie ~clock ~domain:"example.com" ~path:"/" valid_header
433
+
Alcotest.(check bool) "valid cookie accepted" true (Option.is_some cookie_opt2);
435
+
let cookie = Option.get cookie_opt2 in
436
+
Alcotest.(check bool) "cookie is secure" true (Cookeio.secure cookie);
441
+
(fun ppf -> function
442
+
| `Strict -> Format.pp_print_string ppf "Strict"
443
+
| `Lax -> Format.pp_print_string ppf "Lax"
444
+
| `None -> Format.pp_print_string ppf "None")
446
+
"samesite is None" (Some `None) (Cookeio.same_site cookie)
Eio_main.run @@ fun env ->
···
[ test_case "Empty jar operations" `Quick (fun () -> test_empty_jar env) ]
472
+
test_case "Cookie expiry with mock clock" `Quick
473
+
test_cookie_expiry_with_mock_clock;
474
+
test_case "Max-Age parsing with mock clock" `Quick
475
+
test_max_age_parsing_with_mock_clock;
476
+
test_case "Last access time with mock clock" `Quick
477
+
test_last_access_time_with_mock_clock;
478
+
test_case "Parse Set-Cookie with Expires" `Quick
479
+
test_parse_set_cookie_with_expires;
480
+
test_case "SameSite=None validation" `Quick
481
+
test_samesite_none_validation;