11use log:: debug;
22use std:: path:: PathBuf ;
33
4+ /// Trait for reading environment variables - allows mocking in tests
5+ pub trait EnvReader {
6+ fn var ( & self , key : & str ) -> Result < String , std:: env:: VarError > ;
7+ }
8+
9+ /// Production implementation using std::env
10+ pub struct StdEnvReader ;
11+
12+ impl EnvReader for StdEnvReader {
13+ fn var ( & self , key : & str ) -> Result < String , std:: env:: VarError > {
14+ std:: env:: var ( key)
15+ }
16+ }
17+
418/// Locate the configuration file following XDG Base Directory specification
519///
620/// Supports both rb.kdl and rb.toml (preferring .kdl)
@@ -13,6 +27,14 @@ use std::path::PathBuf;
1327/// 5. %APPDATA%/rb/rb.kdl or rb.toml (Windows)
1428/// 6. ~/.rb.kdl or ~/.rb.toml (cross-platform fallback)
1529pub fn locate_config_file ( override_path : Option < PathBuf > ) -> Option < PathBuf > {
30+ locate_config_file_with_env ( override_path, & StdEnvReader )
31+ }
32+
33+ /// Internal function that accepts an environment reader for testing
34+ fn locate_config_file_with_env (
35+ override_path : Option < PathBuf > ,
36+ env : & dyn EnvReader ,
37+ ) -> Option < PathBuf > {
1638 debug ! ( "Searching for configuration file..." ) ;
1739
1840 // 1. Check for explicit override first
@@ -25,7 +47,7 @@ pub fn locate_config_file(override_path: Option<PathBuf>) -> Option<PathBuf> {
2547 }
2648
2749 // 2. Check RB_CONFIG environment variable
28- if let Ok ( rb_config) = std :: env:: var ( "RB_CONFIG" ) {
50+ if let Ok ( rb_config) = env. var ( "RB_CONFIG" ) {
2951 let config_path = PathBuf :: from ( rb_config) ;
3052 debug ! ( " Checking RB_CONFIG env var: {}" , config_path. display( ) ) ;
3153 if config_path. exists ( ) {
@@ -35,7 +57,7 @@ pub fn locate_config_file(override_path: Option<PathBuf>) -> Option<PathBuf> {
3557 }
3658
3759 // 3. Try XDG_CONFIG_HOME (Unix/Linux)
38- if let Ok ( xdg_config) = std :: env:: var ( "XDG_CONFIG_HOME" ) {
60+ if let Ok ( xdg_config) = env. var ( "XDG_CONFIG_HOME" ) {
3961 let base_path = PathBuf :: from ( xdg_config) . join ( "rb" ) ;
4062 // Try .kdl first, then .toml
4163 for ext in & [ "rb.kdl" , "rb.toml" ] {
@@ -98,6 +120,34 @@ pub fn locate_config_file(override_path: Option<PathBuf>) -> Option<PathBuf> {
98120#[ cfg( test) ]
99121mod tests {
100122 use super :: * ;
123+ use std:: collections:: HashMap ;
124+
125+ /// Mock environment reader for testing without global state mutation
126+ struct MockEnvReader {
127+ vars : HashMap < String , String > ,
128+ }
129+
130+ impl MockEnvReader {
131+ fn new ( ) -> Self {
132+ Self {
133+ vars : HashMap :: new ( ) ,
134+ }
135+ }
136+
137+ fn with_var ( mut self , key : impl Into < String > , value : impl Into < String > ) -> Self {
138+ self . vars . insert ( key. into ( ) , value. into ( ) ) ;
139+ self
140+ }
141+ }
142+
143+ impl EnvReader for MockEnvReader {
144+ fn var ( & self , key : & str ) -> Result < String , std:: env:: VarError > {
145+ self . vars
146+ . get ( key)
147+ . cloned ( )
148+ . ok_or ( std:: env:: VarError :: NotPresent )
149+ }
150+ }
101151
102152 #[ test]
103153 fn test_locate_config_file_returns_option ( ) {
@@ -127,24 +177,44 @@ mod tests {
127177 fn test_locate_config_file_with_env_var ( ) {
128178 use std:: fs;
129179 let temp_dir = std:: env:: temp_dir ( ) ;
130- let config_path = temp_dir. join ( "test_rb_env .toml" ) ;
180+ let config_path = temp_dir. join ( "test_rb_env_mock .toml" ) ;
131181
132182 // Create a temporary config file
133183 fs:: write ( & config_path, "# test config" ) . expect ( "Failed to write test config" ) ;
134184
135- // Set environment variable (unsafe but required for testing)
136- unsafe {
137- std:: env:: set_var ( "RB_CONFIG" , & config_path) ;
138- }
185+ // Use mock environment - no global state mutation!
186+ let mock_env =
187+ MockEnvReader :: new ( ) . with_var ( "RB_CONFIG" , config_path. to_string_lossy ( ) . to_string ( ) ) ;
139188
140189 // Should return the env var path
141- let result = locate_config_file ( None ) ;
190+ let result = locate_config_file_with_env ( None , & mock_env ) ;
142191 assert_eq ! ( result, Some ( config_path. clone( ) ) ) ;
143192
144193 // Cleanup
145- unsafe {
146- std:: env:: remove_var ( "RB_CONFIG" ) ;
147- }
148194 let _ = fs:: remove_file ( & config_path) ;
149195 }
196+
197+ #[ test]
198+ fn test_locate_config_file_with_xdg_config_home ( ) {
199+ use std:: fs;
200+ let temp_dir = std:: env:: temp_dir ( ) ;
201+ let xdg_base = temp_dir. join ( "test_xdg_config" ) ;
202+ let rb_dir = xdg_base. join ( "rb" ) ;
203+ let config_path = rb_dir. join ( "rb.toml" ) ;
204+
205+ // Create directory structure
206+ fs:: create_dir_all ( & rb_dir) . expect ( "Failed to create test directory" ) ;
207+ fs:: write ( & config_path, "# test config" ) . expect ( "Failed to write test config" ) ;
208+
209+ // Use mock environment
210+ let mock_env = MockEnvReader :: new ( )
211+ . with_var ( "XDG_CONFIG_HOME" , xdg_base. to_string_lossy ( ) . to_string ( ) ) ;
212+
213+ // Should return the XDG config path
214+ let result = locate_config_file_with_env ( None , & mock_env) ;
215+ assert_eq ! ( result, Some ( config_path. clone( ) ) ) ;
216+
217+ // Cleanup
218+ let _ = fs:: remove_dir_all ( & xdg_base) ;
219+ }
150220}
0 commit comments