use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use crate::secrets::{Secret, SecretsError};
pub trait SecretsStore: Send + Sync {
fn register(&self, secret: Secret) -> Result<(), SecretsError>;
fn lookup(&self, name: &str) -> Option<String>;
fn list(&self) -> Vec<String>;
fn delete(&self, name: &str) -> Result<(), SecretsError>;
}
#[derive(Clone, Default)]
pub struct InMemorySecretsStore {
data: Arc<RwLock<HashMap<String, String>>>,
}
impl InMemorySecretsStore {
pub fn new() -> Self {
Self::default()
}
}
impl SecretsStore for InMemorySecretsStore {
fn register(&self, secret: Secret) -> Result<(), SecretsError> {
let mut data = self.data.write().expect("secrets store lock poisoned");
if data.contains_key(&secret.name) {
return Err(SecretsError::AlreadyRegistered { name: secret.name });
}
data.insert(secret.name, secret.value);
Ok(())
}
fn lookup(&self, name: &str) -> Option<String> {
let data = self.data.read().expect("secrets store lock poisoned");
data.get(name).cloned()
}
fn list(&self) -> Vec<String> {
let data = self.data.read().expect("secrets store lock poisoned");
let mut names: Vec<String> = data.keys().cloned().collect();
names.sort();
names
}
fn delete(&self, name: &str) -> Result<(), SecretsError> {
let mut data = self.data.write().expect("secrets store lock poisoned");
if data.remove(name).is_none() {
return Err(SecretsError::NotFound { name: name.to_owned() });
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn secret(name: &str, value: &str) -> Secret {
Secret {
name: name.to_owned(),
value: value.to_owned(),
}
}
#[test]
fn register_stores_a_new_secret() {
let store = InMemorySecretsStore::new();
let result = store.register(secret("DB_PASSWORD", "real-secret-1"));
assert!(result.is_ok());
assert_eq!(store.lookup("DB_PASSWORD").as_deref(), Some("real-secret-1"));
}
#[test]
fn lookup_returns_none_for_unknown_name() {
let store = InMemorySecretsStore::new();
store.register(secret("DB_PASSWORD", "real-secret-1")).unwrap();
assert_eq!(store.lookup("UNKNOWN"), None);
}
#[test]
fn delete_removes_a_registered_secret() {
let store = InMemorySecretsStore::new();
store.register(secret("DB_PASSWORD", "real-secret-1")).unwrap();
store.delete("DB_PASSWORD").unwrap();
assert_eq!(store.lookup("DB_PASSWORD"), None);
assert!(store.list().is_empty());
}
#[test]
fn list_returns_only_names_sorted_lexicographically() {
let store = InMemorySecretsStore::new();
store.register(secret("STRIPE_KEY", "real-1")).unwrap();
store.register(secret("DB_PASSWORD", "real-2")).unwrap();
store.register(secret("API_TOKEN", "real-3")).unwrap();
let names = store.list();
assert_eq!(names, vec!["API_TOKEN", "DB_PASSWORD", "STRIPE_KEY"]);
for value in ["real-1", "real-2", "real-3"] {
assert!(
!names.iter().any(|n| n.contains(value)),
"list() must never expose credential values; found {value:?}"
);
}
}
#[test]
fn register_duplicate_returns_already_registered() {
let store = InMemorySecretsStore::new();
store.register(secret("DB_PASSWORD", "real-secret-1")).unwrap();
let err = store
.register(secret("DB_PASSWORD", "real-secret-2"))
.expect_err("duplicate register must fail");
assert_eq!(
err,
SecretsError::AlreadyRegistered {
name: "DB_PASSWORD".to_owned()
}
);
assert_eq!(store.lookup("DB_PASSWORD").as_deref(), Some("real-secret-1"));
}
#[test]
fn delete_missing_returns_not_found() {
let store = InMemorySecretsStore::new();
let err = store.delete("UNKNOWN").expect_err("delete of missing name must fail");
assert_eq!(
err,
SecretsError::NotFound {
name: "UNKNOWN".to_owned()
}
);
}
}