use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use tracing::debug;
use crate::core::error::Result;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenStorage {
#[serde(default)]
pub access_token: String,
#[serde(default)]
pub refresh_token: Option<String>,
#[serde(default)]
pub expires_at: u64, #[serde(default = "default_token_type")]
pub token_type: String,
#[serde(default)]
pub scope: String,
}
fn default_token_type() -> String {
"Bearer".to_string()
}
impl TokenStorage {
pub fn is_expired(&self) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
now + 60 >= self.expires_at
}
pub fn remaining_secs(&self) -> u64 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.expires_at.saturating_sub(now)
}
}
impl super::GoogleAuth {
pub(crate) async fn save_token(&self, storage: &TokenStorage) -> Result<()> {
self.store.save(storage).await?;
debug!("Token persisted via {} store", self.store.backend());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minimal_old_shape_token_json_still_deserializes()
-> std::result::Result<(), Box<dyn std::error::Error>> {
let old = r#"{"access_token":"old-token","expires_at":4102444800}"#;
let storage: TokenStorage = serde_json::from_str(old)?;
assert_eq!(storage.access_token, "old-token");
assert_eq!(storage.expires_at, 4_102_444_800);
assert_eq!(storage.refresh_token, None);
assert_eq!(storage.token_type, "Bearer");
assert_eq!(storage.scope, "");
Ok(())
}
#[test]
fn token_json_with_unknown_future_fields_is_ignored()
-> std::result::Result<(), Box<dyn std::error::Error>> {
let newer = r#"{"access_token":"t","expires_at":1,"token_type":"Bearer","scope":"s","future_field":42}"#;
let storage: TokenStorage = serde_json::from_str(newer)?;
assert_eq!(storage.access_token, "t");
Ok(())
}
#[test]
fn current_shape_round_trips() -> std::result::Result<(), Box<dyn std::error::Error>> {
let storage = TokenStorage {
access_token: "a".into(),
refresh_token: Some("r".into()),
expires_at: 5,
token_type: "Bearer".into(),
scope: "s".into(),
};
let encoded = serde_json::to_string(&storage)?;
let decoded: TokenStorage = serde_json::from_str(&encoded)?;
assert_eq!(decoded.access_token, "a");
assert_eq!(decoded.refresh_token.as_deref(), Some("r"));
assert_eq!(decoded.expires_at, 5);
assert_eq!(decoded.token_type, "Bearer");
assert_eq!(decoded.scope, "s");
Ok(())
}
}