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 {MINIMUM_SECRET_VALUE_LENGTH} characters long"
166            );
167        }
168
169        api_key
170    }
171
172    /// Gets the API key from environment variable or returns None if not set or invalid
173    pub fn get_api_key_optional() -> Option<SecretString> {
174        env::var("API_KEY")
175            .ok()
176            .map(|key| SecretString::new(&key))
177            .filter(|key| key.has_minimum_length(MINIMUM_SECRET_VALUE_LENGTH))
178    }
179
180    /// Gets the rate limit requests per second from environment variable or default
181    pub fn get_rate_limit_requests_per_second() -> u64 {
182        env::var("RATE_LIMIT_REQUESTS_PER_SECOND")
183            .unwrap_or_else(|_| "100".to_string())
184            .parse()
185            .unwrap_or(100)
186    }
187
188    /// Gets the rate limit burst size from environment variable or default
189    pub fn get_rate_limit_burst_size() -> u32 {
190        env::var("RATE_LIMIT_BURST_SIZE")
191            .unwrap_or_else(|_| "300".to_string())
192            .parse()
193            .unwrap_or(300)
194    }
195
196    /// Gets the metrics port from environment variable or default
197    pub fn get_metrics_port() -> u16 {
198        env::var("METRICS_PORT")
199            .unwrap_or_else(|_| "8081".to_string())
200            .parse()
201            .unwrap_or(8081)
202    }
203
204    /// Gets the enable swagger setting from environment variable or default
205    pub fn get_enable_swagger() -> bool {
206        env::var("ENABLE_SWAGGER")
207            .map(|v| v.to_lowercase() == "true")
208            .unwrap_or(false)
209    }
210
211    /// Gets the Redis connection timeout from environment variable or default
212    pub fn get_redis_connection_timeout_ms() -> u64 {
213        env::var("REDIS_CONNECTION_TIMEOUT_MS")
214            .unwrap_or_else(|_| "10000".to_string())
215            .parse()
216            .unwrap_or(10000)
217    }
218
219    /// Gets the Redis key prefix from environment variable or default
220    pub fn get_redis_key_prefix() -> String {
221        env::var("REDIS_KEY_PREFIX").unwrap_or_else(|_| "oz-relayer".to_string())
222    }
223
224    /// Gets the RPC timeout from environment variable or default
225    pub fn get_rpc_timeout_ms() -> u64 {
226        env::var("RPC_TIMEOUT_MS")
227            .unwrap_or_else(|_| "10000".to_string())
228            .parse()
229            .unwrap_or(10000)
230    }
231
232    /// Gets the provider max retries from environment variable or default
233    pub fn get_provider_max_retries() -> u8 {
234        env::var("PROVIDER_MAX_RETRIES")
235            .unwrap_or_else(|_| "3".to_string())
236            .parse()
237            .unwrap_or(3)
238    }
239
240    /// Gets the provider retry base delay from environment variable or default
241    pub fn get_provider_retry_base_delay_ms() -> u64 {
242        env::var("PROVIDER_RETRY_BASE_DELAY_MS")
243            .unwrap_or_else(|_| "100".to_string())
244            .parse()
245            .unwrap_or(100)
246    }
247
248    /// Gets the provider retry max delay from environment variable or default
249    pub fn get_provider_retry_max_delay_ms() -> u64 {
250        env::var("PROVIDER_RETRY_MAX_DELAY_MS")
251            .unwrap_or_else(|_| "2000".to_string())
252            .parse()
253            .unwrap_or(2000)
254    }
255
256    /// Gets the provider max failovers from environment variable or default
257    pub fn get_provider_max_failovers() -> u8 {
258        env::var("PROVIDER_MAX_FAILOVERS")
259            .unwrap_or_else(|_| "3".to_string())
260            .parse()
261            .unwrap_or(3)
262    }
263
264    /// Gets the repository storage type from environment variable or default
265    pub fn get_repository_storage_type() -> RepositoryStorageType {
266        env::var("REPOSITORY_STORAGE_TYPE")
267            .unwrap_or_else(|_| "in_memory".to_string())
268            .parse()
269            .unwrap_or(RepositoryStorageType::InMemory)
270    }
271
272    /// Gets the reset storage on start setting from environment variable or default
273    pub fn get_reset_storage_on_start() -> bool {
274        env::var("RESET_STORAGE_ON_START")
275            .map(|v| v.to_lowercase() == "true")
276            .unwrap_or(false)
277    }
278
279    /// Gets the storage encryption key from environment variable or None
280    pub fn get_storage_encryption_key() -> Option<SecretString> {
281        env::var("STORAGE_ENCRYPTION_KEY")
282            .map(|v| SecretString::new(&v))
283            .ok()
284    }
285
286    /// Gets the transaction expiration hours from environment variable or default
287    pub fn get_transaction_expiration_hours() -> u64 {
288        env::var("TRANSACTION_EXPIRATION_HOURS")
289            .unwrap_or_else(|_| "4".to_string())
290            .parse()
291            .unwrap_or(4)
292    }
293
294    /// Get worker concurrency from environment variable or use default
295    ///
296    /// Environment variable format: `BACKGROUND_WORKER_{WORKER_NAME}_CONCURRENCY`
297    /// Example: `BACKGROUND_WORKER_TRANSACTION_REQUEST_CONCURRENCY=20`
298    pub fn get_worker_concurrency(worker_name: &str, default: usize) -> usize {
299        let env_var = format!(
300            "BACKGROUND_WORKER_{}_CONCURRENCY",
301            worker_name.to_uppercase()
302        );
303        env::var(&env_var)
304            .ok()
305            .and_then(|v| v.parse().ok())
306            .unwrap_or(default)
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use lazy_static::lazy_static;
314    use std::env;
315    use std::sync::Mutex;
316
317    // Use a mutex to ensure tests don't run in parallel when modifying env vars
318    lazy_static! {
319        static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
320    }
321
322    fn setup() {
323        // Clear all environment variables first
324        env::remove_var("HOST");
325        env::remove_var("APP_PORT");
326        env::remove_var("REDIS_URL");
327        env::remove_var("CONFIG_DIR");
328        env::remove_var("CONFIG_FILE_NAME");
329        env::remove_var("CONFIG_FILE_PATH");
330        env::remove_var("API_KEY");
331        env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
332        env::remove_var("RATE_LIMIT_BURST_SIZE");
333        env::remove_var("METRICS_PORT");
334        env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
335        env::remove_var("RPC_TIMEOUT_MS");
336        env::remove_var("PROVIDER_MAX_RETRIES");
337        env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
338        env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
339        env::remove_var("PROVIDER_MAX_FAILOVERS");
340        env::remove_var("REPOSITORY_STORAGE_TYPE");
341        env::remove_var("RESET_STORAGE_ON_START");
342        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
343        // Set required variables for most tests
344        env::set_var("REDIS_URL", "redis://localhost:6379");
345        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
346        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
347    }
348
349    #[test]
350    fn test_default_values() {
351        let _lock = match ENV_MUTEX.lock() {
352            Ok(guard) => guard,
353            Err(poisoned) => poisoned.into_inner(),
354        };
355        setup();
356
357        let config = ServerConfig::from_env();
358
359        assert_eq!(config.host, "0.0.0.0");
360        assert_eq!(config.port, 8080);
361        assert_eq!(config.redis_url, "redis://localhost:6379");
362        assert_eq!(config.config_file_path, "./config/config.json");
363        assert_eq!(
364            config.api_key,
365            SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
366        );
367        assert_eq!(config.rate_limit_requests_per_second, 100);
368        assert_eq!(config.rate_limit_burst_size, 300);
369        assert_eq!(config.metrics_port, 8081);
370        assert_eq!(config.redis_connection_timeout_ms, 5000);
371        assert_eq!(config.rpc_timeout_ms, 10000);
372        assert_eq!(config.provider_max_retries, 3);
373        assert_eq!(config.provider_retry_base_delay_ms, 100);
374        assert_eq!(config.provider_retry_max_delay_ms, 2000);
375        assert_eq!(config.provider_max_failovers, 3);
376        assert_eq!(
377            config.repository_storage_type,
378            RepositoryStorageType::InMemory
379        );
380        assert!(!config.reset_storage_on_start);
381        assert_eq!(config.transaction_expiration_hours, 4);
382    }
383
384    #[test]
385    fn test_invalid_port_values() {
386        let _lock = match ENV_MUTEX.lock() {
387            Ok(guard) => guard,
388            Err(poisoned) => poisoned.into_inner(),
389        };
390        setup();
391        env::set_var("REDIS_URL", "redis://localhost:6379");
392        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
393        env::set_var("APP_PORT", "not_a_number");
394        env::set_var("METRICS_PORT", "also_not_a_number");
395        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "invalid");
396        env::set_var("RATE_LIMIT_BURST_SIZE", "invalid");
397        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "invalid");
398        env::set_var("RPC_TIMEOUT_MS", "invalid");
399        env::set_var("PROVIDER_MAX_RETRIES", "invalid");
400        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "invalid");
401        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "invalid");
402        env::set_var("PROVIDER_MAX_FAILOVERS", "invalid");
403        env::set_var("REPOSITORY_STORAGE_TYPE", "invalid");
404        env::set_var("RESET_STORAGE_ON_START", "invalid");
405        env::set_var("TRANSACTION_EXPIRATION_HOURS", "invalid");
406        let config = ServerConfig::from_env();
407
408        // Should fall back to defaults when parsing fails
409        assert_eq!(config.port, 8080);
410        assert_eq!(config.metrics_port, 8081);
411        assert_eq!(config.rate_limit_requests_per_second, 100);
412        assert_eq!(config.rate_limit_burst_size, 300);
413        assert_eq!(config.redis_connection_timeout_ms, 10000);
414        assert_eq!(config.rpc_timeout_ms, 10000);
415        assert_eq!(config.provider_max_retries, 3);
416        assert_eq!(config.provider_retry_base_delay_ms, 100);
417        assert_eq!(config.provider_retry_max_delay_ms, 2000);
418        assert_eq!(config.provider_max_failovers, 3);
419        assert_eq!(
420            config.repository_storage_type,
421            RepositoryStorageType::InMemory
422        );
423        assert!(!config.reset_storage_on_start);
424        assert_eq!(config.transaction_expiration_hours, 4);
425    }
426
427    #[test]
428    fn test_custom_values() {
429        let _lock = match ENV_MUTEX.lock() {
430            Ok(guard) => guard,
431            Err(poisoned) => poisoned.into_inner(),
432        };
433        setup();
434
435        env::set_var("HOST", "127.0.0.1");
436        env::set_var("APP_PORT", "9090");
437        env::set_var("REDIS_URL", "redis://custom:6379");
438        env::set_var("CONFIG_DIR", "custom");
439        env::set_var("CONFIG_FILE_NAME", "path.json");
440        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
441        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "200");
442        env::set_var("RATE_LIMIT_BURST_SIZE", "500");
443        env::set_var("METRICS_PORT", "9091");
444        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "10000");
445        env::set_var("RPC_TIMEOUT_MS", "33333");
446        env::set_var("PROVIDER_MAX_RETRIES", "5");
447        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
448        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "3000");
449        env::set_var("PROVIDER_MAX_FAILOVERS", "4");
450        env::set_var("REPOSITORY_STORAGE_TYPE", "in_memory");
451        env::set_var("RESET_STORAGE_ON_START", "true");
452        env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
453        let config = ServerConfig::from_env();
454
455        assert_eq!(config.host, "127.0.0.1");
456        assert_eq!(config.port, 9090);
457        assert_eq!(config.redis_url, "redis://custom:6379");
458        assert_eq!(config.config_file_path, "custom/path.json");
459        assert_eq!(
460            config.api_key,
461            SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
462        );
463        assert_eq!(config.rate_limit_requests_per_second, 200);
464        assert_eq!(config.rate_limit_burst_size, 500);
465        assert_eq!(config.metrics_port, 9091);
466        assert_eq!(config.redis_connection_timeout_ms, 10000);
467        assert_eq!(config.rpc_timeout_ms, 33333);
468        assert_eq!(config.provider_max_retries, 5);
469        assert_eq!(config.provider_retry_base_delay_ms, 200);
470        assert_eq!(config.provider_retry_max_delay_ms, 3000);
471        assert_eq!(config.provider_max_failovers, 4);
472        assert_eq!(
473            config.repository_storage_type,
474            RepositoryStorageType::InMemory
475        );
476        assert!(config.reset_storage_on_start);
477        assert_eq!(config.transaction_expiration_hours, 6);
478    }
479
480    #[test]
481    #[should_panic(expected = "Security error: API_KEY must be at least 32 characters long")]
482    fn test_invalid_api_key_length() {
483        let _lock = match ENV_MUTEX.lock() {
484            Ok(guard) => guard,
485            Err(poisoned) => poisoned.into_inner(),
486        };
487        setup();
488        env::set_var("REDIS_URL", "redis://localhost:6379");
489        env::set_var("API_KEY", "insufficient_length");
490        env::set_var("APP_PORT", "8080");
491        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "100");
492        env::set_var("RATE_LIMIT_BURST_SIZE", "300");
493        env::set_var("METRICS_PORT", "9091");
494        env::set_var("TRANSACTION_EXPIRATION_HOURS", "4");
495
496        let _ = ServerConfig::from_env();
497
498        panic!("Test should have panicked before reaching here");
499    }
500
501    // Tests for individual getter methods
502    #[test]
503    fn test_individual_getters_with_defaults() {
504        let _lock = match ENV_MUTEX.lock() {
505            Ok(guard) => guard,
506            Err(poisoned) => poisoned.into_inner(),
507        };
508
509        // Clear all environment variables to test defaults
510        env::remove_var("HOST");
511        env::remove_var("APP_PORT");
512        env::remove_var("REDIS_URL");
513        env::remove_var("CONFIG_DIR");
514        env::remove_var("CONFIG_FILE_NAME");
515        env::remove_var("API_KEY");
516        env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
517        env::remove_var("RATE_LIMIT_BURST_SIZE");
518        env::remove_var("METRICS_PORT");
519        env::remove_var("ENABLE_SWAGGER");
520        env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
521        env::remove_var("REDIS_KEY_PREFIX");
522        env::remove_var("RPC_TIMEOUT_MS");
523        env::remove_var("PROVIDER_MAX_RETRIES");
524        env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
525        env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
526        env::remove_var("PROVIDER_MAX_FAILOVERS");
527        env::remove_var("REPOSITORY_STORAGE_TYPE");
528        env::remove_var("RESET_STORAGE_ON_START");
529        env::remove_var("STORAGE_ENCRYPTION_KEY");
530        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
531
532        // Test individual getters with defaults
533        assert_eq!(ServerConfig::get_host(), "0.0.0.0");
534        assert_eq!(ServerConfig::get_port(), 8080);
535        assert_eq!(ServerConfig::get_redis_url_optional(), None);
536        assert_eq!(ServerConfig::get_config_file_path(), "./config/config.json");
537        assert_eq!(ServerConfig::get_api_key_optional(), None);
538        assert_eq!(ServerConfig::get_rate_limit_requests_per_second(), 100);
539        assert_eq!(ServerConfig::get_rate_limit_burst_size(), 300);
540        assert_eq!(ServerConfig::get_metrics_port(), 8081);
541        assert!(!ServerConfig::get_enable_swagger());
542        assert_eq!(ServerConfig::get_redis_connection_timeout_ms(), 10000);
543        assert_eq!(ServerConfig::get_redis_key_prefix(), "oz-relayer");
544        assert_eq!(ServerConfig::get_rpc_timeout_ms(), 10000);
545        assert_eq!(ServerConfig::get_provider_max_retries(), 3);
546        assert_eq!(ServerConfig::get_provider_retry_base_delay_ms(), 100);
547        assert_eq!(ServerConfig::get_provider_retry_max_delay_ms(), 2000);
548        assert_eq!(ServerConfig::get_provider_max_failovers(), 3);
549        assert_eq!(
550            ServerConfig::get_repository_storage_type(),
551            RepositoryStorageType::InMemory
552        );
553        assert!(!ServerConfig::get_reset_storage_on_start());
554        assert!(ServerConfig::get_storage_encryption_key().is_none());
555        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 4);
556    }
557
558    #[test]
559    fn test_individual_getters_with_custom_values() {
560        let _lock = match ENV_MUTEX.lock() {
561            Ok(guard) => guard,
562            Err(poisoned) => poisoned.into_inner(),
563        };
564
565        // Set custom values
566        env::set_var("HOST", "192.168.1.1");
567        env::set_var("APP_PORT", "9999");
568        env::set_var("REDIS_URL", "redis://custom:6379");
569        env::set_var("CONFIG_DIR", "/custom/config");
570        env::set_var("CONFIG_FILE_NAME", "custom.json");
571        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
572        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "500");
573        env::set_var("RATE_LIMIT_BURST_SIZE", "1000");
574        env::set_var("METRICS_PORT", "9999");
575        env::set_var("ENABLE_SWAGGER", "true");
576        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
577        env::set_var("REDIS_KEY_PREFIX", "custom-prefix");
578        env::set_var("RPC_TIMEOUT_MS", "15000");
579        env::set_var("PROVIDER_MAX_RETRIES", "5");
580        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
581        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "5000");
582        env::set_var("PROVIDER_MAX_FAILOVERS", "10");
583        env::set_var("REPOSITORY_STORAGE_TYPE", "redis");
584        env::set_var("RESET_STORAGE_ON_START", "true");
585        env::set_var("STORAGE_ENCRYPTION_KEY", "my-encryption-key");
586        env::set_var("TRANSACTION_EXPIRATION_HOURS", "12");
587
588        // Test individual getters with custom values
589        assert_eq!(ServerConfig::get_host(), "192.168.1.1");
590        assert_eq!(ServerConfig::get_port(), 9999);
591        assert_eq!(
592            ServerConfig::get_redis_url_optional(),
593            Some("redis://custom:6379".to_string())
594        );
595        assert_eq!(
596            ServerConfig::get_config_file_path(),
597            "/custom/config/custom.json"
598        );
599        assert!(ServerConfig::get_api_key_optional().is_some());
600        assert_eq!(ServerConfig::get_rate_limit_requests_per_second(), 500);
601        assert_eq!(ServerConfig::get_rate_limit_burst_size(), 1000);
602        assert_eq!(ServerConfig::get_metrics_port(), 9999);
603        assert!(ServerConfig::get_enable_swagger());
604        assert_eq!(ServerConfig::get_redis_connection_timeout_ms(), 5000);
605        assert_eq!(ServerConfig::get_redis_key_prefix(), "custom-prefix");
606        assert_eq!(ServerConfig::get_rpc_timeout_ms(), 15000);
607        assert_eq!(ServerConfig::get_provider_max_retries(), 5);
608        assert_eq!(ServerConfig::get_provider_retry_base_delay_ms(), 200);
609        assert_eq!(ServerConfig::get_provider_retry_max_delay_ms(), 5000);
610        assert_eq!(ServerConfig::get_provider_max_failovers(), 10);
611        assert_eq!(
612            ServerConfig::get_repository_storage_type(),
613            RepositoryStorageType::Redis
614        );
615        assert!(ServerConfig::get_reset_storage_on_start());
616        assert!(ServerConfig::get_storage_encryption_key().is_some());
617        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 12);
618    }
619
620    #[test]
621    #[should_panic(expected = "REDIS_URL must be set")]
622    fn test_get_redis_url_panics_when_not_set() {
623        let _lock = match ENV_MUTEX.lock() {
624            Ok(guard) => guard,
625            Err(poisoned) => poisoned.into_inner(),
626        };
627
628        env::remove_var("REDIS_URL");
629        let _ = ServerConfig::get_redis_url();
630    }
631
632    #[test]
633    #[should_panic(expected = "API_KEY must be set")]
634    fn test_get_api_key_panics_when_not_set() {
635        let _lock = match ENV_MUTEX.lock() {
636            Ok(guard) => guard,
637            Err(poisoned) => poisoned.into_inner(),
638        };
639
640        env::remove_var("API_KEY");
641        let _ = ServerConfig::get_api_key();
642    }
643
644    #[test]
645    fn test_optional_getters_return_none_safely() {
646        let _lock = match ENV_MUTEX.lock() {
647            Ok(guard) => guard,
648            Err(poisoned) => poisoned.into_inner(),
649        };
650
651        env::remove_var("REDIS_URL");
652        env::remove_var("API_KEY");
653        env::remove_var("STORAGE_ENCRYPTION_KEY");
654
655        assert!(ServerConfig::get_redis_url_optional().is_none());
656        assert!(ServerConfig::get_api_key_optional().is_none());
657        assert!(ServerConfig::get_storage_encryption_key().is_none());
658    }
659
660    #[test]
661    fn test_refactored_from_env_equivalence() {
662        let _lock = match ENV_MUTEX.lock() {
663            Ok(guard) => guard,
664            Err(poisoned) => poisoned.into_inner(),
665        };
666        setup();
667
668        // Set custom values to test both default and custom paths
669        env::set_var("HOST", "custom-host");
670        env::set_var("APP_PORT", "7777");
671        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "250");
672        env::set_var("METRICS_PORT", "7778");
673        env::set_var("ENABLE_SWAGGER", "true");
674        env::set_var("PROVIDER_MAX_RETRIES", "7");
675        env::set_var("TRANSACTION_EXPIRATION_HOURS", "8");
676
677        let config = ServerConfig::from_env();
678
679        // Verify the refactored from_env() produces the same results as individual getters
680        assert_eq!(config.host, ServerConfig::get_host());
681        assert_eq!(config.port, ServerConfig::get_port());
682        assert_eq!(config.redis_url, ServerConfig::get_redis_url());
683        assert_eq!(
684            config.config_file_path,
685            ServerConfig::get_config_file_path()
686        );
687        assert_eq!(config.api_key, ServerConfig::get_api_key());
688        assert_eq!(
689            config.rate_limit_requests_per_second,
690            ServerConfig::get_rate_limit_requests_per_second()
691        );
692        assert_eq!(
693            config.rate_limit_burst_size,
694            ServerConfig::get_rate_limit_burst_size()
695        );
696        assert_eq!(config.metrics_port, ServerConfig::get_metrics_port());
697        assert_eq!(config.enable_swagger, ServerConfig::get_enable_swagger());
698        assert_eq!(
699            config.redis_connection_timeout_ms,
700            ServerConfig::get_redis_connection_timeout_ms()
701        );
702        assert_eq!(
703            config.redis_key_prefix,
704            ServerConfig::get_redis_key_prefix()
705        );
706        assert_eq!(config.rpc_timeout_ms, ServerConfig::get_rpc_timeout_ms());
707        assert_eq!(
708            config.provider_max_retries,
709            ServerConfig::get_provider_max_retries()
710        );
711        assert_eq!(
712            config.provider_retry_base_delay_ms,
713            ServerConfig::get_provider_retry_base_delay_ms()
714        );
715        assert_eq!(
716            config.provider_retry_max_delay_ms,
717            ServerConfig::get_provider_retry_max_delay_ms()
718        );
719        assert_eq!(
720            config.provider_max_failovers,
721            ServerConfig::get_provider_max_failovers()
722        );
723        assert_eq!(
724            config.repository_storage_type,
725            ServerConfig::get_repository_storage_type()
726        );
727        assert_eq!(
728            config.reset_storage_on_start,
729            ServerConfig::get_reset_storage_on_start()
730        );
731        assert_eq!(
732            config.storage_encryption_key,
733            ServerConfig::get_storage_encryption_key()
734        );
735        assert_eq!(
736            config.transaction_expiration_hours,
737            ServerConfig::get_transaction_expiration_hours()
738        );
739    }
740
741    mod get_worker_concurrency_tests {
742        use super::*;
743        use serial_test::serial;
744
745        #[test]
746        #[serial]
747        fn test_returns_default_when_env_not_set() {
748            let worker_name = "test_worker";
749            let env_var = format!(
750                "BACKGROUND_WORKER_{}_CONCURRENCY",
751                worker_name.to_uppercase()
752            );
753
754            // Ensure env var is not set
755            env::remove_var(&env_var);
756
757            let default_value = 42;
758            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
759
760            assert_eq!(
761                result, default_value,
762                "Should return default value when env var is not set"
763            );
764        }
765
766        #[test]
767        #[serial]
768        fn test_returns_env_value_when_set() {
769            let worker_name = "status_checker";
770            let env_var = format!(
771                "BACKGROUND_WORKER_{}_CONCURRENCY",
772                worker_name.to_uppercase()
773            );
774
775            // Set env var to a specific value
776            env::set_var(&env_var, "100");
777
778            let default_value = 10;
779            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
780
781            assert_eq!(result, 100, "Should return env var value when set");
782
783            // Cleanup
784            env::remove_var(&env_var);
785        }
786
787        #[test]
788        #[serial]
789        fn test_returns_default_when_env_invalid() {
790            let worker_name = "invalid_worker";
791            let env_var = format!(
792                "BACKGROUND_WORKER_{}_CONCURRENCY",
793                worker_name.to_uppercase()
794            );
795
796            // Set env var to invalid value
797            env::set_var(&env_var, "not_a_number");
798
799            let default_value = 25;
800            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
801
802            assert_eq!(
803                result, default_value,
804                "Should return default value when env var is invalid"
805            );
806
807            // Cleanup
808            env::remove_var(&env_var);
809        }
810
811        #[test]
812        #[serial]
813        fn test_returns_default_when_env_empty() {
814            let worker_name = "empty_worker";
815            let env_var = format!(
816                "BACKGROUND_WORKER_{}_CONCURRENCY",
817                worker_name.to_uppercase()
818            );
819
820            // Set env var to empty string
821            env::set_var(&env_var, "");
822
823            let default_value = 15;
824            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
825
826            assert_eq!(
827                result, default_value,
828                "Should return default value when env var is empty"
829            );
830
831            // Cleanup
832            env::remove_var(&env_var);
833        }
834
835        #[test]
836        #[serial]
837        fn test_returns_default_when_env_negative() {
838            let worker_name = "negative_worker";
839            let env_var = format!(
840                "BACKGROUND_WORKER_{}_CONCURRENCY",
841                worker_name.to_uppercase()
842            );
843
844            // Set env var to negative value
845            env::set_var(&env_var, "-5");
846
847            let default_value = 20;
848            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
849
850            assert_eq!(
851                result, default_value,
852                "Should return default value when env var is negative"
853            );
854
855            // Cleanup
856            env::remove_var(&env_var);
857        }
858
859        #[test]
860        #[serial]
861        fn test_env_var_name_formatting() {
862            // Test that worker names are properly uppercased
863            let worker_names = vec![
864                (
865                    "transaction_sender",
866                    "BACKGROUND_WORKER_TRANSACTION_SENDER_CONCURRENCY",
867                ),
868                (
869                    "status_checker_evm",
870                    "BACKGROUND_WORKER_STATUS_CHECKER_EVM_CONCURRENCY",
871                ),
872                (
873                    "notification_sender",
874                    "BACKGROUND_WORKER_NOTIFICATION_SENDER_CONCURRENCY",
875                ),
876            ];
877
878            for (worker_name, expected_env_var) in worker_names {
879                let actual_env_var = format!(
880                    "BACKGROUND_WORKER_{}_CONCURRENCY",
881                    worker_name.to_uppercase()
882                );
883                assert_eq!(
884                    actual_env_var, expected_env_var,
885                    "Env var name should be correctly formatted for worker: {}",
886                    worker_name
887                );
888            }
889        }
890
891        #[test]
892        #[serial]
893        fn test_zero_value() {
894            let worker_name = "zero_worker";
895            let env_var = format!(
896                "BACKGROUND_WORKER_{}_CONCURRENCY",
897                worker_name.to_uppercase()
898            );
899
900            // Set env var to zero
901            env::set_var(&env_var, "0");
902
903            let default_value = 30;
904            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
905
906            assert_eq!(result, 0, "Should accept zero as a valid value");
907
908            // Cleanup
909            env::remove_var(&env_var);
910        }
911
912        #[test]
913        #[serial]
914        fn test_large_value() {
915            let worker_name = "large_worker";
916            let env_var = format!(
917                "BACKGROUND_WORKER_{}_CONCURRENCY",
918                worker_name.to_uppercase()
919            );
920
921            // Set env var to a large value
922            env::set_var(&env_var, "10000");
923
924            let default_value = 50;
925            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
926
927            assert_eq!(result, 10000, "Should accept large values");
928
929            // Cleanup
930            env::remove_var(&env_var);
931        }
932
933        #[test]
934        #[serial]
935        fn test_whitespace_in_value() {
936            let worker_name = "whitespace_worker";
937            let env_var = format!(
938                "BACKGROUND_WORKER_{}_CONCURRENCY",
939                worker_name.to_uppercase()
940            );
941
942            // Set env var with leading/trailing whitespace
943            env::set_var(&env_var, "  75  ");
944
945            let default_value = 35;
946            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
947
948            // Note: String::parse::<usize>() does NOT trim whitespace, so this will fail to parse
949            // and return the default value
950            assert_eq!(
951                result, default_value,
952                "Should return default value when value has whitespace"
953            );
954
955            // Cleanup
956            env::remove_var(&env_var);
957        }
958
959        #[test]
960        #[serial]
961        fn test_float_value_returns_default() {
962            let worker_name = "float_worker";
963            let env_var = format!(
964                "BACKGROUND_WORKER_{}_CONCURRENCY",
965                worker_name.to_uppercase()
966            );
967
968            // Set env var to float value
969            env::set_var(&env_var, "12.5");
970
971            let default_value = 40;
972            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
973
974            assert_eq!(
975                result, default_value,
976                "Should return default value for float input"
977            );
978
979            // Cleanup
980            env::remove_var(&env_var);
981        }
982    }
983}