fireblocks_config/
lib.rs

1#![doc = include_str!("../README.md")]
2mod config;
3mod error;
4pub use error::Error;
5use serde::Deserialize;
6pub type Result<T> = std::result::Result<T, error::Error>;
7use clap::ValueEnum;
8pub use config::*;
9
10#[derive(Copy, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Default)]
11pub enum OutputFormat {
12    #[default]
13    /// Ascii Table
14    Table,
15    /// Tab separated
16    Tsv,
17    Json,
18}
19
20#[cfg(test)]
21mod tests {
22    use {
23        super::*,
24        std::{path::PathBuf, time::Duration},
25    };
26
27    #[test]
28    fn test_signer_config_default() -> anyhow::Result<()> {
29        let cfg = super::Signer::default();
30        assert!(!cfg.broadcast);
31        assert_eq!(cfg.vault.len(), 0);
32        Ok(())
33    }
34
35    #[ignore]
36    #[test]
37    fn test_gpg_config() -> anyhow::Result<()> {
38        let b = "examples/config-gpg.toml";
39        let cfg = FireblocksConfig::new(b, &[])?;
40        cfg.get_key()?;
41        Ok(())
42    }
43
44    #[test]
45    fn test_config() -> anyhow::Result<()> {
46        let b = "examples/default.toml";
47        let cfg = FireblocksConfig::new(b, &[])?;
48        assert_eq!("blah", cfg.api_key);
49        assert!(cfg.secret_path.is_some());
50        if let Some(p) = cfg.secret_path.as_ref() {
51            assert_eq!(PathBuf::from("examples/test.pem"), *p);
52        }
53        assert_eq!("https://sandbox-api.fireblocks.io/v1", cfg.url);
54        assert_eq!(OutputFormat::Table, cfg.display_config.output);
55        unsafe {
56            std::env::set_var("FIREBLOCKS_SECRET", "override");
57        }
58        let cfg = FireblocksConfig::new(b, &[])?;
59        assert!(cfg.secret.is_some());
60        assert_eq!(String::from("override").as_bytes(), cfg.get_key()?);
61        if let Some(ref k) = cfg.secret_path {
62            assert_eq!(PathBuf::from("examples/test.pem"), *k);
63        }
64
65        assert_eq!(cfg.signer.vault, "0");
66        assert!(!cfg.signer.broadcast);
67        unsafe {
68            std::env::remove_var("FIREBLOCKS_SECRET");
69        }
70        Ok(())
71    }
72
73    #[test]
74    fn test_config_override() -> anyhow::Result<()> {
75        let b = "examples/default.toml";
76        let cfg_override = "examples/override.toml";
77        let cfg = FireblocksConfig::with_overrides(b, vec![cfg_override])?;
78        assert_eq!("production", cfg.api_key);
79        assert!(cfg.secret_path.is_some());
80        if let Some(p) = cfg.secret_path.as_ref() {
81            assert_eq!(PathBuf::from("examples/test.pem"), *p);
82        }
83        assert_eq!("https://api.fireblocks.io/v1", cfg.url);
84        assert_eq!(OutputFormat::Table, cfg.display_config.output);
85        assert!(cfg.debug);
86        assert!(cfg.mainnet);
87        Ok(())
88    }
89
90    #[test]
91    fn test_embedded_key() -> anyhow::Result<()> {
92        let b = "examples/default.toml";
93        let cfg_override = "examples/embedded.toml";
94        let cfg = FireblocksConfig::new(b, &[cfg_override])?;
95        assert!(cfg.secret.is_some());
96        let secret = cfg.secret.unwrap();
97        assert_eq!(String::from("i am a secret").as_bytes(), secret.as_bytes());
98        Ok(())
99    }
100
101    #[test]
102    fn test_duration_parsing() -> anyhow::Result<()> {
103        let b = "examples/default.toml";
104        let cfg = FireblocksConfig::new(b, &[])?;
105
106        // Verify that string values in TOML are parsed as Duration
107        assert_eq!(cfg.signer.poll_timeout, Duration::from_secs(120));
108        assert_eq!(cfg.signer.poll_interval, Duration::from_secs(5));
109
110        Ok(())
111    }
112
113    #[test]
114    fn test_extra_config() -> anyhow::Result<()> {
115        let b = "examples/default.toml";
116        let cfg = FireblocksConfig::new(b, &[])?;
117
118        // Test extra configuration values from [extra] section
119        assert_eq!(cfg.get_extra::<String, _>("rpc_url")?, "https://rpc.com");
120        assert!(!cfg.get_extra::<bool, _>("fail_fast")?);
121        assert_eq!(cfg.get_extra::<i64, _>("timeout")?, 40);
122
123        // Test with String key (AsRef<str> flexibility)
124        let key = String::from("rpc_url");
125        assert_eq!(cfg.get_extra::<String, _>(&key)?, "https://rpc.com");
126
127        // Test non-existent key returns NotPresent error
128        let result = cfg.get_extra::<String, _>("non_existent");
129        assert!(result.is_err());
130        if let Err(Error::NotPresent { key }) = result {
131            assert_eq!(key, "non_existent");
132        } else {
133            panic!("Expected NotPresent error");
134        }
135
136        // Test has_extra with different key types
137        assert!(cfg.has_extra("rpc_url"));
138        assert!(cfg.has_extra(String::from("fail_fast")));
139        assert!(cfg.has_extra("timeout"));
140        assert!(!cfg.has_extra("non_existent"));
141
142        // Test get_extra_duration
143        let timeout_duration = cfg.get_extra_duration("timeout")?;
144        assert_eq!(timeout_duration, Duration::from_secs(40));
145
146        // Test get_extra_duration with non-existent key
147        let result = cfg.get_extra_duration("non_existent");
148        assert!(result.is_err());
149
150        Ok(())
151    }
152
153    #[test]
154    fn test_duration_defaults() -> anyhow::Result<()> {
155        let b = "examples/notime.toml";
156        let cfg = FireblocksConfig::new(b, &[])?;
157        // Verify that string values in TOML are parsed as Duration
158        assert_eq!(cfg.signer.poll_timeout, default_poll_timeout());
159        assert_eq!(cfg.signer.poll_interval, default_poll_interval());
160        Ok(())
161    }
162
163    #[test]
164    fn test_tilde() -> anyhow::Result<()> {
165        let expanded = format!("{}", expand_tilde("~/blah/default.toml").display());
166        assert!(expanded.contains("/home"));
167        Ok(())
168    }
169
170    #[test]
171    fn test_xdg_init() {
172        // This test just ensures the XDG methods compile and can be called
173        // In a real environment, it would try to load from ~/.config/fireblocks/
174        match FireblocksConfig::init() {
175            Ok(_) => {
176                // Config loaded successfully from XDG directory
177            }
178            Err(_) => {
179                // Expected if no config exists in XDG directory
180                // This is fine for the compilation test
181            }
182        }
183
184        // Test with &str slice
185        match FireblocksConfig::init_with_profiles(&["test", "production"]) {
186            Ok(_) => {
187                // Config loaded successfully
188            }
189            Err(_) => {
190                // Expected if no config exists
191            }
192        }
193
194        // Test with Vec<String> to verify flexibility
195        let profiles: Vec<String> = vec!["staging".to_string(), "production".to_string()];
196        match FireblocksConfig::init_with_profiles(&profiles) {
197            Ok(_) => {
198                // Config loaded successfully
199            }
200            Err(_) => {
201                // Expected if no config exists
202            }
203        }
204    }
205}