env_wrapper/
lib.rs

1//! A wrapper around the standard [`std::env`](https://doc.rust-lang.org/std/env/index.html)
2//! functions that allows for a test double to be injected during testing.
3//!
4//! # Motivation
5//! Testing code that relies on the state of environment variables can be
6//! fragile, since the state may change between tests or be polluted by other tests.
7//! The ideal solution is to have a private set of environment variables per test,
8//! so these problems cannot happen.
9//!
10//! # Approach
11//! This crate introduces the [`RealEnvironment`](RealEnvironment)
12//! (a wrapper around the functions in [`std::env`](https://doc.rust-lang.org/std/env/index.html))
13//! and
14//! [`FakeEnvironment`](FakeEnvironment) structs, which implement the
15//! [`Environment`](Environment) trait. Instead of using
16//! [`std::env`](https://doc.rust-lang.org/std/env/index.html) directly,
17//! use [`RealEnvironment`](RealEnvironment) with
18//! [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection)
19//! so each of your tests can have a private set of environment variables.
20//!
21//! # Example
22//! Scenario: An app looks for the presence of the `CONFIG_LOCATION` environment
23//! variable. If it isn't set, it uses a default location.
24//!
25//! ```rust
26//! use env_wrapper::{Environment, RealEnvironment};
27//!
28//! const CONFIG_LOCATION_ENV_VAR_NAME: &str = "CONFIG_LOCATION";
29//! const DEFAULT_CONFIG_LOCATION: &str = "/etc/my_app/service.conf";
30//!
31//! fn main() {
32//!     // In the production code, inject RealEnvironment.
33//!     let real_env = RealEnvironment;
34//!     let config_location = get_config_location(real_env);
35//! }
36//!
37//! fn get_config_location(env: impl Environment) -> String {
38//!     match env.var(CONFIG_LOCATION_ENV_VAR_NAME) {
39//!         Ok(location) => location,
40//!         _ => DEFAULT_CONFIG_LOCATION.to_string(),
41//!     }
42//! }
43//!
44//! #[test]
45//! fn when_the_user_has_set_the_config_location_env_var_then_use_that_location() {
46//!     use env_wrapper::FakeEnvironment;
47//!
48//!     // Arrange
49//!     // Each test should have a separate instance of FakeEnvironment.
50//!     let mut fake_env = FakeEnvironment::new();
51//!     let user_specified_location = "/a/user/specified/location";
52//!     fake_env.set_var(CONFIG_LOCATION_ENV_VAR_NAME, user_specified_location);
53//!     
54//!     // Act
55//!     // In the test code, inject FakeEnvironment.
56//!     let location = get_config_location(fake_env);
57//!
58//!     // Assert
59//!     assert_eq!(location, user_specified_location);
60//! }
61//! ```
62
63#[cfg(test)]
64pub(crate) mod test_helpers;
65
66use std::{
67    collections::HashMap,
68    env::{self, VarError},
69    ffi::{OsStr, OsString},
70};
71
72/// Represents a process's environment.
73pub trait Environment {
74    /// Set an environment variable.
75    fn set_var(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>);
76
77    /// Get an environment variable, checking for valid UTF-8. If valid UTF-8
78    /// checks are not needed, use `var_os`.
79    ///
80    /// # Errors
81    /// * If a key doesn't exist, it should return a `VarError::NotPresent`.
82    /// * If the environment variable value contains invalid UTF-8, it
83    /// should return `VarError::NotUnicode(OsString)`.
84    fn var(&self, key: impl AsRef<OsStr>) -> Result<String, VarError>;
85
86    /// Get an environment variable. This does not check for valid UTF-8.
87    /// If a valid UTF-8 check is needed, use `var` instead.
88    fn var_os(&self, key: impl AsRef<OsStr>) -> Option<OsString>;
89
90    /// Remove an environment variable from the current process environment.
91    fn remove_var(&mut self, key: impl AsRef<OsStr>);
92}
93
94/// The process's environment. Wraps the standard
95/// [`std::env`](https://doc.rust-lang.org/std/env/index.html) functions.
96///
97/// When testing, [`FakeEnvironment`](FakeEnvironment) should likely be used instead.
98///
99/// # Note
100/// Different instances of the struct all reference the _same_ underlying process
101/// environment.
102///
103/// # Example
104/// ```rust
105/// # use env_wrapper::{Environment, RealEnvironment};
106/// let real_env = RealEnvironment;
107/// get_config_location(real_env);
108///
109/// fn get_config_location(env: impl Environment) {
110/// //...
111/// }
112/// ```
113pub struct RealEnvironment;
114
115impl Environment for RealEnvironment {
116    /// From [`std::env::set_var`](https://doc.rust-lang.org/std/env/fn.set_var.html):
117    /// > Sets the environment variable `key` to the value `value` for the currently running
118    /// > process.
119    /// >
120    /// > Note that while concurrent access to environment variables is safe in Rust,
121    /// > some platforms only expose inherently unsafe non-threadsafe APIs for
122    /// > inspecting the environment. As a result, extra care needs to be taken when
123    /// > auditing calls to unsafe external FFI functions to ensure that any external
124    /// > environment accesses are properly synchronized with accesses in Rust.
125    /// >
126    /// > Discussion of this unsafety on Unix may be found in:
127    /// >
128    /// >  - [Austin Group Bugzilla](https://austingroupbugs.net/view.php?id=188)
129    /// >  - [GNU C library Bugzilla](https://sourceware.org/bugzilla/show_bug.cgi?id=15607#c2)
130    /// >
131    /// > # Panics
132    /// >
133    /// > This function may panic if `key` is empty, contains an ASCII equals sign `'='`
134    /// > or the NUL character `'\0'`, or when `value` contains the NUL character.
135    fn set_var(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) {
136        env::set_var(key, value)
137    }
138
139    /// From [`std::env::var`](https://doc.rust-lang.org/std/env/fn.var.html):
140    /// > Fetches the environment variable `key` from the current process.
141    /// >
142    /// > # Errors
143    /// >
144    /// > This function will return an error if the environment variable isn't set.
145    /// >
146    /// > This function may return an error if the environment variable's name contains
147    /// > the equal sign character (`=`) or the NUL character.
148    /// >
149    /// > This function will return an error if the environment variable's value is
150    /// > not valid Unicode. If this is not desired, consider using [`var_os`].
151    fn var(&self, key: impl AsRef<OsStr>) -> Result<String, VarError> {
152        env::var(key)
153    }
154
155    /// From [`std::env::var_os`](https://doc.rust-lang.org/std/env/fn.var_os.html):
156    /// > Fetches the environment variable `key` from the current process, returning
157    /// > [`None`] if the variable isn't set or there's another error.
158    /// >
159    /// > Note that the method will not check if the environment variable
160    /// > is valid Unicode. If you want to have an error on invalid UTF-8,
161    /// > use the [`var`] function instead.
162    /// >
163    /// > # Errors
164    /// >
165    /// > This function returns an error if the environment variable isn't set.
166    /// >
167    /// > This function may return an error if the environment variable's name contains
168    /// > the equal sign character (`=`) or the NUL character.
169    /// >
170    /// > This function may return an error if the environment variable's value contains
171    /// > the NUL character.
172    fn var_os(&self, key: impl AsRef<OsStr>) -> Option<OsString> {
173        env::var_os(key)
174    }
175
176    /// From [`std::env::remove_var`](https://doc.rust-lang.org/std/env/fn.remove_var.html):
177    /// > Removes an environment variable from the environment of the currently running process.
178    /// >
179    /// > Note that while concurrent access to environment variables is safe in Rust,
180    /// > some platforms only expose inherently unsafe non-threadsafe APIs for
181    /// > inspecting the environment. As a result extra care needs to be taken when
182    /// > auditing calls to unsafe external FFI functions to ensure that any external
183    /// > environment accesses are properly synchronized with accesses in Rust.
184    /// >
185    /// > Discussion of this unsafety on Unix may be found in:
186    /// >
187    /// >  - [Austin Group Bugzilla](https://austingroupbugs.net/view.php?id=188)
188    /// >  - [GNU C library Bugzilla](https://sourceware.org/bugzilla/show_bug.cgi?id=15607#c2)
189    /// >
190    /// > # Panics
191    /// >
192    /// > This function may panic if `key` is empty, contains an ASCII equals sign
193    /// > `'='` or the NUL character `'\0'`, or when the value contains the NUL
194    /// > character.
195    fn remove_var(&mut self, key: impl AsRef<OsStr>) {
196        env::remove_var(key)
197    }
198}
199
200/// A fake process environment, suitable for testing.
201///
202/// # Notes
203/// To make sure one test's environment state does not affect another, use a new
204/// instance of `FakeEnvironment` for each test.
205///
206/// # Example
207/// ```rust
208/// # use env_wrapper::{Environment, FakeEnvironment};
209/// const CONFIG_LOCATION_ENV_VAR_NAME: &str = "CONFIG_LOCATION";
210/// const DEFAULT_CONFIG_LOCATION: &str = "/etc/my_app/service.conf";
211///
212/// fn get_config_location(env: impl Environment) -> String {
213///     match env.var(CONFIG_LOCATION_ENV_VAR_NAME) {
214///         Ok(location) => location,
215///         _ => DEFAULT_CONFIG_LOCATION.to_string(),
216///     }
217/// }
218///
219/// #[test]
220/// fn when_the_user_has_set_the_config_location_env_var_then_use_that_location() {
221///
222///     // Arrange
223///     // Each test should have a separate instance of FakeEnvironment.
224///     let mut fake_env = FakeEnvironment::new();
225///     let user_specified_location = "/a/user/specified/location";
226///     fake_env.set_var(CONFIG_LOCATION_ENV_VAR_NAME, user_specified_location);
227///     
228///     // Act
229///     // In test code, inject FakeEnvironment.
230///     let location = get_config_location(fake_env);
231///
232///     // Assert
233///     assert_eq!(location, user_specified_location);
234/// }
235/// ```
236#[derive(Clone, Debug, Default, Eq, PartialEq)]
237pub struct FakeEnvironment {
238    env_vars: HashMap<OsString, OsString>,
239}
240
241impl FakeEnvironment {
242    pub fn new() -> Self {
243        FakeEnvironment {
244            env_vars: HashMap::new(),
245        }
246    }
247}
248
249impl Environment for FakeEnvironment {
250    fn set_var(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) {
251        self.env_vars
252            .insert(key.as_ref().into(), value.as_ref().into());
253    }
254
255    fn var(&self, key: impl AsRef<OsStr>) -> Result<String, VarError> {
256        match self.env_vars.get(key.as_ref()) {
257            Some(val) => match val.to_str() {
258                Some(valid_utf8) => Ok(valid_utf8.into()),
259                None => Err(VarError::NotUnicode(val.into())),
260            },
261            None => Err(VarError::NotPresent),
262        }
263    }
264
265    fn var_os(&self, key: impl AsRef<OsStr>) -> Option<OsString> {
266        self.env_vars.get(key.as_ref()).cloned()
267    }
268
269    fn remove_var(&mut self, key: impl AsRef<OsStr>) {
270        self.env_vars.remove(key.as_ref());
271    }
272}
273
274// These tests represent behavior that should be shared by fake and real
275// implementations. Both are being tested to enforce behavioral parity.
276#[cfg(test)]
277mod tests {
278    use std::{
279        env::VarError,
280        ffi::{OsStr, OsString},
281        os::unix::ffi::OsStrExt,
282    };
283
284    use crate::{test_helpers::random_upper, Environment, FakeEnvironment, RealEnvironment};
285
286    const INVALID_UTF8: [u8; 4] = [0x66, 0x6f, 0x80, 0x6f];
287
288    #[test]
289    fn when_adding_an_environment_variable_then_it_can_be_read() {
290        fn test(mut env: impl Environment) {
291            // Arrange
292            let key = random_upper();
293            let value = random_upper();
294            env.set_var(&key, &value);
295
296            // Act
297            let result = env.var(&key);
298
299            // Assert
300            assert_eq!(result.unwrap(), value);
301        }
302        test(RealEnvironment);
303        test(FakeEnvironment::new());
304    }
305
306    #[test]
307    fn given_a_nonexistent_env_var_when_getting_the_env_var_with_var_then_it_is_a_not_present_error(
308    ) {
309        fn test(env: impl Environment) {
310            // Arrange
311            let nonexistent_key = random_upper();
312
313            // Act
314            let result = env.var(nonexistent_key);
315
316            // Assert
317            assert_eq!(result.unwrap_err(), VarError::NotPresent);
318        }
319        test(RealEnvironment);
320        test(FakeEnvironment::new());
321    }
322
323    #[test]
324    fn when_setting_env_vars_then_multiple_data_types_can_be_used_on_the_same_environment_instance()
325    {
326        fn test(mut env: impl Environment) {
327            // Act
328            env.set_var(&random_upper(), &random_upper());
329            env.set_var(random_upper(), random_upper());
330            env.set_var(OsStr::new(&random_upper()), OsStr::new(&random_upper()));
331            env.set_var(
332                OsString::from(random_upper()),
333                OsString::from(random_upper()),
334            );
335
336            // Assert - none. This is strictly for type enforcement.
337        }
338        test(RealEnvironment);
339        test(FakeEnvironment::new());
340    }
341
342    #[test]
343    fn when_using_var_getter_with_an_invalid_utf8_value_then_it_is_a_not_unicode_error() {
344        fn test(mut env: impl Environment) {
345            // Arrange
346            let key = random_upper();
347            env.set_var(&key, OsStr::from_bytes(&INVALID_UTF8));
348
349            // Act
350            let result = env.var(&key);
351
352            // Assert
353            assert!(matches!(result, Err(VarError::NotUnicode(_))));
354        }
355        test(RealEnvironment);
356        test(FakeEnvironment::new());
357    }
358
359    #[test]
360    fn given_a_nonexistent_env_var_when_getting_the_env_var_with_var_os_then_it_is_none() {
361        fn test(env: impl Environment) {
362            // Arrange
363            let key = random_upper();
364
365            // Act
366            let result = env.var_os(key);
367
368            // Assert
369            assert!(result.is_none());
370        }
371        test(RealEnvironment);
372        test(FakeEnvironment::new());
373    }
374
375    #[test]
376    fn given_an_env_var_with_invalid_utf8_when_getting_the_env_var_with_var_os_then_it_is_some() {
377        fn test(mut env: impl Environment) {
378            // Arrange
379            let key = random_upper();
380            env.set_var(&key, OsStr::from_bytes(&INVALID_UTF8));
381
382            // Act
383            let result = env.var_os(&key);
384
385            // Assert
386            assert!(result.is_some());
387        }
388        test(RealEnvironment);
389        test(FakeEnvironment::new());
390    }
391
392    #[test]
393    fn given_an_existing_environment_variable_when_setting_the_same_environment_variable_then_the_value_is_overwritten(
394    ) {
395        fn test(mut env: impl Environment) {
396            // Arrange
397            let key = random_upper();
398            let val_1 = random_upper();
399            let val_2 = random_upper();
400            env.set_var(&key, &val_1);
401
402            // Act
403            env.set_var(&key, &val_2);
404
405            // Assert
406            assert_eq!(env.var(&key).unwrap(), val_2);
407        }
408        test(RealEnvironment);
409        test(FakeEnvironment::new());
410    }
411
412    #[test]
413    fn given_an_existing_environment_variable_when_removing_the_same_environment_variable_then_the_variable_no_longer_exists(
414    ) {
415        fn test(mut env: impl Environment) {
416            // Arrange
417            let key = random_upper();
418            let value = random_upper();
419            env.set_var(&key, &value);
420
421            // Act
422            env.remove_var(&key);
423
424            // Assert
425            assert_eq!(env.var(&key).unwrap_err(), VarError::NotPresent);
426        }
427        test(RealEnvironment);
428        test(FakeEnvironment::new());
429    }
430
431    #[test]
432    fn when_removing_a_nonexistent_environment_variable_then_do_not_panic() {
433        fn test(mut env: impl Environment) {
434            // Arrange
435            let key = random_upper();
436
437            // Act
438            env.remove_var(&key);
439
440            // Assert - no assertion
441        }
442
443        test(RealEnvironment);
444        test(FakeEnvironment::new());
445    }
446}