use std::collections::HashMap;
use std::sync::Mutex;
use super::{KeyStore, KeyStoreError};
#[derive(Default)]
pub struct MemoryKeyStore {
inner: Mutex<HashMap<String, String>>,
}
impl MemoryKeyStore {
pub fn new() -> Self {
Self::default()
}
}
impl std::fmt::Debug for MemoryKeyStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let names: Vec<String> = self
.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.keys()
.cloned()
.collect();
f.debug_struct("MemoryKeyStore")
.field("providers", &names)
.finish()
}
}
impl KeyStore for MemoryKeyStore {
fn get(&self, provider: &str) -> Option<String> {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.get(provider)
.cloned()
}
fn set(&self, provider: &str, value: &str) -> Result<(), KeyStoreError> {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.insert(provider.to_string(), value.to_string());
Ok(())
}
fn unset(&self, provider: &str) -> Result<(), KeyStoreError> {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.remove(provider);
Ok(())
}
fn list(&self) -> Vec<String> {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.keys()
.cloned()
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_then_get_round_trips() {
let store = MemoryKeyStore::new();
assert_eq!(store.get("fireworks"), None);
store.set("fireworks", "fw-secret").unwrap();
assert_eq!(store.get("fireworks"), Some("fw-secret".to_string()));
}
#[test]
fn unset_removes_and_is_idempotent() {
let store = MemoryKeyStore::new();
store.set("openai", "sk-abc").unwrap();
store.unset("openai").unwrap();
assert_eq!(store.get("openai"), None);
store.unset("openai").unwrap();
}
#[test]
fn list_returns_names_only() {
let store = MemoryKeyStore::new();
store.set("anthropic", "sk-ant-secret").unwrap();
store.set("openrouter", "or-secret").unwrap();
let mut names = store.list();
names.sort();
assert_eq!(
names,
vec!["anthropic".to_string(), "openrouter".to_string()]
);
assert!(!names.contains(&"sk-ant-secret".to_string()));
assert!(!names.contains(&"or-secret".to_string()));
}
#[test]
fn debug_output_never_contains_values() {
let store = MemoryKeyStore::new();
store.set("fireworks", "fw-super-secret-value").unwrap();
let dbg = format!("{store:?}");
assert!(
!dbg.contains("fw-super-secret-value"),
"Debug output leaked a credential value: {dbg}"
);
assert!(
dbg.contains("fireworks"),
"names should remain visible: {dbg}"
);
}
}