use std::time::Duration;
use ferrohealth_ui::view::session::Left;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Lifetimes {
pub idle: Duration,
pub absolute: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Times {
pub(crate) signed_in: i64,
pub(crate) touched: i64,
pub(crate) last_token: Option<i64>,
}
impl Lifetimes {
pub(crate) fn left(&self, times: Times, now: i64) -> Option<Left> {
let mut absolute_end = times.signed_in.saturating_add(millis(self.absolute));
if let Some(expires) = times.last_token {
absolute_end = absolute_end.min(expires);
}
let idle_end = times
.touched
.saturating_add(millis(self.idle))
.min(absolute_end);
let idle = idle_end.saturating_sub(now);
(idle > 0).then(|| Left {
idle: whole(idle),
absolute: whole(absolute_end.saturating_sub(now)),
})
}
}
const MILLIS_PER_SECOND: i64 = 1000;
pub(crate) fn seconds(span: Duration) -> i64 {
i64::try_from(span.as_secs()).unwrap_or(i64::MAX)
}
pub(crate) fn millis(span: Duration) -> i64 {
i64::try_from(span.as_millis()).unwrap_or(i64::MAX)
}
pub(crate) const fn seconds_down(millis: i64) -> i64 {
millis.div_euclid(MILLIS_PER_SECOND)
}
pub(crate) const fn seconds_up(millis: i64) -> i64 {
let whole = millis.div_euclid(MILLIS_PER_SECOND);
if millis.rem_euclid(MILLIS_PER_SECOND) == 0 {
whole
} else {
whole.saturating_add(1)
}
}
pub(crate) const fn from_seconds(seconds: i64) -> i64 {
seconds.saturating_mul(MILLIS_PER_SECOND)
}
fn whole(span: i64) -> u32 {
u32::try_from(seconds_up(span.max(0))).unwrap_or(u32::MAX)
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::{Lifetimes, Times, seconds_down, seconds_up};
const LIFETIMES: Lifetimes = Lifetimes {
idle: Duration::from_secs(60),
absolute: Duration::from_secs(600),
};
const fn s(seconds: i64) -> i64 {
seconds * 1000
}
fn times(signed_in: i64, touched: i64) -> Times {
Times {
signed_in: s(signed_in),
touched: s(touched),
last_token: None,
}
}
#[test]
fn a_fresh_session_has_both_lifetimes_left() {
let left = LIFETIMES.left(times(1000, 1000), s(1000)).expect("live");
assert_eq!((60, 600), (left.idle, left.absolute), "nothing has passed");
}
#[test]
fn the_idle_lifetime_counts_from_the_last_activity() {
let left = LIFETIMES.left(times(1000, 1100), s(1130)).expect("live");
assert_eq!((30, 470), (left.idle, left.absolute), "{left:?}");
assert!(
LIFETIMES.left(times(1000, 1100), s(1160)).is_none(),
"sixty idle seconds end the session"
);
}
#[test]
fn the_absolute_lifetime_counts_from_the_sign_in_whatever_the_activity() {
let left = LIFETIMES.left(times(1000, 1590), s(1590)).expect("live");
assert_eq!(
(10, 10),
(left.idle, left.absolute),
"the idle lifetime never runs past the absolute end"
);
assert!(
LIFETIMES.left(times(1000, 1599), s(1600)).is_none(),
"the absolute lifetime ends the session"
);
}
#[test]
fn an_access_token_no_refresh_token_replaces_ends_the_session() {
let bounded = Times {
last_token: Some(s(1040)),
..times(1000, 1000)
};
let left = LIFETIMES.left(bounded, s(1010)).expect("live");
assert_eq!((30, 30), (left.idle, left.absolute), "{left:?}");
assert!(
LIFETIMES.left(bounded, s(1040)).is_none(),
"the token expired"
);
}
#[test]
fn a_zero_idle_lifetime_admits_no_session() {
let none = Lifetimes {
idle: Duration::ZERO,
..LIFETIMES
};
assert!(none.left(times(1000, 1000), s(1000)).is_none(), "no window");
}
#[test]
fn what_is_left_is_rounded_up_to_a_whole_second() {
let left = LIFETIMES
.left(times(1000, 1000), s(1059) + 1)
.expect("live");
assert_eq!((1, 541), (left.idle, left.absolute), "{left:?}");
}
#[test]
fn a_time_rounds_to_whole_seconds_either_way() {
assert_eq!((1000, 1000), (seconds_down(s(1000)), seconds_up(s(1000))));
assert_eq!(
(1000, 1001),
(seconds_down(s(1000) + 1), seconds_up(s(1000) + 1))
);
assert_eq!((-1, 0), (seconds_down(-1), seconds_up(-1)));
}
}