Skip to main content

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