use thiserror::Error;
use tracing::{debug, warn};
use super::config::TokenStorageBackend;
use super::keyring::{KeyringError, KeyringStore};
use super::token_store::{TokenStore, TokenStoreError};
use crate::oauth::token::Token;
#[derive(Debug, Error)]
pub enum UnifiedTokenError {
#[error("File storage error: {0}")]
File(#[from] TokenStoreError),
#[error("Keyring error: {0}")]
Keyring(#[from] KeyringError),
#[error("Token not found")]
NotFound,
}
pub struct UnifiedTokenStore {
backend: TokenStorageBackend,
keyring: Option<KeyringStore>,
file: TokenStore,
}
impl UnifiedTokenStore {
pub fn new(preferred_backend: TokenStorageBackend) -> Result<Self, UnifiedTokenError> {
let file = TokenStore::new()?;
let (backend, keyring) = match preferred_backend {
TokenStorageBackend::Keyring => match KeyringStore::new() {
Ok(store) => {
debug!("Using keyring for token storage");
(TokenStorageBackend::Keyring, Some(store))
}
Err(e) => {
warn!("Keyring unavailable, falling back to file storage: {}", e);
(TokenStorageBackend::File, None)
}
},
TokenStorageBackend::File => {
debug!("Using file for token storage (configured)");
(TokenStorageBackend::File, None)
}
};
Ok(Self {
backend,
keyring,
file,
})
}
pub fn save(&self, token: &Token) -> Result<(), UnifiedTokenError> {
match self.backend {
TokenStorageBackend::Keyring => {
if let Some(ref keyring) = self.keyring {
keyring.save(token)?;
debug!("Token saved to keyring");
Ok(())
} else {
self.file.save(token)?;
debug!("Token saved to file (keyring fallback)");
Ok(())
}
}
TokenStorageBackend::File => {
self.file.save(token)?;
debug!("Token saved to file");
Ok(())
}
}
}
pub fn load(&self) -> Result<Token, UnifiedTokenError> {
match self.backend {
TokenStorageBackend::Keyring => {
if let Some(ref keyring) = self.keyring {
match keyring.load() {
Ok(token) => {
debug!("Token loaded from keyring");
Ok(token)
}
Err(KeyringError::NotFound) => {
debug!("Token not in keyring, checking file for migration");
match self.file.load() {
Ok(token) => {
debug!("Found token in file, migrating to keyring");
if keyring.save(&token).is_ok() {
let _ = self.file.delete();
debug!("Token migrated from file to keyring");
}
Ok(token)
}
Err(TokenStoreError::NotFound) => Err(UnifiedTokenError::NotFound),
Err(e) => Err(UnifiedTokenError::File(e)),
}
}
Err(e) => Err(UnifiedTokenError::Keyring(e)),
}
} else {
self.file.load().map_err(|e| match e {
TokenStoreError::NotFound => UnifiedTokenError::NotFound,
other => UnifiedTokenError::File(other),
})
}
}
TokenStorageBackend::File => self.file.load().map_err(|e| match e {
TokenStoreError::NotFound => UnifiedTokenError::NotFound,
other => UnifiedTokenError::File(other),
}),
}
}
pub fn delete(&self) -> Result<(), UnifiedTokenError> {
if let Some(ref keyring) = self.keyring {
keyring.delete()?;
debug!("Token deleted from keyring");
}
self.file.delete()?;
debug!("Token deleted from file");
Ok(())
}
pub fn exists(&self) -> bool {
match self.backend {
TokenStorageBackend::Keyring => {
if let Some(ref keyring) = self.keyring {
keyring.exists() || self.file.exists()
} else {
self.file.exists()
}
}
TokenStorageBackend::File => self.file.exists(),
}
}
pub fn backend(&self) -> TokenStorageBackend {
self.backend
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unified_token_error_display() {
let err = UnifiedTokenError::NotFound;
let display = format!("{}", err);
assert!(display.contains("not found"));
}
#[test]
fn file_backend_creates_successfully() {
let store = UnifiedTokenStore::new(TokenStorageBackend::File);
assert!(store.is_ok());
assert_eq!(store.unwrap().backend(), TokenStorageBackend::File);
}
#[test]
fn exists_returns_false_when_empty() {
let store = UnifiedTokenStore::new(TokenStorageBackend::File).unwrap();
let _ = store.exists();
}
}