garmin_cli/client/
tokens.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4/// OAuth1 token obtained after initial SSO authentication.
5/// Long-lived (~1 year), used to obtain OAuth2 tokens.
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub struct OAuth1Token {
8    pub oauth_token: String,
9    pub oauth_token_secret: String,
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub mfa_token: Option<String>,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub mfa_expiration_timestamp: Option<DateTime<Utc>>,
14    #[serde(default = "default_domain")]
15    pub domain: String,
16}
17
18fn default_domain() -> String {
19    "garmin.com".to_string()
20}
21
22impl OAuth1Token {
23    pub fn new(oauth_token: String, oauth_token_secret: String) -> Self {
24        Self {
25            oauth_token,
26            oauth_token_secret,
27            mfa_token: None,
28            mfa_expiration_timestamp: None,
29            domain: default_domain(),
30        }
31    }
32
33    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
34        self.domain = domain.into();
35        self
36    }
37
38    pub fn with_mfa(mut self, mfa_token: String, expiration: Option<DateTime<Utc>>) -> Self {
39        self.mfa_token = Some(mfa_token);
40        self.mfa_expiration_timestamp = expiration;
41        self
42    }
43}
44
45/// OAuth2 Bearer token for API requests.
46/// Short-lived, automatically refreshed using OAuth1 token.
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct OAuth2Token {
49    pub scope: String,
50    pub jti: String,
51    pub token_type: String,
52    pub access_token: String,
53    pub refresh_token: String,
54    pub expires_in: i64,
55    #[serde(default)]
56    pub expires_at: i64,
57    pub refresh_token_expires_in: i64,
58    #[serde(default)]
59    pub refresh_token_expires_at: i64,
60}
61
62impl OAuth2Token {
63    /// Check if the access token has expired.
64    pub fn is_expired(&self) -> bool {
65        let now = Utc::now().timestamp();
66        self.expires_at < now
67    }
68
69    /// Check if the refresh token has expired.
70    pub fn is_refresh_expired(&self) -> bool {
71        let now = Utc::now().timestamp();
72        self.refresh_token_expires_at < now
73    }
74
75    /// Returns the Authorization header value.
76    pub fn authorization_header(&self) -> String {
77        format!("{} {}", self.token_type, self.access_token)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_oauth1_token_new() {
87        let token = OAuth1Token::new(
88            "test_oauth_token".to_string(),
89            "test_oauth_secret".to_string(),
90        );
91
92        assert_eq!(token.oauth_token, "test_oauth_token");
93        assert_eq!(token.oauth_token_secret, "test_oauth_secret");
94        assert_eq!(token.domain, "garmin.com");
95        assert!(token.mfa_token.is_none());
96        assert!(token.mfa_expiration_timestamp.is_none());
97    }
98
99    #[test]
100    fn test_oauth1_token_with_domain() {
101        let token = OAuth1Token::new(
102            "test_oauth_token".to_string(),
103            "test_oauth_secret".to_string(),
104        )
105        .with_domain("garmin.cn");
106
107        assert_eq!(token.domain, "garmin.cn");
108    }
109
110    #[test]
111    fn test_oauth1_token_with_mfa() {
112        let expiration = Utc::now();
113        let token = OAuth1Token::new(
114            "test_oauth_token".to_string(),
115            "test_oauth_secret".to_string(),
116        )
117        .with_mfa("mfa_token_123".to_string(), Some(expiration));
118
119        assert_eq!(token.mfa_token, Some("mfa_token_123".to_string()));
120        assert_eq!(token.mfa_expiration_timestamp, Some(expiration));
121    }
122
123    #[test]
124    fn test_oauth1_token_serialization() {
125        let token = OAuth1Token::new(
126            "test_oauth_token".to_string(),
127            "test_oauth_secret".to_string(),
128        );
129
130        let json = serde_json::to_string(&token).unwrap();
131        let deserialized: OAuth1Token = serde_json::from_str(&json).unwrap();
132
133        assert_eq!(token, deserialized);
134    }
135
136    #[test]
137    fn test_oauth2_token_is_expired() {
138        let expired_token = OAuth2Token {
139            scope: "test".to_string(),
140            jti: "jti123".to_string(),
141            token_type: "Bearer".to_string(),
142            access_token: "access123".to_string(),
143            refresh_token: "refresh123".to_string(),
144            expires_in: 3600,
145            expires_at: 0, // Expired (epoch)
146            refresh_token_expires_in: 86400,
147            refresh_token_expires_at: Utc::now().timestamp() + 86400,
148        };
149
150        assert!(expired_token.is_expired());
151    }
152
153    #[test]
154    fn test_oauth2_token_not_expired() {
155        let valid_token = OAuth2Token {
156            scope: "test".to_string(),
157            jti: "jti123".to_string(),
158            token_type: "Bearer".to_string(),
159            access_token: "access123".to_string(),
160            refresh_token: "refresh123".to_string(),
161            expires_in: 3600,
162            expires_at: Utc::now().timestamp() + 3600, // 1 hour from now
163            refresh_token_expires_in: 86400,
164            refresh_token_expires_at: Utc::now().timestamp() + 86400,
165        };
166
167        assert!(!valid_token.is_expired());
168    }
169
170    #[test]
171    fn test_oauth2_token_refresh_expired() {
172        let token = OAuth2Token {
173            scope: "test".to_string(),
174            jti: "jti123".to_string(),
175            token_type: "Bearer".to_string(),
176            access_token: "access123".to_string(),
177            refresh_token: "refresh123".to_string(),
178            expires_in: 3600,
179            expires_at: Utc::now().timestamp() + 3600,
180            refresh_token_expires_in: 86400,
181            refresh_token_expires_at: 0, // Refresh token expired
182        };
183
184        assert!(token.is_refresh_expired());
185    }
186
187    #[test]
188    fn test_oauth2_token_authorization_header() {
189        let token = OAuth2Token {
190            scope: "test".to_string(),
191            jti: "jti123".to_string(),
192            token_type: "Bearer".to_string(),
193            access_token: "my_access_token".to_string(),
194            refresh_token: "refresh123".to_string(),
195            expires_in: 3600,
196            expires_at: Utc::now().timestamp() + 3600,
197            refresh_token_expires_in: 86400,
198            refresh_token_expires_at: Utc::now().timestamp() + 86400,
199        };
200
201        assert_eq!(token.authorization_header(), "Bearer my_access_token");
202    }
203
204    #[test]
205    fn test_oauth2_token_serialization() {
206        let token = OAuth2Token {
207            scope: "test".to_string(),
208            jti: "jti123".to_string(),
209            token_type: "Bearer".to_string(),
210            access_token: "access123".to_string(),
211            refresh_token: "refresh123".to_string(),
212            expires_in: 3600,
213            expires_at: 1700000000,
214            refresh_token_expires_in: 86400,
215            refresh_token_expires_at: 1700086400,
216        };
217
218        let json = serde_json::to_string(&token).unwrap();
219        let deserialized: OAuth2Token = serde_json::from_str(&json).unwrap();
220
221        assert_eq!(token, deserialized);
222    }
223}