#![cfg(feature = "testing")]
use async_trait::async_trait;
use locket::{
path::{AbsolutePath, CanonicalPath, PathMapping},
provider::{ProviderError, ReferenceParser, SecretReference, SecretsProvider},
secrets::{InjectFailurePolicy, SecretError, SecretFileManager, SecretManagerConfig},
};
use secrecy::SecretString;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use tempfile::tempdir;
#[derive(Debug, Clone, Default)]
struct MockProvider {
data: HashMap<String, SecretString>,
}
impl MockProvider {
fn new(data: Vec<(&str, &str)>) -> Self {
let mut map = HashMap::new();
for (k, v) in data {
map.insert(k.to_string(), SecretString::new(v.into()));
}
Self { data: map }
}
}
impl ReferenceParser for MockProvider {
fn parse(&self, raw: &str) -> Option<SecretReference> {
if raw.starts_with("test:") {
Some(SecretReference::Mock(raw.to_string()))
} else {
None
}
}
}
#[async_trait]
impl SecretsProvider for MockProvider {
async fn fetch_map(
&self,
references: &[SecretReference],
) -> Result<HashMap<SecretReference, SecretString>, ProviderError> {
let mut result = HashMap::new();
for ref_obj in references {
if let SecretReference::Mock(key) = ref_obj {
if let Some(val) = self.data.get(key) {
result.insert(ref_obj.clone(), val.clone());
} else {
return Err(ProviderError::NotFound(key.clone()));
}
}
}
Ok(result)
}
}
fn setup(
tpl_name: &str,
tpl_content: &str,
) -> (tempfile::TempDir, std::path::PathBuf, SecretManagerConfig) {
let tmp = tempdir().unwrap();
let tpl_dir = tmp.path().join("templates");
let out_dir = tmp.path().join("secrets");
std::fs::create_dir_all(&tpl_dir).unwrap();
std::fs::create_dir_all(&out_dir).unwrap();
std::fs::write(tpl_dir.join(tpl_name), tpl_content).unwrap();
let config = SecretManagerConfig {
map: vec![make_mapping(&tpl_dir, &out_dir)],
out: AbsolutePath::new(&out_dir),
..Default::default()
};
(tmp, out_dir, config)
}
#[tokio::test]
async fn test_happy_path_template_rendering() {
let (_tmp, out_dir, config) = setup(
"config.yaml",
"user: {{ test:user }}\npass: {{ test:pass }}",
);
let provider = Arc::new(MockProvider::new(vec![
("test:user", "admin"),
("test:pass", "secret123"),
]));
let manager = SecretFileManager::new(config, provider).unwrap();
manager.inject_all().await.unwrap();
let result = std::fs::read_to_string(out_dir.join("config.yaml")).unwrap();
assert_eq!(result, "user: admin\npass: secret123");
}
#[tokio::test]
async fn test_whole_file_replacement() {
let (_tmp, out_dir, config) = setup("id_rsa", "test:ssh/key");
let key_content = "-----BEGIN RSA PRIVATE KEY-----...";
let provider = Arc::new(MockProvider::new(vec![("test:ssh/key", key_content)]));
let manager = SecretFileManager::new(config, provider).unwrap();
manager.inject_all().await.unwrap();
let result = std::fs::read_to_string(out_dir.join("id_rsa")).unwrap();
assert_eq!(result, key_content);
}
#[tokio::test]
async fn test_policy_error_aborts() {
let (_tmp, _out, mut config) = setup("config.yaml", "Key: {{ test:missing }}");
config.inject_failure_policy = InjectFailurePolicy::Error;
let provider = Arc::new(MockProvider::new(vec![]));
let manager = SecretFileManager::new(config, provider).unwrap();
let result = manager.inject_all().await;
assert!(result.is_err());
match result.unwrap_err() {
SecretError::Provider(ProviderError::NotFound(k)) => {
assert_eq!(k, "test:missing")
}
e => panic!("Unexpected error type: {:?}", e),
}
}
#[tokio::test]
async fn test_policy_copy_unmodified() {
let (_tmp, out_dir, mut config) = setup("config.yaml", "Key: {{ test:missing }}");
config.inject_failure_policy = InjectFailurePolicy::Passthrough;
let provider = Arc::new(MockProvider::new(vec![]));
let manager = SecretFileManager::new(config, provider).unwrap();
manager.inject_all().await.unwrap();
let result = std::fs::read_to_string(out_dir.join("config.yaml")).unwrap();
assert_eq!(result, "Key: {{ test:missing }}");
}
#[tokio::test]
async fn test_ignore_unknown_providers() {
let content = "A: {{ op://real/secret }}\nB: {{ test:valid }}";
let (_tmp, out_dir, config) = setup("mixed.yaml", content);
let provider = Arc::new(MockProvider::new(vec![("test:valid", "value")]));
let manager = SecretFileManager::new(config, provider).unwrap();
manager.inject_all().await.unwrap();
let result = std::fs::read_to_string(out_dir.join("mixed.yaml")).unwrap();
assert_eq!(result, "A: {{ op://real/secret }}\nB: value");
}
fn make_mapping(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> PathMapping {
PathMapping::try_new(
CanonicalPath::try_new(src).expect("test source must exist"),
AbsolutePath::new(dst),
)
.expect("mapping creation failed")
}