use anyhow::Result;
use async_trait::async_trait;
use keyring::Entry;
#[cfg(test)]
use mockall::automock;
#[async_trait]
#[cfg_attr(test, automock)]
pub trait SecretStore: Send + Sync {
async fn set_secret(&self, key: &str, value: &str) -> Result<()>;
async fn get_secret(&self, key: &str) -> Result<Option<String>>;
async fn delete_secret(&self, key: &str) -> Result<()>;
}
pub struct KeyringStore {
service_name: String,
}
impl KeyringStore {
pub fn new(service_name: &str) -> Self {
Self {
service_name: service_name.to_string(),
}
}
}
#[async_trait]
impl SecretStore for KeyringStore {
async fn set_secret(&self, key: &str, value: &str) -> Result<()> {
let entry = Entry::new(&self.service_name, key)
.map_err(|e| anyhow::anyhow!("Failed to create keyring entry: {}", e))?;
entry
.set_password(value)
.map_err(|e| anyhow::anyhow!("Failed to store secret: {}", e))?;
Ok(())
}
async fn get_secret(&self, key: &str) -> Result<Option<String>> {
let entry = Entry::new(&self.service_name, key)
.map_err(|e| anyhow::anyhow!("Failed to create keyring entry: {}", e))?;
match entry.get_password() {
Ok(p) => Ok(Some(p)),
Err(keyring::Error::NoEntry) => Ok(None),
Err(e) => Err(anyhow::anyhow!("Failed to retrieve secret: {}", e)),
}
}
async fn delete_secret(&self, key: &str) -> Result<()> {
let entry = Entry::new(&self.service_name, key)
.map_err(|e| anyhow::anyhow!("Failed to create keyring entry: {}", e))?;
match entry.delete_password() {
Ok(_) | Err(keyring::Error::NoEntry) => Ok(()),
Err(e) => Err(anyhow::anyhow!("Failed to delete secret: {}", e)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_keyring_store_basic_ops() {
let store = KeyringStore::new("magi-rs-test-suite");
let key = "test_key_123";
let value = "super_secret_value_abc";
let _ = store.delete_secret(key).await;
store.set_secret(key, value).await.unwrap();
let retrieved = store.get_secret(key).await.unwrap();
assert_eq!(retrieved, Some(value.to_string()));
store.delete_secret(key).await.unwrap();
let deleted = store.get_secret(key).await.unwrap();
assert!(deleted.is_none());
}
}