Skip to main content

session_guard/
clock.rs

1//! Time source used for every expiry, rotation and cookie `Max-Age` decision.
2
3use std::sync::{Arc, Mutex, PoisonError};
4
5use chrono::{DateTime, Utc};
6
7/// A source of the current time.
8///
9/// The guard never calls [`Utc::now`] directly, so tests (and simulations) can drive time with
10/// [`ManualClock`]. Any `Fn() -> DateTime<Utc>` closure is also a clock.
11pub trait Clock: Send + Sync + 'static {
12    /// The current instant.
13    fn now(&self) -> DateTime<Utc>;
14}
15
16/// The system wall clock.
17#[derive(Clone, Copy, Debug, Default)]
18pub struct SystemClock;
19
20impl Clock for SystemClock {
21    fn now(&self) -> DateTime<Utc> {
22        Utc::now()
23    }
24}
25
26impl<F> Clock for F
27where
28    F: Fn() -> DateTime<Utc> + Send + Sync + 'static,
29{
30    fn now(&self) -> DateTime<Utc> {
31        self()
32    }
33}
34
35/// A clock that only moves when told to. Clones share the same time.
36#[derive(Clone, Debug)]
37pub struct ManualClock {
38    now: Arc<Mutex<DateTime<Utc>>>,
39}
40
41impl ManualClock {
42    /// A clock frozen at `start`.
43    pub fn new(start: DateTime<Utc>) -> Self {
44        Self {
45            now: Arc::new(Mutex::new(start)),
46        }
47    }
48
49    /// Jumps to `instant` (may move backwards).
50    pub fn set(&self, instant: DateTime<Utc>) {
51        *self.now.lock().unwrap_or_else(PoisonError::into_inner) = instant;
52    }
53
54    /// Moves forward by `by`. Saturates instead of overflowing.
55    pub fn advance(&self, by: std::time::Duration) {
56        let mut now = self.now.lock().unwrap_or_else(PoisonError::into_inner);
57        let delta = chrono::TimeDelta::from_std(by).unwrap_or(chrono::TimeDelta::MAX);
58        *now = now.checked_add_signed(delta).unwrap_or(DateTime::<Utc>::MAX_UTC);
59    }
60}
61
62impl Default for ManualClock {
63    fn default() -> Self {
64        Self::new(Utc::now())
65    }
66}
67
68impl Clock for ManualClock {
69    fn now(&self) -> DateTime<Utc> {
70        *self.now.lock().unwrap_or_else(PoisonError::into_inner)
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use std::time::Duration;
77
78    use super::*;
79
80    #[test]
81    fn manual_clock_advances_and_is_shared_between_clones() {
82        let start = DateTime::from_timestamp(1_700_000_000, 0).unwrap();
83        let clock = ManualClock::new(start);
84        let other = clock.clone();
85        clock.advance(Duration::from_secs(90));
86        assert_eq!(other.now(), start + chrono::TimeDelta::seconds(90));
87        other.set(start);
88        assert_eq!(clock.now(), start);
89    }
90
91    #[test]
92    fn manual_clock_saturates() {
93        let clock = ManualClock::new(DateTime::<Utc>::MAX_UTC - chrono::TimeDelta::seconds(1));
94        clock.advance(Duration::from_secs(u64::MAX));
95        assert_eq!(clock.now(), DateTime::<Utc>::MAX_UTC);
96    }
97
98    #[test]
99    fn closures_are_clocks() {
100        let fixed = DateTime::from_timestamp(42, 0).unwrap();
101        let clock = move || fixed;
102        assert_eq!(Clock::now(&clock), fixed);
103    }
104}