reddb_server/utils/
env_secret.rs1pub fn env_with_file_fallback(name: &str) -> Option<String> {
17 if let Ok(value) = std::env::var(name) {
18 if !value.trim().is_empty() {
19 return Some(value);
20 }
21 }
22 let file_var = format!("{name}_FILE");
23 let path = std::env::var(&file_var).ok()?;
24 let trimmed_path = path.trim();
25 if trimmed_path.is_empty() {
26 return None;
27 }
28 match std::fs::read_to_string(trimmed_path) {
29 Ok(contents) => {
30 let value = contents.trim_end_matches(['\n', '\r']).to_string();
31 if value.is_empty() {
32 None
33 } else {
34 Some(value)
35 }
36 }
37 Err(err) => {
38 tracing::warn!(
39 target: "reddb::secrets",
40 env = %file_var,
41 path = %trimmed_path,
42 error = %err,
43 "secret file referenced by {file_var} could not be read; falling back to None"
44 );
45 None
46 }
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use std::sync::Mutex;
54
55 fn env_lock() -> &'static Mutex<()> {
59 static LOCK: std::sync::OnceLock<Mutex<()>> = std::sync::OnceLock::new();
60 LOCK.get_or_init(|| Mutex::new(()))
61 }
62
63 #[test]
64 fn returns_inline_when_set() {
65 let _g = env_lock().lock();
66 unsafe {
67 std::env::set_var("REDDB_TEST_INLINE", "value-from-env");
68 std::env::remove_var("REDDB_TEST_INLINE_FILE");
69 }
70 assert_eq!(
71 env_with_file_fallback("REDDB_TEST_INLINE"),
72 Some("value-from-env".to_string())
73 );
74 unsafe {
75 std::env::remove_var("REDDB_TEST_INLINE");
76 }
77 }
78
79 #[test]
80 fn falls_back_to_file_when_inline_empty() {
81 let _g = env_lock().lock();
82 let dir =
83 std::env::temp_dir().join(format!("reddb-env-secret-test-{}", std::process::id()));
84 let _ = std::fs::create_dir_all(&dir);
85 let path = dir.join("token");
86 std::fs::write(&path, "value-from-file\n").unwrap();
87 unsafe {
88 std::env::remove_var("REDDB_TEST_FALLBACK");
89 std::env::set_var("REDDB_TEST_FALLBACK_FILE", &path);
90 }
91 assert_eq!(
92 env_with_file_fallback("REDDB_TEST_FALLBACK"),
93 Some("value-from-file".to_string()) );
95 unsafe {
96 std::env::remove_var("REDDB_TEST_FALLBACK_FILE");
97 }
98 let _ = std::fs::remove_dir_all(&dir);
99 }
100
101 #[test]
102 fn inline_wins_over_file() {
103 let _g = env_lock().lock();
104 let dir = std::env::temp_dir().join(format!("reddb-env-precedence-{}", std::process::id()));
105 let _ = std::fs::create_dir_all(&dir);
106 let path = dir.join("token");
107 std::fs::write(&path, "from-file").unwrap();
108 unsafe {
109 std::env::set_var("REDDB_TEST_PRIORITY", "from-env");
110 std::env::set_var("REDDB_TEST_PRIORITY_FILE", &path);
111 }
112 assert_eq!(
113 env_with_file_fallback("REDDB_TEST_PRIORITY"),
114 Some("from-env".to_string())
115 );
116 unsafe {
117 std::env::remove_var("REDDB_TEST_PRIORITY");
118 std::env::remove_var("REDDB_TEST_PRIORITY_FILE");
119 }
120 let _ = std::fs::remove_dir_all(&dir);
121 }
122
123 #[test]
124 fn returns_none_when_neither_set() {
125 let _g = env_lock().lock();
126 unsafe {
127 std::env::remove_var("REDDB_TEST_NONE");
128 std::env::remove_var("REDDB_TEST_NONE_FILE");
129 }
130 assert_eq!(env_with_file_fallback("REDDB_TEST_NONE"), None);
131 }
132
133 #[test]
134 fn read_failure_returns_none() {
135 let _g = env_lock().lock();
136 unsafe {
137 std::env::remove_var("REDDB_TEST_BAD");
138 std::env::set_var("REDDB_TEST_BAD_FILE", "/nonexistent/path/zzz");
139 }
140 assert_eq!(env_with_file_fallback("REDDB_TEST_BAD"), None);
141 unsafe {
142 std::env::remove_var("REDDB_TEST_BAD_FILE");
143 }
144 }
145}