xerv_core/testing/providers/
secrets.rs1use parking_lot::RwLock;
6use std::collections::HashMap;
7
8pub trait SecretsProvider: Send + Sync {
14 fn get(&self, key: &str) -> Option<String>;
16
17 fn exists(&self, key: &str) -> bool {
19 self.get(key).is_some()
20 }
21
22 fn keys(&self) -> Vec<String>;
24
25 fn is_mock(&self) -> bool;
27}
28
29pub struct RealSecrets {
35 prefix: String,
36}
37
38impl RealSecrets {
39 pub fn new(prefix: impl Into<String>) -> Self {
45 Self {
46 prefix: prefix.into(),
47 }
48 }
49
50 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
87pub struct MockSecrets {
103 secrets: RwLock<HashMap<String, String>>,
104}
105
106impl MockSecrets {
107 pub fn new() -> Self {
109 Self {
110 secrets: RwLock::new(HashMap::new()),
111 }
112 }
113
114 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 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 pub fn set(&self, key: impl Into<String>, value: impl Into<String>) {
133 self.secrets.write().insert(key.into(), value.into());
134 }
135
136 pub fn remove(&self, key: &str) {
138 self.secrets.write().remove(key);
139 }
140
141 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}