nixfleet_agent/
freshness.rs1use chrono::{DateTime, Utc};
23
24pub const CLOCK_SKEW_SLACK_SECS: i64 = 60;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum FreshnessCheck {
31 Fresh,
32 Stale {
36 signed_at: DateTime<Utc>,
37 freshness_window_secs: u32,
38 age_secs: i64,
39 },
40}
41
42pub fn check_signed_at(
50 signed_at: DateTime<Utc>,
51 freshness_window_secs: u32,
52 now: DateTime<Utc>,
53) -> FreshnessCheck {
54 let age_secs = (now - signed_at).num_seconds();
55 let limit = freshness_window_secs as i64 + CLOCK_SKEW_SLACK_SECS;
56
57 if age_secs > limit {
58 FreshnessCheck::Stale {
59 signed_at,
60 freshness_window_secs,
61 age_secs,
62 }
63 } else {
64 FreshnessCheck::Fresh
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use chrono::TimeZone;
72
73 fn t0() -> DateTime<Utc> {
74 Utc.with_ymd_and_hms(2026, 1, 1, 12, 0, 0).unwrap()
75 }
76
77 #[test]
78 fn fresh_when_age_well_under_window() {
79 let signed = t0();
80 let now = signed + chrono::Duration::seconds(100);
81 assert_eq!(check_signed_at(signed, 3600, now), FreshnessCheck::Fresh);
82 }
83
84 #[test]
85 fn fresh_at_exact_window_boundary() {
86 let signed = t0();
87 let now = signed + chrono::Duration::seconds(3600);
88 assert_eq!(check_signed_at(signed, 3600, now), FreshnessCheck::Fresh);
89 }
90
91 #[test]
92 fn fresh_within_slack_past_window() {
93 let signed = t0();
94 let now = signed + chrono::Duration::seconds(3660);
95 assert_eq!(check_signed_at(signed, 3600, now), FreshnessCheck::Fresh);
96 }
97
98 #[test]
99 fn stale_just_past_slack() {
100 let signed = t0();
101 let now = signed + chrono::Duration::seconds(3661);
102 let result = check_signed_at(signed, 3600, now);
103 assert!(
104 matches!(result, FreshnessCheck::Stale { age_secs: 3661, .. }),
105 "expected Stale at 3661s past signed_at; got {result:?}",
106 );
107 }
108
109 #[test]
110 fn stale_far_past_window() {
111 let signed = t0();
112 let now = signed + chrono::Duration::seconds(86_400 * 3);
113 let result = check_signed_at(signed, 3600, now);
114 match result {
115 FreshnessCheck::Stale {
116 age_secs,
117 freshness_window_secs,
118 ..
119 } => {
120 assert_eq!(freshness_window_secs, 3600);
121 assert_eq!(age_secs, 86_400 * 3);
122 }
123 other => panic!("expected Stale, got {other:?}"),
124 }
125 }
126
127 #[test]
128 fn fresh_when_clock_skew_slightly_negative() {
129 let signed = t0();
130 let now = signed - chrono::Duration::seconds(30);
131 assert_eq!(check_signed_at(signed, 3600, now), FreshnessCheck::Fresh);
132 }
133
134 #[test]
135 fn fresh_with_zero_window_inside_slack() {
136 let signed = t0();
140 let now = signed + chrono::Duration::seconds(30);
141 assert_eq!(check_signed_at(signed, 0, now), FreshnessCheck::Fresh);
142 }
143
144 #[test]
145 fn stale_with_zero_window_past_slack() {
146 let signed = t0();
147 let now = signed + chrono::Duration::seconds(61);
148 let result = check_signed_at(signed, 0, now);
149 assert!(matches!(result, FreshnessCheck::Stale { .. }));
150 }
151}