xerv_core/testing/providers/
env.rs

1//! Environment variable provider for abstracting environment access.
2//!
3//! Allows tests to use mock environment variables while production code
4//! uses the real environment.
5
6use parking_lot::RwLock;
7use std::collections::HashMap;
8
9/// Provider trait for environment variable operations.
10pub trait EnvProvider: Send + Sync {
11    /// Get an environment variable.
12    fn var(&self, key: &str) -> Option<String>;
13
14    /// Set an environment variable.
15    fn set_var(&self, key: &str, value: &str);
16
17    /// Remove an environment variable.
18    fn remove_var(&self, key: &str);
19
20    /// Get all environment variables.
21    fn vars(&self) -> HashMap<String, String>;
22
23    /// Check if this is a mock provider.
24    fn is_mock(&self) -> bool;
25}
26
27/// Real environment provider that uses actual environment variables.
28pub struct RealEnv;
29
30impl RealEnv {
31    /// Create a new real environment provider.
32    pub fn new() -> Self {
33        Self
34    }
35}
36
37impl Default for RealEnv {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl EnvProvider for RealEnv {
44    fn var(&self, key: &str) -> Option<String> {
45        std::env::var(key).ok()
46    }
47
48    fn set_var(&self, key: &str, value: &str) {
49        // SAFETY: This modifies global state. The caller must ensure no other threads
50        // are concurrently reading or modifying the same environment variable.
51        unsafe { std::env::set_var(key, value) };
52    }
53
54    fn remove_var(&self, key: &str) {
55        // SAFETY: This modifies global state. The caller must ensure no other threads
56        // are concurrently reading or modifying the same environment variable.
57        unsafe { std::env::remove_var(key) };
58    }
59
60    fn vars(&self) -> HashMap<String, String> {
61        std::env::vars().collect()
62    }
63
64    fn is_mock(&self) -> bool {
65        false
66    }
67}
68
69/// Mock environment provider for testing.
70///
71/// Provides an isolated environment that doesn't affect the real process environment.
72///
73/// # Example
74///
75/// ```
76/// use xerv_core::testing::{MockEnv, EnvProvider};
77///
78/// let env = MockEnv::new()
79///     .with_var("API_KEY", "test-key")
80///     .with_var("DEBUG", "true");
81///
82/// assert_eq!(env.var("API_KEY"), Some("test-key".to_string()));
83/// assert_eq!(env.var("DEBUG"), Some("true".to_string()));
84/// assert_eq!(env.var("MISSING"), None);
85/// ```
86pub struct MockEnv {
87    vars: RwLock<HashMap<String, String>>,
88}
89
90impl MockEnv {
91    /// Create a new empty mock environment.
92    pub fn new() -> Self {
93        Self {
94            vars: RwLock::new(HashMap::new()),
95        }
96    }
97
98    /// Create a mock environment with the given variables.
99    pub fn with_vars(vars: HashMap<String, String>) -> Self {
100        Self {
101            vars: RwLock::new(vars),
102        }
103    }
104
105    /// Add a variable to the mock environment.
106    pub fn with_var(self, key: impl Into<String>, value: impl Into<String>) -> Self {
107        self.vars.write().insert(key.into(), value.into());
108        self
109    }
110
111    /// Create a mock environment from key-value pairs.
112    pub fn from_pairs(pairs: &[(&str, &str)]) -> Self {
113        let vars: HashMap<String, String> = pairs
114            .iter()
115            .map(|(k, v)| (k.to_string(), v.to_string()))
116            .collect();
117        Self::with_vars(vars)
118    }
119
120    /// Clear all variables.
121    pub fn clear(&self) {
122        self.vars.write().clear();
123    }
124
125    /// Get the number of variables.
126    pub fn len(&self) -> usize {
127        self.vars.read().len()
128    }
129
130    /// Check if the environment is empty.
131    pub fn is_empty(&self) -> bool {
132        self.vars.read().is_empty()
133    }
134}
135
136impl Default for MockEnv {
137    fn default() -> Self {
138        Self::new()
139    }
140}
141
142impl EnvProvider for MockEnv {
143    fn var(&self, key: &str) -> Option<String> {
144        self.vars.read().get(key).cloned()
145    }
146
147    fn set_var(&self, key: &str, value: &str) {
148        self.vars.write().insert(key.to_string(), value.to_string());
149    }
150
151    fn remove_var(&self, key: &str) {
152        self.vars.write().remove(key);
153    }
154
155    fn vars(&self) -> HashMap<String, String> {
156        self.vars.read().clone()
157    }
158
159    fn is_mock(&self) -> bool {
160        true
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn mock_env_basic() {
170        let env = MockEnv::new().with_var("FOO", "bar").with_var("BAZ", "qux");
171
172        assert_eq!(env.var("FOO"), Some("bar".to_string()));
173        assert_eq!(env.var("BAZ"), Some("qux".to_string()));
174        assert_eq!(env.var("MISSING"), None);
175    }
176
177    #[test]
178    fn mock_env_set_and_remove() {
179        let env = MockEnv::new();
180
181        env.set_var("KEY", "value");
182        assert_eq!(env.var("KEY"), Some("value".to_string()));
183
184        env.remove_var("KEY");
185        assert_eq!(env.var("KEY"), None);
186    }
187
188    #[test]
189    fn mock_env_from_pairs() {
190        let env = MockEnv::from_pairs(&[("A", "1"), ("B", "2"), ("C", "3")]);
191
192        assert_eq!(env.var("A"), Some("1".to_string()));
193        assert_eq!(env.var("B"), Some("2".to_string()));
194        assert_eq!(env.var("C"), Some("3".to_string()));
195        assert_eq!(env.len(), 3);
196    }
197
198    #[test]
199    fn mock_env_vars() {
200        let env = MockEnv::new().with_var("X", "1").with_var("Y", "2");
201
202        let vars = env.vars();
203        assert_eq!(vars.len(), 2);
204        assert_eq!(vars.get("X"), Some(&"1".to_string()));
205        assert_eq!(vars.get("Y"), Some(&"2".to_string()));
206    }
207
208    #[test]
209    fn mock_env_clear() {
210        let env = MockEnv::new().with_var("A", "1").with_var("B", "2");
211
212        assert_eq!(env.len(), 2);
213        env.clear();
214        assert!(env.is_empty());
215    }
216
217    #[test]
218    fn mock_env_isolation() {
219        // Verify mock doesn't affect real environment
220        let key = "XERV_TEST_ISOLATION_KEY";
221        let env = MockEnv::new().with_var(key, "mock_value");
222
223        assert_eq!(env.var(key), Some("mock_value".to_string()));
224        assert!(std::env::var(key).is_err());
225    }
226}