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