txgate_core/config_loader.rs
1//! Configuration loader for the `TxGate` signing service.
2//!
3//! This module provides utilities for loading, saving, and managing configuration
4//! files from the filesystem. It handles path expansion (e.g., `~` to home directory)
5//! and provides sensible defaults when configuration files don't exist.
6//!
7//! # Default Location
8//!
9//! Configuration is stored at `~/.txgate/config.toml` by default.
10//!
11//! # Examples
12//!
13//! ## Loading configuration with defaults
14//!
15//! ```no_run
16//! use txgate_core::config_loader::load_config;
17//!
18//! // Load config from default location, using defaults if file doesn't exist
19//! let config = load_config().expect("failed to load config");
20//! ```
21//!
22//! ## Using `ConfigLoader` for more control
23//!
24//! ```no_run
25//! use txgate_core::config_loader::ConfigLoader;
26//! use std::path::PathBuf;
27//!
28//! // Create loader with default base directory (~/.txgate)
29//! let loader = ConfigLoader::new().expect("failed to create loader");
30//!
31//! // Check if config exists
32//! if loader.exists() {
33//! let config = loader.load().expect("failed to load config");
34//! println!("Loaded config with timeout: {}s", config.server.timeout_secs);
35//! } else {
36//! // Write default configuration
37//! loader.write_default().expect("failed to write default config");
38//! }
39//! ```
40//!
41//! ## Custom base directory
42//!
43//! ```no_run
44//! use txgate_core::config_loader::ConfigLoader;
45//! use std::path::PathBuf;
46//!
47//! let loader = ConfigLoader::with_base_dir(PathBuf::from("/custom/txgate"));
48//! let config = loader.load().expect("failed to load config");
49//! ```
50
51use crate::config::Config;
52use crate::error::ConfigError;
53use std::fs;
54use std::path::{Path, PathBuf};
55
56/// The default configuration file name.
57const CONFIG_FILE_NAME: &str = "config.toml";
58
59/// The default base directory name within the home directory.
60const BASE_DIR_NAME: &str = ".txgate";
61
62/// Configuration loader that handles reading and writing configuration files.
63///
64/// The `ConfigLoader` manages configuration file operations including:
65/// - Loading configuration from TOML files
66/// - Saving configuration to TOML files
67/// - Creating default configuration files
68/// - Path expansion for `~` (home directory)
69///
70/// # Examples
71///
72/// ```no_run
73/// use txgate_core::config_loader::ConfigLoader;
74///
75/// // Create with default base directory (~/.txgate)
76/// let loader = ConfigLoader::new().expect("failed to create loader");
77///
78/// // Load configuration (returns defaults if file doesn't exist)
79/// let config = loader.load().expect("failed to load config");
80/// ```
81#[derive(Debug, Clone)]
82pub struct ConfigLoader {
83 /// Base directory for `TxGate` files (default: ~/.txgate).
84 base_dir: PathBuf,
85}
86
87impl ConfigLoader {
88 /// Creates a new `ConfigLoader` with the default base directory (`~/.txgate`).
89 ///
90 /// # Errors
91 ///
92 /// Returns [`ConfigError::NoHomeDirectory`] if the home directory cannot be determined.
93 ///
94 /// # Examples
95 ///
96 /// ```no_run
97 /// use txgate_core::config_loader::ConfigLoader;
98 ///
99 /// let loader = ConfigLoader::new().expect("failed to create loader");
100 /// ```
101 pub fn new() -> Result<Self, ConfigError> {
102 let base_dir = default_base_dir()?;
103 Ok(Self { base_dir })
104 }
105
106 /// Creates a `ConfigLoader` with a custom base directory.
107 ///
108 /// # Arguments
109 ///
110 /// * `base_dir` - The base directory for `TxGate` files
111 ///
112 /// # Examples
113 ///
114 /// ```
115 /// use txgate_core::config_loader::ConfigLoader;
116 /// use std::path::PathBuf;
117 ///
118 /// let loader = ConfigLoader::with_base_dir(PathBuf::from("/custom/txgate"));
119 /// ```
120 #[must_use]
121 pub const fn with_base_dir(base_dir: PathBuf) -> Self {
122 Self { base_dir }
123 }
124
125 /// Returns the path to the configuration file.
126 ///
127 /// # Examples
128 ///
129 /// ```no_run
130 /// use txgate_core::config_loader::ConfigLoader;
131 ///
132 /// let loader = ConfigLoader::new().expect("failed to create loader");
133 /// let path = loader.config_path();
134 /// // path is something like "/home/user/.txgate/config.toml"
135 /// ```
136 #[must_use]
137 pub fn config_path(&self) -> PathBuf {
138 self.base_dir.join(CONFIG_FILE_NAME)
139 }
140
141 /// Returns the base directory for `TxGate` files.
142 ///
143 /// # Examples
144 ///
145 /// ```no_run
146 /// use txgate_core::config_loader::ConfigLoader;
147 ///
148 /// let loader = ConfigLoader::new().expect("failed to create loader");
149 /// let base = loader.base_dir();
150 /// // base is something like "/home/user/.txgate"
151 /// ```
152 #[must_use]
153 pub fn base_dir(&self) -> &Path {
154 &self.base_dir
155 }
156
157 /// Loads configuration from the file.
158 ///
159 /// If the configuration file doesn't exist, returns the default configuration.
160 /// If the file exists but contains invalid TOML, returns a parse error.
161 ///
162 /// # Errors
163 ///
164 /// Returns [`ConfigError::ParseFailed`] if the file contains invalid TOML.
165 /// Returns [`ConfigError::Io`] if there's an I/O error reading the file.
166 ///
167 /// # Examples
168 ///
169 /// ```no_run
170 /// use txgate_core::config_loader::ConfigLoader;
171 ///
172 /// let loader = ConfigLoader::new().expect("failed to create loader");
173 /// let config = loader.load().expect("failed to load config");
174 /// ```
175 pub fn load(&self) -> Result<Config, ConfigError> {
176 let config_path = self.config_path();
177
178 if !config_path.exists() {
179 return Ok(Config::default());
180 }
181
182 Self::load_from_path(&config_path)
183 }
184
185 /// Loads configuration from the file, failing if the file doesn't exist.
186 ///
187 /// Unlike [`load`](Self::load), this method returns an error if the
188 /// configuration file is not found.
189 ///
190 /// # Errors
191 ///
192 /// Returns [`ConfigError::FileNotFound`] if the configuration file doesn't exist.
193 /// Returns [`ConfigError::ParseFailed`] if the file contains invalid TOML.
194 /// Returns [`ConfigError::Io`] if there's an I/O error reading the file.
195 ///
196 /// # Examples
197 ///
198 /// ```no_run
199 /// use txgate_core::config_loader::ConfigLoader;
200 ///
201 /// let loader = ConfigLoader::new().expect("failed to create loader");
202 /// match loader.load_required() {
203 /// Ok(config) => println!("Config loaded: {:?}", config),
204 /// Err(e) => eprintln!("Config required but not found: {}", e),
205 /// }
206 /// ```
207 pub fn load_required(&self) -> Result<Config, ConfigError> {
208 let config_path = self.config_path();
209
210 if !config_path.exists() {
211 return Err(ConfigError::file_not_found(
212 config_path.display().to_string(),
213 ));
214 }
215
216 Self::load_from_path(&config_path)
217 }
218
219 /// Saves configuration to the file.
220 ///
221 /// Creates the base directory if it doesn't exist.
222 ///
223 /// # Errors
224 ///
225 /// Returns [`ConfigError::Io`] if there's an I/O error writing the file.
226 /// Returns [`ConfigError::ParseFailed`] if the configuration cannot be serialized.
227 ///
228 /// # Examples
229 ///
230 /// ```no_run
231 /// use txgate_core::config_loader::ConfigLoader;
232 /// use txgate_core::config::Config;
233 ///
234 /// let loader = ConfigLoader::new().expect("failed to create loader");
235 /// let config = Config::builder()
236 /// .timeout_secs(60)
237 /// .build();
238 ///
239 /// loader.save(&config).expect("failed to save config");
240 /// ```
241 pub fn save(&self, config: &Config) -> Result<(), ConfigError> {
242 // Ensure base directory exists
243 self.ensure_base_dir()?;
244
245 let config_path = self.config_path();
246
247 let toml_str = toml::to_string_pretty(config).map_err(|e| {
248 ConfigError::parse_failed(format!("failed to serialize configuration: {e}"))
249 })?;
250
251 fs::write(&config_path, toml_str).map_err(|e| {
252 ConfigError::io(
253 format!("failed to write configuration to {}", config_path.display()),
254 e,
255 )
256 })?;
257
258 Ok(())
259 }
260
261 /// Writes the default configuration file.
262 ///
263 /// Creates the base directory if it doesn't exist.
264 /// Uses the formatted default TOML from [`Config::default_toml`].
265 ///
266 /// # Errors
267 ///
268 /// Returns [`ConfigError::Io`] if there's an I/O error writing the file.
269 ///
270 /// # Examples
271 ///
272 /// ```no_run
273 /// use txgate_core::config_loader::ConfigLoader;
274 ///
275 /// let loader = ConfigLoader::new().expect("failed to create loader");
276 /// loader.write_default().expect("failed to write default config");
277 /// ```
278 pub fn write_default(&self) -> Result<(), ConfigError> {
279 // Ensure base directory exists
280 self.ensure_base_dir()?;
281
282 let config_path = self.config_path();
283 let default_toml = Config::default_toml();
284
285 fs::write(&config_path, default_toml).map_err(|e| {
286 ConfigError::io(
287 format!(
288 "failed to write default configuration to {}",
289 config_path.display()
290 ),
291 e,
292 )
293 })?;
294
295 Ok(())
296 }
297
298 /// Checks if the configuration file exists.
299 ///
300 /// # Examples
301 ///
302 /// ```no_run
303 /// use txgate_core::config_loader::ConfigLoader;
304 ///
305 /// let loader = ConfigLoader::new().expect("failed to create loader");
306 /// if loader.exists() {
307 /// println!("Config file found");
308 /// } else {
309 /// println!("Config file not found, will use defaults");
310 /// }
311 /// ```
312 #[must_use]
313 pub fn exists(&self) -> bool {
314 self.config_path().exists()
315 }
316
317 /// Ensures the base directory exists, creating it if necessary.
318 fn ensure_base_dir(&self) -> Result<(), ConfigError> {
319 if !self.base_dir.exists() {
320 fs::create_dir_all(&self.base_dir).map_err(|e| {
321 ConfigError::io(
322 format!(
323 "failed to create base directory {}",
324 self.base_dir.display()
325 ),
326 e,
327 )
328 })?;
329 }
330 Ok(())
331 }
332
333 /// Loads configuration from a specific path.
334 fn load_from_path(path: &Path) -> Result<Config, ConfigError> {
335 let content = fs::read_to_string(path)
336 .map_err(|e| ConfigError::io(format!("failed to read {}", path.display()), e))?;
337
338 let config: Config = toml::from_str(&content).map_err(|e| {
339 ConfigError::parse_failed(format!("invalid TOML in {}: {e}", path.display()))
340 })?;
341
342 Ok(config)
343 }
344}
345
346/// Expands `~` in paths to the home directory.
347///
348/// If the path starts with `~`, it is replaced with the user's home directory.
349/// Otherwise, the path is returned unchanged (converted to `PathBuf`).
350///
351/// # Errors
352///
353/// Returns [`ConfigError::NoHomeDirectory`] if the path starts with `~` and
354/// the home directory cannot be determined.
355///
356/// # Examples
357///
358/// ```no_run
359/// use txgate_core::config_loader::expand_path;
360///
361/// // Expands ~ to home directory
362/// let path = expand_path("~/.txgate/config.toml").expect("failed to expand path");
363/// // path is something like "/home/user/.txgate/config.toml"
364///
365/// // Absolute paths are unchanged
366/// let path = expand_path("/etc/txgate/config.toml").expect("failed to expand path");
367/// assert_eq!(path.to_string_lossy(), "/etc/txgate/config.toml");
368/// ```
369pub fn expand_path(path: &str) -> Result<PathBuf, ConfigError> {
370 if let Some(rest) = path.strip_prefix("~/") {
371 let home = dirs::home_dir().ok_or_else(ConfigError::no_home_directory)?;
372 Ok(home.join(rest))
373 } else if path == "~" {
374 dirs::home_dir().ok_or_else(ConfigError::no_home_directory)
375 } else {
376 Ok(PathBuf::from(path))
377 }
378}
379
380/// Returns the default base directory for `TxGate` files (`~/.txgate`).
381///
382/// # Errors
383///
384/// Returns [`ConfigError::NoHomeDirectory`] if the home directory cannot be determined.
385///
386/// # Examples
387///
388/// ```no_run
389/// use txgate_core::config_loader::default_base_dir;
390///
391/// let base_dir = default_base_dir().expect("failed to get base dir");
392/// // base_dir is something like "/home/user/.txgate"
393/// ```
394pub fn default_base_dir() -> Result<PathBuf, ConfigError> {
395 let home = dirs::home_dir().ok_or_else(ConfigError::no_home_directory)?;
396 Ok(home.join(BASE_DIR_NAME))
397}
398
399/// Loads configuration from the default location with defaults for missing values.
400///
401/// This is a convenience function that:
402/// 1. Creates a [`ConfigLoader`] with the default base directory
403/// 2. Loads the configuration (returning defaults if file doesn't exist)
404///
405/// # Errors
406///
407/// Returns [`ConfigError::NoHomeDirectory`] if the home directory cannot be determined.
408/// Returns [`ConfigError::ParseFailed`] if the configuration file contains invalid TOML.
409/// Returns [`ConfigError::Io`] if there's an I/O error reading the file.
410///
411/// # Examples
412///
413/// ```no_run
414/// use txgate_core::config_loader::load_config;
415///
416/// let config = load_config().expect("failed to load config");
417/// println!("Socket path: {}", config.server.socket_path);
418/// ```
419pub fn load_config() -> Result<Config, ConfigError> {
420 let loader = ConfigLoader::new()?;
421 loader.load()
422}
423
424/// Loads configuration from the default location and expands all paths.
425///
426/// This is a convenience function that:
427/// 1. Loads the configuration using [`load_config`]
428/// 2. Expands `~` in `server.socket_path` to the full path
429/// 3. Expands `~` in `keys.directory` to the full path
430///
431/// # Errors
432///
433/// Returns [`ConfigError::NoHomeDirectory`] if the home directory cannot be determined.
434/// Returns [`ConfigError::ParseFailed`] if the configuration file contains invalid TOML.
435/// Returns [`ConfigError::Io`] if there's an I/O error reading the file.
436///
437/// # Examples
438///
439/// ```no_run
440/// use txgate_core::config_loader::load_config_with_expanded_paths;
441///
442/// let config = load_config_with_expanded_paths().expect("failed to load config");
443/// // socket_path is something like "/home/user/.txgate/txgate.sock"
444/// println!("Socket path: {}", config.server.socket_path);
445/// ```
446pub fn load_config_with_expanded_paths() -> Result<Config, ConfigError> {
447 let mut config = load_config()?;
448
449 // Expand socket path
450 let expanded_socket = expand_path(&config.server.socket_path)?;
451 config.server.socket_path = expanded_socket.to_string_lossy().to_string();
452
453 // Expand keys directory
454 let expanded_keys_dir = expand_path(&config.keys.directory)?;
455 config.keys.directory = expanded_keys_dir.to_string_lossy().to_string();
456
457 Ok(config)
458}
459
460#[cfg(test)]
461mod tests {
462 #![allow(
463 clippy::expect_used,
464 clippy::unwrap_used,
465 clippy::panic,
466 clippy::indexing_slicing,
467 clippy::similar_names,
468 clippy::redundant_clone,
469 clippy::manual_string_new,
470 clippy::needless_raw_string_hashes,
471 clippy::needless_collect,
472 clippy::unreadable_literal
473 )]
474
475 use super::*;
476 use std::fs;
477 use tempfile::TempDir;
478
479 // -------------------------------------------------------------------------
480 // expand_path tests
481 // -------------------------------------------------------------------------
482
483 #[test]
484 fn test_expand_path_with_tilde_prefix() {
485 let result = expand_path("~/.txgate/config.toml");
486 assert!(result.is_ok());
487
488 let path = result.expect("should succeed");
489 let home = dirs::home_dir().expect("home dir should exist");
490 assert_eq!(path, home.join(".txgate/config.toml"));
491 }
492
493 #[test]
494 fn test_expand_path_with_tilde_only() {
495 let result = expand_path("~");
496 assert!(result.is_ok());
497
498 let path = result.expect("should succeed");
499 let home = dirs::home_dir().expect("home dir should exist");
500 assert_eq!(path, home);
501 }
502
503 #[test]
504 fn test_expand_path_with_absolute_path() {
505 let result = expand_path("/etc/txgate/config.toml");
506 assert!(result.is_ok());
507
508 let path = result.expect("should succeed");
509 assert_eq!(path, PathBuf::from("/etc/txgate/config.toml"));
510 }
511
512 #[test]
513 fn test_expand_path_with_relative_path() {
514 let result = expand_path("txgate/config.toml");
515 assert!(result.is_ok());
516
517 let path = result.expect("should succeed");
518 assert_eq!(path, PathBuf::from("txgate/config.toml"));
519 }
520
521 #[test]
522 fn test_expand_path_with_embedded_tilde() {
523 // Tilde in middle of path should not be expanded
524 let result = expand_path("/path/to/~/config.toml");
525 assert!(result.is_ok());
526
527 let path = result.expect("should succeed");
528 assert_eq!(path, PathBuf::from("/path/to/~/config.toml"));
529 }
530
531 // -------------------------------------------------------------------------
532 // default_base_dir tests
533 // -------------------------------------------------------------------------
534
535 #[test]
536 fn test_default_base_dir() {
537 let result = default_base_dir();
538 assert!(result.is_ok());
539
540 let path = result.expect("should succeed");
541 let home = dirs::home_dir().expect("home dir should exist");
542 assert_eq!(path, home.join(".txgate"));
543 }
544
545 // -------------------------------------------------------------------------
546 // ConfigLoader::new tests
547 // -------------------------------------------------------------------------
548
549 #[test]
550 fn test_config_loader_new() {
551 let result = ConfigLoader::new();
552 assert!(result.is_ok());
553
554 let loader = result.expect("should succeed");
555 let expected_base = default_base_dir().expect("should get default base dir");
556 assert_eq!(loader.base_dir(), expected_base);
557 }
558
559 // -------------------------------------------------------------------------
560 // ConfigLoader::with_base_dir tests
561 // -------------------------------------------------------------------------
562
563 #[test]
564 fn test_config_loader_with_base_dir() {
565 let custom_path = PathBuf::from("/custom/txgate");
566 let loader = ConfigLoader::with_base_dir(custom_path.clone());
567 assert_eq!(loader.base_dir(), custom_path);
568 }
569
570 // -------------------------------------------------------------------------
571 // ConfigLoader::config_path tests
572 // -------------------------------------------------------------------------
573
574 #[test]
575 fn test_config_loader_config_path() {
576 let base = PathBuf::from("/test/base");
577 let loader = ConfigLoader::with_base_dir(base);
578 assert_eq!(
579 loader.config_path(),
580 PathBuf::from("/test/base/config.toml")
581 );
582 }
583
584 // -------------------------------------------------------------------------
585 // ConfigLoader::exists tests
586 // -------------------------------------------------------------------------
587
588 #[test]
589 fn test_config_loader_exists_false() {
590 let temp_dir = TempDir::new().expect("failed to create temp dir");
591 let loader = ConfigLoader::with_base_dir(temp_dir.path().to_path_buf());
592
593 assert!(!loader.exists());
594 }
595
596 #[test]
597 fn test_config_loader_exists_true() {
598 let temp_dir = TempDir::new().expect("failed to create temp dir");
599 let config_path = temp_dir.path().join("config.toml");
600 fs::write(&config_path, "# test config").expect("failed to write test file");
601
602 let loader = ConfigLoader::with_base_dir(temp_dir.path().to_path_buf());
603 assert!(loader.exists());
604 }
605
606 // -------------------------------------------------------------------------
607 // ConfigLoader::load tests
608 // -------------------------------------------------------------------------
609
610 #[test]
611 fn test_load_with_missing_file_returns_defaults() {
612 let temp_dir = TempDir::new().expect("failed to create temp dir");
613 let loader = ConfigLoader::with_base_dir(temp_dir.path().to_path_buf());
614
615 let result = loader.load();
616 assert!(result.is_ok());
617
618 let config = result.expect("should succeed");
619 assert_eq!(config, Config::default());
620 }
621
622 #[test]
623 fn test_load_with_valid_toml() {
624 let temp_dir = TempDir::new().expect("failed to create temp dir");
625 let config_path = temp_dir.path().join("config.toml");
626
627 let toml_content = r#"
628[server]
629socket_path = "/custom/socket.sock"
630timeout_secs = 60
631
632[keys]
633directory = "/custom/keys"
634default_key = "production"
635
636[policy]
637whitelist_enabled = true
638whitelist = ["0xABC"]
639"#;
640 fs::write(&config_path, toml_content).expect("failed to write test file");
641
642 let loader = ConfigLoader::with_base_dir(temp_dir.path().to_path_buf());
643 let result = loader.load();
644 assert!(result.is_ok());
645
646 let config = result.expect("should succeed");
647 assert_eq!(config.server.socket_path, "/custom/socket.sock");
648 assert_eq!(config.server.timeout_secs, 60);
649 assert_eq!(config.keys.directory, "/custom/keys");
650 assert_eq!(config.keys.default_key, "production");
651 assert!(config.policy.whitelist_enabled);
652 assert_eq!(config.policy.whitelist, vec!["0xABC"]);
653 }
654
655 #[test]
656 fn test_load_with_partial_toml_uses_defaults() {
657 let temp_dir = TempDir::new().expect("failed to create temp dir");
658 let config_path = temp_dir.path().join("config.toml");
659
660 let toml_content = r#"
661[server]
662timeout_secs = 120
663"#;
664 fs::write(&config_path, toml_content).expect("failed to write test file");
665
666 let loader = ConfigLoader::with_base_dir(temp_dir.path().to_path_buf());
667 let result = loader.load();
668 assert!(result.is_ok());
669
670 let config = result.expect("should succeed");
671 // Specified value
672 assert_eq!(config.server.timeout_secs, 120);
673 // Default values
674 assert_eq!(config.server.socket_path, "~/.txgate/txgate.sock");
675 assert_eq!(config.keys.directory, "~/.txgate/keys");
676 assert_eq!(config.keys.default_key, "default");
677 }
678
679 #[test]
680 fn test_load_with_invalid_toml_returns_parse_error() {
681 let temp_dir = TempDir::new().expect("failed to create temp dir");
682 let config_path = temp_dir.path().join("config.toml");
683
684 let invalid_toml = "this is not valid toml [[[";
685 fs::write(&config_path, invalid_toml).expect("failed to write test file");
686
687 let loader = ConfigLoader::with_base_dir(temp_dir.path().to_path_buf());
688 let result = loader.load();
689 assert!(result.is_err());
690
691 let err = result.expect_err("should fail");
692 assert!(matches!(err, ConfigError::ParseFailed { .. }));
693 }
694
695 #[test]
696 fn test_load_with_empty_file_returns_defaults() {
697 let temp_dir = TempDir::new().expect("failed to create temp dir");
698 let config_path = temp_dir.path().join("config.toml");
699
700 fs::write(&config_path, "").expect("failed to write test file");
701
702 let loader = ConfigLoader::with_base_dir(temp_dir.path().to_path_buf());
703 let result = loader.load();
704 assert!(result.is_ok());
705
706 let config = result.expect("should succeed");
707 assert_eq!(config, Config::default());
708 }
709
710 // -------------------------------------------------------------------------
711 // ConfigLoader::load_required tests
712 // -------------------------------------------------------------------------
713
714 #[test]
715 fn test_load_required_with_missing_file_returns_error() {
716 let temp_dir = TempDir::new().expect("failed to create temp dir");
717 let loader = ConfigLoader::with_base_dir(temp_dir.path().to_path_buf());
718
719 let result = loader.load_required();
720 assert!(result.is_err());
721
722 let err = result.expect_err("should fail");
723 assert!(matches!(err, ConfigError::FileNotFound { .. }));
724 }
725
726 #[test]
727 fn test_load_required_with_existing_file() {
728 let temp_dir = TempDir::new().expect("failed to create temp dir");
729 let config_path = temp_dir.path().join("config.toml");
730
731 fs::write(&config_path, "[server]\ntimeout_secs = 45").expect("failed to write test file");
732
733 let loader = ConfigLoader::with_base_dir(temp_dir.path().to_path_buf());
734 let result = loader.load_required();
735 assert!(result.is_ok());
736
737 let config = result.expect("should succeed");
738 assert_eq!(config.server.timeout_secs, 45);
739 }
740
741 // -------------------------------------------------------------------------
742 // ConfigLoader::save tests
743 // -------------------------------------------------------------------------
744
745 #[test]
746 fn test_save_creates_directory_and_file() {
747 let temp_dir = TempDir::new().expect("failed to create temp dir");
748 let base_dir = temp_dir.path().join("nested/txgate");
749 let loader = ConfigLoader::with_base_dir(base_dir.clone());
750
751 let config = Config::builder()
752 .socket_path("/custom/socket.sock")
753 .timeout_secs(90)
754 .build();
755
756 let result = loader.save(&config);
757 assert!(result.is_ok());
758
759 // Verify file was created
760 assert!(loader.exists());
761
762 // Verify content
763 let content = fs::read_to_string(loader.config_path()).expect("failed to read file");
764 assert!(content.contains("socket_path = \"/custom/socket.sock\""));
765 assert!(content.contains("timeout_secs = 90"));
766 }
767
768 #[test]
769 fn test_save_and_load_roundtrip() {
770 let temp_dir = TempDir::new().expect("failed to create temp dir");
771 let loader = ConfigLoader::with_base_dir(temp_dir.path().to_path_buf());
772
773 let original = Config::builder()
774 .socket_path("/test/socket.sock")
775 .timeout_secs(75)
776 .keys_directory("/test/keys")
777 .default_key("test-key")
778 .build();
779
780 loader.save(&original).expect("save should succeed");
781
782 let loaded = loader.load().expect("load should succeed");
783 assert_eq!(original, loaded);
784 }
785
786 // -------------------------------------------------------------------------
787 // ConfigLoader::write_default tests
788 // -------------------------------------------------------------------------
789
790 #[test]
791 fn test_write_default_creates_valid_config() {
792 let temp_dir = TempDir::new().expect("failed to create temp dir");
793 let loader = ConfigLoader::with_base_dir(temp_dir.path().to_path_buf());
794
795 let result = loader.write_default();
796 assert!(result.is_ok());
797
798 // Verify file exists
799 assert!(loader.exists());
800
801 // Verify it can be loaded and matches defaults
802 let loaded = loader.load().expect("load should succeed");
803 assert_eq!(loaded.server.socket_path, "~/.txgate/txgate.sock");
804 assert_eq!(loaded.server.timeout_secs, 30);
805 assert_eq!(loaded.keys.directory, "~/.txgate/keys");
806 assert_eq!(loaded.keys.default_key, "default");
807 }
808
809 #[test]
810 fn test_write_default_creates_nested_directory() {
811 let temp_dir = TempDir::new().expect("failed to create temp dir");
812 let base_dir = temp_dir.path().join("deeply/nested/txgate/dir");
813 let loader = ConfigLoader::with_base_dir(base_dir.clone());
814
815 let result = loader.write_default();
816 assert!(result.is_ok());
817
818 assert!(base_dir.exists());
819 assert!(loader.exists());
820 }
821
822 // -------------------------------------------------------------------------
823 // load_config tests
824 // -------------------------------------------------------------------------
825
826 #[test]
827 fn test_load_config_function() {
828 // This test just verifies the function doesn't panic
829 // Actual file loading depends on system state
830 let result = load_config();
831 // Should succeed even if file doesn't exist (returns defaults)
832 assert!(result.is_ok());
833 }
834
835 // -------------------------------------------------------------------------
836 // load_config_with_expanded_paths tests
837 // -------------------------------------------------------------------------
838
839 #[test]
840 fn test_load_config_with_expanded_paths() {
841 let result = load_config_with_expanded_paths();
842 assert!(result.is_ok());
843
844 let config = result.expect("should succeed");
845
846 // Socket path should be expanded (not start with ~)
847 assert!(!config.server.socket_path.starts_with('~'));
848
849 // Keys directory should be expanded (not start with ~)
850 assert!(!config.keys.directory.starts_with('~'));
851
852 // They should contain the home directory
853 let home = dirs::home_dir().expect("home dir should exist");
854 assert!(config
855 .server
856 .socket_path
857 .starts_with(home.to_string_lossy().as_ref()));
858 assert!(config
859 .keys
860 .directory
861 .starts_with(home.to_string_lossy().as_ref()));
862 }
863
864 // -------------------------------------------------------------------------
865 // Edge case tests
866 // -------------------------------------------------------------------------
867
868 #[test]
869 fn test_config_loader_clone() {
870 let loader = ConfigLoader::with_base_dir(PathBuf::from("/test/path"));
871 let cloned = loader.clone();
872 assert_eq!(loader.base_dir(), cloned.base_dir());
873 }
874
875 #[test]
876 fn test_config_loader_debug() {
877 let loader = ConfigLoader::with_base_dir(PathBuf::from("/test/path"));
878 let debug_str = format!("{loader:?}");
879 assert!(debug_str.contains("ConfigLoader"));
880 assert!(debug_str.contains("/test/path"));
881 }
882
883 #[test]
884 fn test_save_overwrites_existing_file() {
885 let temp_dir = TempDir::new().expect("failed to create temp dir");
886 let loader = ConfigLoader::with_base_dir(temp_dir.path().to_path_buf());
887
888 // Save first config
889 let config1 = Config::builder().timeout_secs(10).build();
890 loader.save(&config1).expect("first save should succeed");
891
892 // Save second config (overwrite)
893 let config2 = Config::builder().timeout_secs(20).build();
894 loader.save(&config2).expect("second save should succeed");
895
896 // Load and verify it's the second config
897 let loaded = loader.load().expect("load should succeed");
898 assert_eq!(loaded.server.timeout_secs, 20);
899 }
900}