openzeppelin_relayer/config/
server_config.rs

1/// Configuration for the server, including network and rate limiting settings.
2use std::{env, str::FromStr};
3use strum::Display;
4
5use crate::{constants::MINIMUM_SECRET_VALUE_LENGTH, models::SecretString};
6
7#[derive(Debug, Clone, PartialEq, Eq, Display)]
8pub enum RepositoryStorageType {
9    InMemory,
10    Redis,
11}
12
13impl FromStr for RepositoryStorageType {
14    type Err = String;
15
16    fn from_str(s: &str) -> Result<Self, Self::Err> {
17        match s.to_lowercase().as_str() {
18            "inmemory" | "in_memory" => Ok(Self::InMemory),
19            "redis" => Ok(Self::Redis),
20            _ => Err(format!("Invalid repository storage type: {}", s)),
21        }
22    }
23}
24
25#[derive(Debug, Clone)]
26pub struct ServerConfig {
27    /// The host address the server will bind to.
28    pub host: String,
29    /// The port number the server will listen on.
30    pub port: u16,
31    /// The URL for the Redis instance.
32    pub redis_url: String,
33    /// The file path to the server's configuration file.
34    pub config_file_path: String,
35    /// The API key used for authentication.
36    pub api_key: SecretString,
37    /// The number of requests allowed per second.
38    pub rate_limit_requests_per_second: u64,
39    /// The maximum burst size for rate limiting.
40    pub rate_limit_burst_size: u32,
41    /// The port number for exposing metrics.
42    pub metrics_port: u16,
43    /// Enable Swagger UI.
44    pub enable_swagger: bool,
45    /// The number of seconds to wait for a Redis connection.
46    pub redis_connection_timeout_ms: u64,
47    /// The prefix for the Redis key.
48    pub redis_key_prefix: String,
49    /// The number of milliseconds to wait for an RPC response.
50    pub rpc_timeout_ms: u64,
51    /// Maximum number of retry attempts for provider operations.
52    pub provider_max_retries: u8,
53    /// Base delay between retry attempts (milliseconds).
54    pub provider_retry_base_delay_ms: u64,
55    /// Maximum delay between retry attempts (milliseconds).
56    pub provider_retry_max_delay_ms: u64,
57    /// Maximum number of failovers (switching to different providers).
58    pub provider_max_failovers: u8,
59    /// The type of repository storage to use.
60    pub repository_storage_type: RepositoryStorageType,
61    /// Flag to force config file processing.
62    pub reset_storage_on_start: bool,
63    /// The encryption key for the storage.
64    pub storage_encryption_key: Option<SecretString>,
65    /// Transaction expiration time in hours for transactions in final states.
66    pub transaction_expiration_hours: u64,
67}
68
69impl ServerConfig {
70    /// Creates a new `ServerConfig` instance from environment variables.
71    ///
72    /// # Panics
73    ///
74    /// This function will panic if the `REDIS_URL` or `API_KEY` environment
75    /// variables are not set, as they are required for the server to function.
76    ///
77    /// # Defaults
78    ///
79    /// - `HOST` defaults to `"0.0.0.0"`.
80    /// - `APP_PORT` defaults to `8080`.
81    /// - `CONFIG_DIR` defaults to `"config/config.json"`.
82    /// - `RATE_LIMIT_REQUESTS_PER_SECOND` defaults to `100`.
83    /// - `RATE_LIMIT_BURST_SIZE` defaults to `300`.
84    /// - `METRICS_PORT` defaults to `8081`.
85    /// - `PROVIDER_MAX_RETRIES` defaults to `3`.
86    /// - `PROVIDER_RETRY_BASE_DELAY_MS` defaults to `100`.
87    /// - `PROVIDER_RETRY_MAX_DELAY_MS` defaults to `2000`.
88    /// - `PROVIDER_MAX_FAILOVERS` defaults to `3`.
89    /// - `REPOSITORY_STORAGE_TYPE` defaults to `"in_memory"`.
90    /// - `TRANSACTION_EXPIRATION_HOURS` defaults to `4`.
91    pub fn from_env() -> Self {
92        Self {
93            host: Self::get_host(),
94            port: Self::get_port(),
95            redis_url: Self::get_redis_url(), // Uses panicking version as required
96            config_file_path: Self::get_config_file_path(),
97            api_key: Self::get_api_key(), // Uses panicking version as required
98            rate_limit_requests_per_second: Self::get_rate_limit_requests_per_second(),
99            rate_limit_burst_size: Self::get_rate_limit_burst_size(),
100            metrics_port: Self::get_metrics_port(),
101            enable_swagger: Self::get_enable_swagger(),
102            redis_connection_timeout_ms: Self::get_redis_connection_timeout_ms(),
103            redis_key_prefix: Self::get_redis_key_prefix(),
104            rpc_timeout_ms: Self::get_rpc_timeout_ms(),
105            provider_max_retries: Self::get_provider_max_retries(),
106            provider_retry_base_delay_ms: Self::get_provider_retry_base_delay_ms(),
107            provider_retry_max_delay_ms: Self::get_provider_retry_max_delay_ms(),
108            provider_max_failovers: Self::get_provider_max_failovers(),
109            repository_storage_type: Self::get_repository_storage_type(),
110            reset_storage_on_start: Self::get_reset_storage_on_start(),
111            storage_encryption_key: Self::get_storage_encryption_key(),
112            transaction_expiration_hours: Self::get_transaction_expiration_hours(),
113        }
114    }
115
116    // Individual getter methods for each configuration field
117
118    /// Gets the host from environment variable or default
119    pub fn get_host() -> String {
120        env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string())
121    }
122
123    /// Gets the port from environment variable or default
124    pub fn get_port() -> u16 {
125        env::var("APP_PORT")
126            .unwrap_or_else(|_| "8080".to_string())
127            .parse()
128            .unwrap_or(8080)
129    }
130
131    /// Gets the Redis URL from environment variable (panics if not set)
132    pub fn get_redis_url() -> String {
133        env::var("REDIS_URL").expect("REDIS_URL must be set")
134    }
135
136    /// Gets the Redis URL from environment variable or returns None if not set
137    pub fn get_redis_url_optional() -> Option<String> {
138        env::var("REDIS_URL").ok()
139    }
140
141    /// Gets the config file path from environment variables or default
142    pub fn get_config_file_path() -> String {
143        let conf_dir = if env::var("IN_DOCKER")
144            .map(|val| val == "true")
145            .unwrap_or(false)
146        {
147            "config/".to_string()
148        } else {
149            env::var("CONFIG_DIR").unwrap_or_else(|_| "./config".to_string())
150        };
151
152        let conf_dir = format!("{}/", conf_dir.trim_end_matches('/'));
153        let config_file_name =
154            env::var("CONFIG_FILE_NAME").unwrap_or_else(|_| "config.json".to_string());
155
156        format!("{}{}", conf_dir, config_file_name)
157    }
158
159    /// Gets the API key from environment variable (panics if not set or too short)
160    pub fn get_api_key() -> SecretString {
161        let api_key = SecretString::new(&env::var("API_KEY").expect("API_KEY must be set"));
162
163        if !api_key.has_minimum_length(MINIMUM_SECRET_VALUE_LENGTH) {
164            panic!(
165                "Security error: API_KEY must be at least {} characters long",
166                MINIMUM_SECRET_VALUE_LENGTH
167            );
168        }
169
170        api_key
171    }
172
173    /// Gets the API key from environment variable or returns None if not set or invalid
174    pub fn get_api_key_optional() -> Option<SecretString> {
175        env::var("API_KEY")
176            .ok()
177            .map(|key| SecretString::new(&key))
178            .filter(|key| key.has_minimum_length(MINIMUM_SECRET_VALUE_LENGTH))
179    }
180
181    /// Gets the rate limit requests per second from environment variable or default
182    pub fn get_rate_limit_requests_per_second() -> u64 {
183        env::var("RATE_LIMIT_REQUESTS_PER_SECOND")
184            .unwrap_or_else(|_| "100".to_string())
185            .parse()
186            .unwrap_or(100)
187    }
188
189    /// Gets the rate limit burst size from environment variable or default
190    pub fn get_rate_limit_burst_size() -> u32 {
191        env::var("RATE_LIMIT_BURST_SIZE")
192            .unwrap_or_else(|_| "300".to_string())
193            .parse()
194            .unwrap_or(300)
195    }
196
197    /// Gets the metrics port from environment variable or default
198    pub fn get_metrics_port() -> u16 {
199        env::var("METRICS_PORT")
200            .unwrap_or_else(|_| "8081".to_string())
201            .parse()
202            .unwrap_or(8081)
203    }
204
205    /// Gets the enable swagger setting from environment variable or default
206    pub fn get_enable_swagger() -> bool {
207        env::var("ENABLE_SWAGGER")
208            .map(|v| v.to_lowercase() == "true")
209            .unwrap_or(false)
210    }
211
212    /// Gets the Redis connection timeout from environment variable or default
213    pub fn get_redis_connection_timeout_ms() -> u64 {
214        env::var("REDIS_CONNECTION_TIMEOUT_MS")
215            .unwrap_or_else(|_| "10000".to_string())
216            .parse()
217            .unwrap_or(10000)
218    }
219
220    /// Gets the Redis key prefix from environment variable or default
221    pub fn get_redis_key_prefix() -> String {
222        env::var("REDIS_KEY_PREFIX").unwrap_or_else(|_| "oz-relayer".to_string())
223    }
224
225    /// Gets the RPC timeout from environment variable or default
226    pub fn get_rpc_timeout_ms() -> u64 {
227        env::var("RPC_TIMEOUT_MS")
228            .unwrap_or_else(|_| "10000".to_string())
229            .parse()
230            .unwrap_or(10000)
231    }
232
233    /// Gets the provider max retries from environment variable or default
234    pub fn get_provider_max_retries() -> u8 {
235        env::var("PROVIDER_MAX_RETRIES")
236            .unwrap_or_else(|_| "3".to_string())
237            .parse()
238            .unwrap_or(3)
239    }
240
241    /// Gets the provider retry base delay from environment variable or default
242    pub fn get_provider_retry_base_delay_ms() -> u64 {
243        env::var("PROVIDER_RETRY_BASE_DELAY_MS")
244            .unwrap_or_else(|_| "100".to_string())
245            .parse()
246            .unwrap_or(100)
247    }
248
249    /// Gets the provider retry max delay from environment variable or default
250    pub fn get_provider_retry_max_delay_ms() -> u64 {
251        env::var("PROVIDER_RETRY_MAX_DELAY_MS")
252            .unwrap_or_else(|_| "2000".to_string())
253            .parse()
254            .unwrap_or(2000)
255    }
256
257    /// Gets the provider max failovers from environment variable or default
258    pub fn get_provider_max_failovers() -> u8 {
259        env::var("PROVIDER_MAX_FAILOVERS")
260            .unwrap_or_else(|_| "3".to_string())
261            .parse()
262            .unwrap_or(3)
263    }
264
265    /// Gets the repository storage type from environment variable or default
266    pub fn get_repository_storage_type() -> RepositoryStorageType {
267        env::var("REPOSITORY_STORAGE_TYPE")
268            .unwrap_or_else(|_| "in_memory".to_string())
269            .parse()
270            .unwrap_or(RepositoryStorageType::InMemory)
271    }
272
273    /// Gets the reset storage on start setting from environment variable or default
274    pub fn get_reset_storage_on_start() -> bool {
275        env::var("RESET_STORAGE_ON_START")
276            .map(|v| v.to_lowercase() == "true")
277            .unwrap_or(false)
278    }
279
280    /// Gets the storage encryption key from environment variable or None
281    pub fn get_storage_encryption_key() -> Option<SecretString> {
282        env::var("STORAGE_ENCRYPTION_KEY")
283            .map(|v| SecretString::new(&v))
284            .ok()
285    }
286
287    /// Gets the transaction expiration hours from environment variable or default
288    pub fn get_transaction_expiration_hours() -> u64 {
289        env::var("TRANSACTION_EXPIRATION_HOURS")
290            .unwrap_or_else(|_| "4".to_string())
291            .parse()
292            .unwrap_or(4)
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use lazy_static::lazy_static;
300    use std::env;
301    use std::sync::Mutex;
302
303    // Use a mutex to ensure tests don't run in parallel when modifying env vars
304    lazy_static! {
305        static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
306    }
307
308    fn setup() {
309        // Clear all environment variables first
310        env::remove_var("HOST");
311        env::remove_var("APP_PORT");
312        env::remove_var("REDIS_URL");
313        env::remove_var("CONFIG_DIR");
314        env::remove_var("CONFIG_FILE_NAME");
315        env::remove_var("CONFIG_FILE_PATH");
316        env::remove_var("API_KEY");
317        env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
318        env::remove_var("RATE_LIMIT_BURST_SIZE");
319        env::remove_var("METRICS_PORT");
320        env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
321        env::remove_var("RPC_TIMEOUT_MS");
322        env::remove_var("PROVIDER_MAX_RETRIES");
323        env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
324        env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
325        env::remove_var("PROVIDER_MAX_FAILOVERS");
326        env::remove_var("REPOSITORY_STORAGE_TYPE");
327        env::remove_var("RESET_STORAGE_ON_START");
328        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
329        // Set required variables for most tests
330        env::set_var("REDIS_URL", "redis://localhost:6379");
331        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
332        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
333    }
334
335    #[test]
336    fn test_default_values() {
337        let _lock = match ENV_MUTEX.lock() {
338            Ok(guard) => guard,
339            Err(poisoned) => poisoned.into_inner(),
340        };
341        setup();
342
343        let config = ServerConfig::from_env();
344
345        assert_eq!(config.host, "0.0.0.0");
346        assert_eq!(config.port, 8080);
347        assert_eq!(config.redis_url, "redis://localhost:6379");
348        assert_eq!(config.config_file_path, "./config/config.json");
349        assert_eq!(
350            config.api_key,
351            SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
352        );
353        assert_eq!(config.rate_limit_requests_per_second, 100);
354        assert_eq!(config.rate_limit_burst_size, 300);
355        assert_eq!(config.metrics_port, 8081);
356        assert_eq!(config.redis_connection_timeout_ms, 5000);
357        assert_eq!(config.rpc_timeout_ms, 10000);
358        assert_eq!(config.provider_max_retries, 3);
359        assert_eq!(config.provider_retry_base_delay_ms, 100);
360        assert_eq!(config.provider_retry_max_delay_ms, 2000);
361        assert_eq!(config.provider_max_failovers, 3);
362        assert_eq!(
363            config.repository_storage_type,
364            RepositoryStorageType::InMemory
365        );
366        assert!(!config.reset_storage_on_start);
367        assert_eq!(config.transaction_expiration_hours, 4);
368    }
369
370    #[test]
371    fn test_invalid_port_values() {
372        let _lock = match ENV_MUTEX.lock() {
373            Ok(guard) => guard,
374            Err(poisoned) => poisoned.into_inner(),
375        };
376        setup();
377        env::set_var("REDIS_URL", "redis://localhost:6379");
378        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
379        env::set_var("APP_PORT", "not_a_number");
380        env::set_var("METRICS_PORT", "also_not_a_number");
381        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "invalid");
382        env::set_var("RATE_LIMIT_BURST_SIZE", "invalid");
383        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "invalid");
384        env::set_var("RPC_TIMEOUT_MS", "invalid");
385        env::set_var("PROVIDER_MAX_RETRIES", "invalid");
386        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "invalid");
387        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "invalid");
388        env::set_var("PROVIDER_MAX_FAILOVERS", "invalid");
389        env::set_var("REPOSITORY_STORAGE_TYPE", "invalid");
390        env::set_var("RESET_STORAGE_ON_START", "invalid");
391        env::set_var("TRANSACTION_EXPIRATION_HOURS", "invalid");
392        let config = ServerConfig::from_env();
393
394        // Should fall back to defaults when parsing fails
395        assert_eq!(config.port, 8080);
396        assert_eq!(config.metrics_port, 8081);
397        assert_eq!(config.rate_limit_requests_per_second, 100);
398        assert_eq!(config.rate_limit_burst_size, 300);
399        assert_eq!(config.redis_connection_timeout_ms, 10000);
400        assert_eq!(config.rpc_timeout_ms, 10000);
401        assert_eq!(config.provider_max_retries, 3);
402        assert_eq!(config.provider_retry_base_delay_ms, 100);
403        assert_eq!(config.provider_retry_max_delay_ms, 2000);
404        assert_eq!(config.provider_max_failovers, 3);
405        assert_eq!(
406            config.repository_storage_type,
407            RepositoryStorageType::InMemory
408        );
409        assert!(!config.reset_storage_on_start);
410        assert_eq!(config.transaction_expiration_hours, 4);
411    }
412
413    #[test]
414    fn test_custom_values() {
415        let _lock = match ENV_MUTEX.lock() {
416            Ok(guard) => guard,
417            Err(poisoned) => poisoned.into_inner(),
418        };
419        setup();
420
421        env::set_var("HOST", "127.0.0.1");
422        env::set_var("APP_PORT", "9090");
423        env::set_var("REDIS_URL", "redis://custom:6379");
424        env::set_var("CONFIG_DIR", "custom");
425        env::set_var("CONFIG_FILE_NAME", "path.json");
426        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
427        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "200");
428        env::set_var("RATE_LIMIT_BURST_SIZE", "500");
429        env::set_var("METRICS_PORT", "9091");
430        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "10000");
431        env::set_var("RPC_TIMEOUT_MS", "33333");
432        env::set_var("PROVIDER_MAX_RETRIES", "5");
433        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
434        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "3000");
435        env::set_var("PROVIDER_MAX_FAILOVERS", "4");
436        env::set_var("REPOSITORY_STORAGE_TYPE", "in_memory");
437        env::set_var("RESET_STORAGE_ON_START", "true");
438        env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
439        let config = ServerConfig::from_env();
440
441        assert_eq!(config.host, "127.0.0.1");
442        assert_eq!(config.port, 9090);
443        assert_eq!(config.redis_url, "redis://custom:6379");
444        assert_eq!(config.config_file_path, "custom/path.json");
445        assert_eq!(
446            config.api_key,
447            SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
448        );
449        assert_eq!(config.rate_limit_requests_per_second, 200);
450        assert_eq!(config.rate_limit_burst_size, 500);
451        assert_eq!(config.metrics_port, 9091);
452        assert_eq!(config.redis_connection_timeout_ms, 10000);
453        assert_eq!(config.rpc_timeout_ms, 33333);
454        assert_eq!(config.provider_max_retries, 5);
455        assert_eq!(config.provider_retry_base_delay_ms, 200);
456        assert_eq!(config.provider_retry_max_delay_ms, 3000);
457        assert_eq!(config.provider_max_failovers, 4);
458        assert_eq!(
459            config.repository_storage_type,
460            RepositoryStorageType::InMemory
461        );
462        assert!(config.reset_storage_on_start);
463        assert_eq!(config.transaction_expiration_hours, 6);
464    }
465
466    #[test]
467    #[should_panic(expected = "Security error: API_KEY must be at least 32 characters long")]
468    fn test_invalid_api_key_length() {
469        let _lock = match ENV_MUTEX.lock() {
470            Ok(guard) => guard,
471            Err(poisoned) => poisoned.into_inner(),
472        };
473        setup();
474        env::set_var("REDIS_URL", "redis://localhost:6379");
475        env::set_var("API_KEY", "insufficient_length");
476        env::set_var("APP_PORT", "8080");
477        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "100");
478        env::set_var("RATE_LIMIT_BURST_SIZE", "300");
479        env::set_var("METRICS_PORT", "9091");
480        env::set_var("TRANSACTION_EXPIRATION_HOURS", "4");
481
482        let _ = ServerConfig::from_env();
483
484        panic!("Test should have panicked before reaching here");
485    }
486
487    // Tests for individual getter methods
488    #[test]
489    fn test_individual_getters_with_defaults() {
490        let _lock = match ENV_MUTEX.lock() {
491            Ok(guard) => guard,
492            Err(poisoned) => poisoned.into_inner(),
493        };
494
495        // Clear all environment variables to test defaults
496        env::remove_var("HOST");
497        env::remove_var("APP_PORT");
498        env::remove_var("REDIS_URL");
499        env::remove_var("CONFIG_DIR");
500        env::remove_var("CONFIG_FILE_NAME");
501        env::remove_var("API_KEY");
502        env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
503        env::remove_var("RATE_LIMIT_BURST_SIZE");
504        env::remove_var("METRICS_PORT");
505        env::remove_var("ENABLE_SWAGGER");
506        env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
507        env::remove_var("REDIS_KEY_PREFIX");
508        env::remove_var("RPC_TIMEOUT_MS");
509        env::remove_var("PROVIDER_MAX_RETRIES");
510        env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
511        env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
512        env::remove_var("PROVIDER_MAX_FAILOVERS");
513        env::remove_var("REPOSITORY_STORAGE_TYPE");
514        env::remove_var("RESET_STORAGE_ON_START");
515        env::remove_var("STORAGE_ENCRYPTION_KEY");
516        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
517
518        // Test individual getters with defaults
519        assert_eq!(ServerConfig::get_host(), "0.0.0.0");
520        assert_eq!(ServerConfig::get_port(), 8080);
521        assert_eq!(ServerConfig::get_redis_url_optional(), None);
522        assert_eq!(ServerConfig::get_config_file_path(), "./config/config.json");
523        assert_eq!(ServerConfig::get_api_key_optional(), None);
524        assert_eq!(ServerConfig::get_rate_limit_requests_per_second(), 100);
525        assert_eq!(ServerConfig::get_rate_limit_burst_size(), 300);
526        assert_eq!(ServerConfig::get_metrics_port(), 8081);
527        assert!(!ServerConfig::get_enable_swagger());
528        assert_eq!(ServerConfig::get_redis_connection_timeout_ms(), 10000);
529        assert_eq!(ServerConfig::get_redis_key_prefix(), "oz-relayer");
530        assert_eq!(ServerConfig::get_rpc_timeout_ms(), 10000);
531        assert_eq!(ServerConfig::get_provider_max_retries(), 3);
532        assert_eq!(ServerConfig::get_provider_retry_base_delay_ms(), 100);
533        assert_eq!(ServerConfig::get_provider_retry_max_delay_ms(), 2000);
534        assert_eq!(ServerConfig::get_provider_max_failovers(), 3);
535        assert_eq!(
536            ServerConfig::get_repository_storage_type(),
537            RepositoryStorageType::InMemory
538        );
539        assert!(!ServerConfig::get_reset_storage_on_start());
540        assert!(ServerConfig::get_storage_encryption_key().is_none());
541        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 4);
542    }
543
544    #[test]
545    fn test_individual_getters_with_custom_values() {
546        let _lock = match ENV_MUTEX.lock() {
547            Ok(guard) => guard,
548            Err(poisoned) => poisoned.into_inner(),
549        };
550
551        // Set custom values
552        env::set_var("HOST", "192.168.1.1");
553        env::set_var("APP_PORT", "9999");
554        env::set_var("REDIS_URL", "redis://custom:6379");
555        env::set_var("CONFIG_DIR", "/custom/config");
556        env::set_var("CONFIG_FILE_NAME", "custom.json");
557        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
558        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "500");
559        env::set_var("RATE_LIMIT_BURST_SIZE", "1000");
560        env::set_var("METRICS_PORT", "9999");
561        env::set_var("ENABLE_SWAGGER", "true");
562        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
563        env::set_var("REDIS_KEY_PREFIX", "custom-prefix");
564        env::set_var("RPC_TIMEOUT_MS", "15000");
565        env::set_var("PROVIDER_MAX_RETRIES", "5");
566        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
567        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "5000");
568        env::set_var("PROVIDER_MAX_FAILOVERS", "10");
569        env::set_var("REPOSITORY_STORAGE_TYPE", "redis");
570        env::set_var("RESET_STORAGE_ON_START", "true");
571        env::set_var("STORAGE_ENCRYPTION_KEY", "my-encryption-key");
572        env::set_var("TRANSACTION_EXPIRATION_HOURS", "12");
573
574        // Test individual getters with custom values
575        assert_eq!(ServerConfig::get_host(), "192.168.1.1");
576        assert_eq!(ServerConfig::get_port(), 9999);
577        assert_eq!(
578            ServerConfig::get_redis_url_optional(),
579            Some("redis://custom:6379".to_string())
580        );
581        assert_eq!(
582            ServerConfig::get_config_file_path(),
583            "/custom/config/custom.json"
584        );
585        assert!(ServerConfig::get_api_key_optional().is_some());
586        assert_eq!(ServerConfig::get_rate_limit_requests_per_second(), 500);
587        assert_eq!(ServerConfig::get_rate_limit_burst_size(), 1000);
588        assert_eq!(ServerConfig::get_metrics_port(), 9999);
589        assert!(ServerConfig::get_enable_swagger());
590        assert_eq!(ServerConfig::get_redis_connection_timeout_ms(), 5000);
591        assert_eq!(ServerConfig::get_redis_key_prefix(), "custom-prefix");
592        assert_eq!(ServerConfig::get_rpc_timeout_ms(), 15000);
593        assert_eq!(ServerConfig::get_provider_max_retries(), 5);
594        assert_eq!(ServerConfig::get_provider_retry_base_delay_ms(), 200);
595        assert_eq!(ServerConfig::get_provider_retry_max_delay_ms(), 5000);
596        assert_eq!(ServerConfig::get_provider_max_failovers(), 10);
597        assert_eq!(
598            ServerConfig::get_repository_storage_type(),
599            RepositoryStorageType::Redis
600        );
601        assert!(ServerConfig::get_reset_storage_on_start());
602        assert!(ServerConfig::get_storage_encryption_key().is_some());
603        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 12);
604    }
605
606    #[test]
607    #[should_panic(expected = "REDIS_URL must be set")]
608    fn test_get_redis_url_panics_when_not_set() {
609        let _lock = match ENV_MUTEX.lock() {
610            Ok(guard) => guard,
611            Err(poisoned) => poisoned.into_inner(),
612        };
613
614        env::remove_var("REDIS_URL");
615        let _ = ServerConfig::get_redis_url();
616    }
617
618    #[test]
619    #[should_panic(expected = "API_KEY must be set")]
620    fn test_get_api_key_panics_when_not_set() {
621        let _lock = match ENV_MUTEX.lock() {
622            Ok(guard) => guard,
623            Err(poisoned) => poisoned.into_inner(),
624        };
625
626        env::remove_var("API_KEY");
627        let _ = ServerConfig::get_api_key();
628    }
629
630    #[test]
631    fn test_optional_getters_return_none_safely() {
632        let _lock = match ENV_MUTEX.lock() {
633            Ok(guard) => guard,
634            Err(poisoned) => poisoned.into_inner(),
635        };
636
637        env::remove_var("REDIS_URL");
638        env::remove_var("API_KEY");
639        env::remove_var("STORAGE_ENCRYPTION_KEY");
640
641        assert!(ServerConfig::get_redis_url_optional().is_none());
642        assert!(ServerConfig::get_api_key_optional().is_none());
643        assert!(ServerConfig::get_storage_encryption_key().is_none());
644    }
645
646    #[test]
647    fn test_refactored_from_env_equivalence() {
648        let _lock = match ENV_MUTEX.lock() {
649            Ok(guard) => guard,
650            Err(poisoned) => poisoned.into_inner(),
651        };
652        setup();
653
654        // Set custom values to test both default and custom paths
655        env::set_var("HOST", "custom-host");
656        env::set_var("APP_PORT", "7777");
657        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "250");
658        env::set_var("METRICS_PORT", "7778");
659        env::set_var("ENABLE_SWAGGER", "true");
660        env::set_var("PROVIDER_MAX_RETRIES", "7");
661        env::set_var("TRANSACTION_EXPIRATION_HOURS", "8");
662
663        let config = ServerConfig::from_env();
664
665        // Verify the refactored from_env() produces the same results as individual getters
666        assert_eq!(config.host, ServerConfig::get_host());
667        assert_eq!(config.port, ServerConfig::get_port());
668        assert_eq!(config.redis_url, ServerConfig::get_redis_url());
669        assert_eq!(
670            config.config_file_path,
671            ServerConfig::get_config_file_path()
672        );
673        assert_eq!(config.api_key, ServerConfig::get_api_key());
674        assert_eq!(
675            config.rate_limit_requests_per_second,
676            ServerConfig::get_rate_limit_requests_per_second()
677        );
678        assert_eq!(
679            config.rate_limit_burst_size,
680            ServerConfig::get_rate_limit_burst_size()
681        );
682        assert_eq!(config.metrics_port, ServerConfig::get_metrics_port());
683        assert_eq!(config.enable_swagger, ServerConfig::get_enable_swagger());
684        assert_eq!(
685            config.redis_connection_timeout_ms,
686            ServerConfig::get_redis_connection_timeout_ms()
687        );
688        assert_eq!(
689            config.redis_key_prefix,
690            ServerConfig::get_redis_key_prefix()
691        );
692        assert_eq!(config.rpc_timeout_ms, ServerConfig::get_rpc_timeout_ms());
693        assert_eq!(
694            config.provider_max_retries,
695            ServerConfig::get_provider_max_retries()
696        );
697        assert_eq!(
698            config.provider_retry_base_delay_ms,
699            ServerConfig::get_provider_retry_base_delay_ms()
700        );
701        assert_eq!(
702            config.provider_retry_max_delay_ms,
703            ServerConfig::get_provider_retry_max_delay_ms()
704        );
705        assert_eq!(
706            config.provider_max_failovers,
707            ServerConfig::get_provider_max_failovers()
708        );
709        assert_eq!(
710            config.repository_storage_type,
711            ServerConfig::get_repository_storage_type()
712        );
713        assert_eq!(
714            config.reset_storage_on_start,
715            ServerConfig::get_reset_storage_on_start()
716        );
717        assert_eq!(
718            config.storage_encryption_key,
719            ServerConfig::get_storage_encryption_key()
720        );
721        assert_eq!(
722            config.transaction_expiration_hours,
723            ServerConfig::get_transaction_expiration_hours()
724        );
725    }
726}