xerv_core/testing/providers/
secrets.rs

1//! Secrets provider for abstracting secret management.
2//!
3//! Allows tests to use mock secrets while production code uses a real secret manager.
4
5use parking_lot::RwLock;
6use std::collections::HashMap;
7
8/// Provider trait for secret management.
9///
10/// Secrets are sensitive values that should not be logged or exposed.
11/// Unlike environment variables, secrets are typically stored in a
12/// secure vault or secret manager.
13pub trait SecretsProvider: Send + Sync {
14    /// Get a secret by key.
15    fn get(&self, key: &str) -> Option<String>;
16
17    /// Check if a secret exists.
18    fn exists(&self, key: &str) -> bool {
19        self.get(key).is_some()
20    }
21
22    /// List all secret keys (but not values).
23    fn keys(&self) -> Vec<String>;
24
25    /// Check if this is a mock provider.
26    fn is_mock(&self) -> bool;
27}
28
29/// Real secrets provider that reads from environment variables.
30///
31/// This is a simple implementation that reads secrets from environment
32/// variables. In production, you might want to integrate with a proper
33/// secret manager like HashiCorp Vault, AWS Secrets Manager, etc.
34pub struct RealSecrets {
35    prefix: String,
36}
37
38impl RealSecrets {
39    /// Create a new real secrets provider.
40    ///
41    /// Secrets are read from environment variables with the given prefix.
42    /// For example, with prefix "APP_SECRET_", the key "API_KEY" would
43    /// be read from "APP_SECRET_API_KEY".
44    pub fn new(prefix: impl Into<String>) -> Self {
45        Self {
46            prefix: prefix.into(),
47        }
48    }
49
50    /// Create a real secrets provider with no prefix.
51    pub fn no_prefix() -> Self {
52        Self {
53            prefix: String::new(),
54        }
55    }
56}
57
58impl Default for RealSecrets {
59    fn default() -> Self {
60        Self::no_prefix()
61    }
62}
63
64impl SecretsProvider for RealSecrets {
65    fn get(&self, key: &str) -> Option<String> {
66        let env_key = format!("{}{}", self.prefix, key);
67        std::env::var(&env_key).ok()
68    }
69
70    fn keys(&self) -> Vec<String> {
71        std::env::vars()
72            .filter_map(|(k, _)| {
73                if k.starts_with(&self.prefix) {
74                    Some(k[self.prefix.len()..].to_string())
75                } else {
76                    None
77                }
78            })
79            .collect()
80    }
81
82    fn is_mock(&self) -> bool {
83        false
84    }
85}
86
87/// Mock secrets provider for testing.
88///
89/// # Example
90///
91/// ```
92/// use xerv_core::testing::{MockSecrets, SecretsProvider};
93///
94/// let secrets = MockSecrets::new()
95///     .with_secret("API_KEY", "sk-test-12345")
96///     .with_secret("DB_PASSWORD", "super-secret");
97///
98/// assert_eq!(secrets.get("API_KEY"), Some("sk-test-12345".to_string()));
99/// assert!(secrets.exists("DB_PASSWORD"));
100/// assert!(!secrets.exists("MISSING"));
101/// ```
102pub struct MockSecrets {
103    secrets: RwLock<HashMap<String, String>>,
104}
105
106impl MockSecrets {
107    /// Create a new empty mock secrets provider.
108    pub fn new() -> Self {
109        Self {
110            secrets: RwLock::new(HashMap::new()),
111        }
112    }
113
114    /// Add a secret.
115    pub fn with_secret(self, key: impl Into<String>, value: impl Into<String>) -> Self {
116        self.secrets.write().insert(key.into(), value.into());
117        self
118    }
119
120    /// Create from key-value pairs.
121    pub fn from_pairs(pairs: &[(&str, &str)]) -> Self {
122        let secrets: HashMap<String, String> = pairs
123            .iter()
124            .map(|(k, v)| (k.to_string(), v.to_string()))
125            .collect();
126        Self {
127            secrets: RwLock::new(secrets),
128        }
129    }
130
131    /// Set a secret (for dynamic updates during tests).
132    pub fn set(&self, key: impl Into<String>, value: impl Into<String>) {
133        self.secrets.write().insert(key.into(), value.into());
134    }
135
136    /// Remove a secret.
137    pub fn remove(&self, key: &str) {
138        self.secrets.write().remove(key);
139    }
140
141    /// Clear all secrets.
142    pub fn clear(&self) {
143        self.secrets.write().clear();
144    }
145}
146
147impl Default for MockSecrets {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153impl SecretsProvider for MockSecrets {
154    fn get(&self, key: &str) -> Option<String> {
155        self.secrets.read().get(key).cloned()
156    }
157
158    fn keys(&self) -> Vec<String> {
159        self.secrets.read().keys().cloned().collect()
160    }
161
162    fn is_mock(&self) -> bool {
163        true
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn mock_secrets_basic() {
173        let secrets = MockSecrets::new()
174            .with_secret("KEY1", "value1")
175            .with_secret("KEY2", "value2");
176
177        assert_eq!(secrets.get("KEY1"), Some("value1".to_string()));
178        assert_eq!(secrets.get("KEY2"), Some("value2".to_string()));
179        assert_eq!(secrets.get("KEY3"), None);
180    }
181
182    #[test]
183    fn mock_secrets_exists() {
184        let secrets = MockSecrets::new().with_secret("EXISTS", "value");
185
186        assert!(secrets.exists("EXISTS"));
187        assert!(!secrets.exists("MISSING"));
188    }
189
190    #[test]
191    fn mock_secrets_keys() {
192        let secrets = MockSecrets::new()
193            .with_secret("A", "1")
194            .with_secret("B", "2")
195            .with_secret("C", "3");
196
197        let mut keys = secrets.keys();
198        keys.sort();
199        assert_eq!(keys, vec!["A", "B", "C"]);
200    }
201
202    #[test]
203    fn mock_secrets_dynamic_update() {
204        let secrets = MockSecrets::new();
205
206        secrets.set("DYNAMIC", "initial");
207        assert_eq!(secrets.get("DYNAMIC"), Some("initial".to_string()));
208
209        secrets.set("DYNAMIC", "updated");
210        assert_eq!(secrets.get("DYNAMIC"), Some("updated".to_string()));
211
212        secrets.remove("DYNAMIC");
213        assert_eq!(secrets.get("DYNAMIC"), None);
214    }
215
216    #[test]
217    fn mock_secrets_from_pairs() {
218        let secrets = MockSecrets::from_pairs(&[("X", "1"), ("Y", "2")]);
219
220        assert_eq!(secrets.get("X"), Some("1".to_string()));
221        assert_eq!(secrets.get("Y"), Some("2".to_string()));
222    }
223}