use anyhow::Result;
use async_trait::async_trait;
use std::collections::HashMap;
use tokio::sync::RwLock;
#[async_trait]
pub trait SecretStoreProvider: Send + Sync {
async fn get_secret(&self, name: &str) -> Result<String>;
}
pub struct MemorySecretStoreProvider {
secrets: RwLock<HashMap<String, String>>,
}
impl MemorySecretStoreProvider {
pub fn new() -> Self {
Self {
secrets: RwLock::new(HashMap::new()),
}
}
#[must_use]
pub fn with_secret(self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.secrets
.try_write()
.expect("MemorySecretStoreProvider should not be shared during construction")
.insert(name.into(), value.into());
self
}
pub async fn set_secret(&self, name: impl Into<String>, value: impl Into<String>) {
self.secrets.write().await.insert(name.into(), value.into());
}
pub async fn remove_secret(&self, name: &str) -> Option<String> {
self.secrets.write().await.remove(name)
}
}
impl Default for MemorySecretStoreProvider {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl SecretStoreProvider for MemorySecretStoreProvider {
async fn get_secret(&self, name: &str) -> Result<String> {
self.secrets
.read()
.await
.get(name)
.cloned()
.ok_or_else(|| anyhow::anyhow!("Secret not found: {name}"))
}
}
impl std::fmt::Debug for MemorySecretStoreProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemorySecretStoreProvider")
.field("secrets", &"[REDACTED]")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_memory_store_get_secret() {
let store = MemorySecretStoreProvider::new()
.with_secret("DB_PASSWORD", "hunter2")
.with_secret("API_KEY", "abc123");
assert_eq!(store.get_secret("DB_PASSWORD").await.unwrap(), "hunter2");
assert_eq!(store.get_secret("API_KEY").await.unwrap(), "abc123");
}
#[tokio::test]
async fn test_memory_store_missing_secret() {
let store = MemorySecretStoreProvider::new();
let result = store.get_secret("NONEXISTENT").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Secret not found"));
}
#[tokio::test]
async fn test_memory_store_set_and_remove() {
let store = MemorySecretStoreProvider::new();
store.set_secret("KEY", "value").await;
assert_eq!(store.get_secret("KEY").await.unwrap(), "value");
let removed = store.remove_secret("KEY").await;
assert_eq!(removed, Some("value".to_string()));
assert!(store.get_secret("KEY").await.is_err());
}
#[test]
fn test_memory_store_debug_redacts() {
let store = MemorySecretStoreProvider::new().with_secret("KEY", "secret_value");
let debug = format!("{store:?}");
assert!(debug.contains("[REDACTED]"));
assert!(!debug.contains("secret_value"));
}
#[test]
fn test_memory_store_default() {
let store = MemorySecretStoreProvider::default();
let rt = tokio::runtime::Runtime::new().unwrap();
assert!(rt.block_on(store.get_secret("anything")).is_err());
}
}