pidge_client/auth/
tokens.rs1use chrono::{DateTime, Duration, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(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 pub fn needs_refresh(&self) -> bool {
18 Utc::now() + Duration::seconds(60) >= self.expires_at
19 }
20}
21
22impl std::fmt::Debug for TokenSet {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 f.debug_struct("TokenSet")
28 .field("access_token", &"<redacted>")
29 .field("refresh_token", &"<redacted>")
30 .field("expires_at", &self.expires_at)
31 .finish()
32 }
33}
34
35#[cfg(test)]
36mod tests {
37 use super::*;
38
39 #[test]
40 fn fresh_token_does_not_need_refresh() {
41 let t = TokenSet {
42 access_token: "a".into(),
43 refresh_token: "r".into(),
44 expires_at: Utc::now() + Duration::seconds(3600),
45 };
46 assert!(!t.needs_refresh());
47 }
48
49 #[test]
50 fn token_expiring_within_60s_needs_refresh() {
51 let t = TokenSet {
52 access_token: "a".into(),
53 refresh_token: "r".into(),
54 expires_at: Utc::now() + Duration::seconds(30),
55 };
56 assert!(t.needs_refresh());
57 }
58
59 #[test]
60 fn already_expired_token_needs_refresh() {
61 let t = TokenSet {
62 access_token: "a".into(),
63 refresh_token: "r".into(),
64 expires_at: Utc::now() - Duration::seconds(10),
65 };
66 assert!(t.needs_refresh());
67 }
68
69 #[test]
70 fn tokens_roundtrip_through_json() {
71 let t = TokenSet {
72 access_token: "ey…".into(),
73 refresh_token: "M.C5…".into(),
74 expires_at: DateTime::parse_from_rfc3339("2026-05-13T23:00:00Z")
75 .unwrap()
76 .to_utc(),
77 };
78 let json = serde_json::to_string(&t).unwrap();
79 let t2: TokenSet = serde_json::from_str(&json).unwrap();
80 assert_eq!(t, t2);
81 }
82
83 #[test]
84 fn debug_redacts_both_tokens() {
85 let t = TokenSet {
86 access_token: "super-secret-access".into(),
87 refresh_token: "super-secret-refresh".into(),
88 expires_at: Utc::now(),
89 };
90
91 let debug = format!("{t:?}");
92
93 assert!(!debug.contains("super-secret-access"));
94 assert!(!debug.contains("super-secret-refresh"));
95 assert!(debug.contains("<redacted>"));
96 }
97}