Skip to main content

pidge_client/auth/
tokens.rs

1//! Token storage shape — what gets serialized into the keychain.
2
3use chrono::{DateTime, Duration, Utc};
4use serde::{Deserialize, Serialize};
5
6/// A user's OAuth tokens for one account.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct TokenSet {
9    pub access_token: String,
10    pub refresh_token: String,
11    pub expires_at: DateTime<Utc>,
12}
13
14impl TokenSet {
15    /// True if the access token is within 60 seconds of expiring (or already expired).
16    /// We refresh before this threshold to absorb clock skew.
17    pub fn needs_refresh(&self) -> bool {
18        Utc::now() + Duration::seconds(60) >= self.expires_at
19    }
20}
21
22#[cfg(test)]
23mod tests {
24    use super::*;
25
26    #[test]
27    fn fresh_token_does_not_need_refresh() {
28        let t = TokenSet {
29            access_token: "a".into(),
30            refresh_token: "r".into(),
31            expires_at: Utc::now() + Duration::seconds(3600),
32        };
33        assert!(!t.needs_refresh());
34    }
35
36    #[test]
37    fn token_expiring_within_60s_needs_refresh() {
38        let t = TokenSet {
39            access_token: "a".into(),
40            refresh_token: "r".into(),
41            expires_at: Utc::now() + Duration::seconds(30),
42        };
43        assert!(t.needs_refresh());
44    }
45
46    #[test]
47    fn already_expired_token_needs_refresh() {
48        let t = TokenSet {
49            access_token: "a".into(),
50            refresh_token: "r".into(),
51            expires_at: Utc::now() - Duration::seconds(10),
52        };
53        assert!(t.needs_refresh());
54    }
55
56    #[test]
57    fn tokens_roundtrip_through_json() {
58        let t = TokenSet {
59            access_token: "ey…".into(),
60            refresh_token: "M.C5…".into(),
61            expires_at: DateTime::parse_from_rfc3339("2026-05-13T23:00:00Z")
62                .unwrap()
63                .to_utc(),
64        };
65        let json = serde_json::to_string(&t).unwrap();
66        let t2: TokenSet = serde_json::from_str(&json).unwrap();
67        assert_eq!(t, t2);
68    }
69}