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::{
6    constants::{
7        DEFAULT_PROVIDER_FAILURE_EXPIRATION_SECS, DEFAULT_PROVIDER_FAILURE_THRESHOLD,
8        DEFAULT_PROVIDER_PAUSE_DURATION_SECS, MINIMUM_SECRET_VALUE_LENGTH,
9        STELLAR_FEE_FORWARDER_MAINNET, STELLAR_SOROSWAP_MAINNET_FACTORY,
10        STELLAR_SOROSWAP_MAINNET_NATIVE_WRAPPER, STELLAR_SOROSWAP_MAINNET_ROUTER,
11    },
12    models::SecretString,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq, Display)]
16pub enum RepositoryStorageType {
17    InMemory,
18    Redis,
19}
20
21impl FromStr for RepositoryStorageType {
22    type Err = String;
23
24    fn from_str(s: &str) -> Result<Self, Self::Err> {
25        match s.to_lowercase().as_str() {
26            "inmemory" | "in_memory" => Ok(Self::InMemory),
27            "redis" => Ok(Self::Redis),
28            _ => Err(format!("Invalid repository storage type: {s}")),
29        }
30    }
31}
32
33/// Returns `Some(s.to_string())` when `s` is non-empty, `None` otherwise.
34fn non_empty_const(s: &str) -> Option<String> {
35    if s.is_empty() {
36        None
37    } else {
38        Some(s.to_string())
39    }
40}
41
42#[derive(Debug, Clone)]
43pub struct ServerConfig {
44    /// The host address the server will bind to.
45    pub host: String,
46    /// The port number the server will listen on.
47    pub port: u16,
48    /// The URL for the Redis primary instance (used for write operations).
49    pub redis_url: String,
50    /// Optional URL for Redis reader endpoint (used for read operations).
51    /// When set, read operations use this endpoint while writes use `redis_url`.
52    /// Useful for AWS ElastiCache with read replicas.
53    pub redis_reader_url: Option<String>,
54    /// The file path to the server's configuration file.
55    pub config_file_path: String,
56    /// The API key used for authentication.
57    pub api_key: SecretString,
58    /// The number of requests allowed per second.
59    pub rate_limit_requests_per_second: u64,
60    /// The maximum burst size for rate limiting.
61    pub rate_limit_burst_size: u32,
62    /// The port number for exposing metrics.
63    pub metrics_port: u16,
64    /// Enable Swagger UI.
65    pub enable_swagger: bool,
66    /// The number of seconds to wait for a Redis connection.
67    pub redis_connection_timeout_ms: u64,
68    /// The prefix for the Redis key.
69    pub redis_key_prefix: String,
70    /// Maximum number of connections in the Redis pool.
71    pub redis_pool_max_size: usize,
72    /// Maximum pool size for reader connections. Defaults to 1000.
73    /// Useful for read-heavy workloads where more reader connections are beneficial.
74    pub redis_reader_pool_max_size: usize,
75    /// Timeout in milliseconds waiting to get a connection from the pool.
76    pub redis_pool_timeout_ms: u64,
77    /// The number of milliseconds to wait for an RPC response.
78    pub rpc_timeout_ms: u64,
79    /// Maximum number of retry attempts for provider operations.
80    pub provider_max_retries: u8,
81    /// Base delay between retry attempts (milliseconds).
82    pub provider_retry_base_delay_ms: u64,
83    /// Maximum delay between retry attempts (milliseconds).
84    pub provider_retry_max_delay_ms: u64,
85    /// Maximum number of failovers (switching to different providers).
86    pub provider_max_failovers: u8,
87    /// Number of consecutive failures before pausing a provider.
88    pub provider_failure_threshold: u32,
89    /// Duration in seconds to pause a provider after reaching failure threshold.
90    pub provider_pause_duration_secs: u64,
91    /// Duration in seconds after which failures are considered stale and reset.
92    pub provider_failure_expiration_secs: u64,
93    /// The type of repository storage to use.
94    pub repository_storage_type: RepositoryStorageType,
95    /// Flag to force config file processing.
96    pub reset_storage_on_start: bool,
97    /// The encryption key for the storage.
98    pub storage_encryption_key: Option<SecretString>,
99    /// Transaction expiration time in hours for transactions in final states.
100    /// Supports fractional values (e.g., 0.1 = 6 minutes).
101    pub transaction_expiration_hours: f64,
102    /// Comma-separated list of allowed RPC hosts (domains or IPs). If non-empty, only these hosts are permitted.
103    pub rpc_allowed_hosts: Vec<String>,
104    /// If true, block private IP addresses (RFC 1918, loopback, link-local). Cloud metadata endpoints are always blocked.
105    pub rpc_block_private_ips: bool,
106    /// Maximum number of concurrent requests allowed for /api/v1/relayers/* endpoints.
107    pub relayer_concurrency_limit: usize,
108    /// Maximum number of concurrent TCP connections server-wide.
109    pub max_connections: usize,
110    /// TCP listen connection backlog size (pending connections queue).
111    /// Higher values allow more connections to be queued during traffic bursts.
112    pub connection_backlog: u32,
113    /// Request handler timeout in seconds for API endpoints.
114    pub request_timeout_seconds: u64,
115    /// Stellar mainnet FeeForwarder contract address for gas abstraction.
116    pub stellar_mainnet_fee_forwarder_address: Option<String>,
117    /// Stellar testnet FeeForwarder contract address for gas abstraction.
118    pub stellar_testnet_fee_forwarder_address: Option<String>,
119    /// Stellar mainnet Soroswap router contract address.
120    pub stellar_mainnet_soroswap_router_address: Option<String>,
121    /// Stellar testnet Soroswap router contract address.
122    pub stellar_testnet_soroswap_router_address: Option<String>,
123    /// Stellar mainnet Soroswap factory contract address.
124    pub stellar_mainnet_soroswap_factory_address: Option<String>,
125    /// Stellar testnet Soroswap factory contract address.
126    pub stellar_testnet_soroswap_factory_address: Option<String>,
127    /// Stellar mainnet native XLM wrapper token address for Soroswap.
128    pub stellar_mainnet_soroswap_native_wrapper_address: Option<String>,
129    /// Stellar testnet native XLM wrapper token address for Soroswap.
130    pub stellar_testnet_soroswap_native_wrapper_address: Option<String>,
131}
132
133impl ServerConfig {
134    /// Creates a new `ServerConfig` instance from environment variables.
135    ///
136    /// # Panics
137    ///
138    /// This function will panic if the `REDIS_URL` or `API_KEY` environment
139    /// variables are not set, as they are required for the server to function.
140    ///
141    /// # Defaults
142    ///
143    /// - `HOST` defaults to `"0.0.0.0"`.
144    /// - `APP_PORT` defaults to `8080`.
145    /// - `CONFIG_DIR` defaults to `"config/config.json"`.
146    /// - `RATE_LIMIT_REQUESTS_PER_SECOND` defaults to `100`.
147    /// - `RATE_LIMIT_BURST_SIZE` defaults to `300`.
148    /// - `METRICS_PORT` defaults to `8081`.
149    /// - `PROVIDER_MAX_RETRIES` defaults to `3`.
150    /// - `PROVIDER_RETRY_BASE_DELAY_MS` defaults to `100`.
151    /// - `PROVIDER_RETRY_MAX_DELAY_MS` defaults to `2000`.
152    /// - `PROVIDER_MAX_FAILOVERS` defaults to `3`.
153    /// - `PROVIDER_FAILURE_THRESHOLD` defaults to `3`.
154    /// - `PROVIDER_PAUSE_DURATION_SECS` defaults to `60` (1 minute).
155    /// - `PROVIDER_FAILURE_EXPIRATION_SECS` defaults to `60` (1 minute).
156    /// - `REPOSITORY_STORAGE_TYPE` defaults to `"in_memory"`.
157    /// - `TRANSACTION_EXPIRATION_HOURS` defaults to `4`.
158    /// - `REQUEST_TIMEOUT_SECONDS` defaults to `30` (security measure for DoS protection).
159    /// - `CONNECTION_BACKLOG` defaults to `511` (production-ready value for traffic bursts).
160    pub fn from_env() -> Self {
161        Self {
162            host: Self::get_host(),
163            port: Self::get_port(),
164            redis_url: Self::get_redis_url(), // Uses panicking version as required
165            redis_reader_url: Self::get_redis_reader_url_optional(),
166            redis_reader_pool_max_size: Self::get_redis_reader_pool_max_size(),
167            config_file_path: Self::get_config_file_path(),
168            api_key: Self::get_api_key(), // Uses panicking version as required
169            rate_limit_requests_per_second: Self::get_rate_limit_requests_per_second(),
170            rate_limit_burst_size: Self::get_rate_limit_burst_size(),
171            metrics_port: Self::get_metrics_port(),
172            enable_swagger: Self::get_enable_swagger(),
173            redis_connection_timeout_ms: Self::get_redis_connection_timeout_ms(),
174            redis_key_prefix: Self::get_redis_key_prefix(),
175            redis_pool_max_size: Self::get_redis_pool_max_size(),
176            redis_pool_timeout_ms: Self::get_redis_pool_timeout_ms(),
177            rpc_timeout_ms: Self::get_rpc_timeout_ms(),
178            provider_max_retries: Self::get_provider_max_retries(),
179            provider_retry_base_delay_ms: Self::get_provider_retry_base_delay_ms(),
180            provider_retry_max_delay_ms: Self::get_provider_retry_max_delay_ms(),
181            provider_max_failovers: Self::get_provider_max_failovers(),
182            provider_failure_threshold: Self::get_provider_failure_threshold(),
183            provider_pause_duration_secs: Self::get_provider_pause_duration_secs(),
184            provider_failure_expiration_secs: Self::get_provider_failure_expiration_secs(),
185            repository_storage_type: Self::get_repository_storage_type(),
186            reset_storage_on_start: Self::get_reset_storage_on_start(),
187            storage_encryption_key: Self::get_storage_encryption_key(),
188            transaction_expiration_hours: Self::get_transaction_expiration_hours(),
189            rpc_allowed_hosts: Self::get_rpc_allowed_hosts(),
190            rpc_block_private_ips: Self::get_rpc_block_private_ips(),
191            relayer_concurrency_limit: Self::get_relayer_concurrency_limit(),
192            max_connections: Self::get_max_connections(),
193            connection_backlog: Self::get_connection_backlog(),
194            request_timeout_seconds: Self::get_request_timeout_seconds(),
195            stellar_mainnet_fee_forwarder_address: Self::get_stellar_mainnet_fee_forwarder_address(
196            ),
197            stellar_testnet_fee_forwarder_address: Self::get_stellar_testnet_fee_forwarder_address(
198            ),
199            stellar_mainnet_soroswap_router_address:
200                Self::get_stellar_mainnet_soroswap_router_address(),
201            stellar_testnet_soroswap_router_address:
202                Self::get_stellar_testnet_soroswap_router_address(),
203            stellar_mainnet_soroswap_factory_address:
204                Self::get_stellar_mainnet_soroswap_factory_address(),
205            stellar_testnet_soroswap_factory_address:
206                Self::get_stellar_testnet_soroswap_factory_address(),
207            stellar_mainnet_soroswap_native_wrapper_address:
208                Self::get_stellar_mainnet_soroswap_native_wrapper_address(),
209            stellar_testnet_soroswap_native_wrapper_address:
210                Self::get_stellar_testnet_soroswap_native_wrapper_address(),
211        }
212    }
213
214    // Individual getter methods for each configuration field
215
216    /// Gets the host from environment variable or default
217    pub fn get_host() -> String {
218        env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string())
219    }
220
221    /// Gets the port from environment variable or default
222    pub fn get_port() -> u16 {
223        env::var("APP_PORT")
224            .unwrap_or_else(|_| "8080".to_string())
225            .parse()
226            .unwrap_or(8080)
227    }
228
229    /// Gets the Redis URL from environment variable (panics if not set)
230    pub fn get_redis_url() -> String {
231        env::var("REDIS_URL").expect("REDIS_URL must be set")
232    }
233
234    /// Gets the Redis URL from environment variable or returns None if not set
235    pub fn get_redis_url_optional() -> Option<String> {
236        env::var("REDIS_URL").ok()
237    }
238
239    /// Gets the Redis reader URL from environment variable or returns None if not set.
240    /// When set, read operations will use this endpoint while writes use REDIS_URL.
241    /// Useful for AWS ElastiCache with read replicas.
242    pub fn get_redis_reader_url_optional() -> Option<String> {
243        env::var("REDIS_READER_URL").ok()
244    }
245
246    /// Gets the config file path from environment variables or default
247    pub fn get_config_file_path() -> String {
248        let conf_dir = if env::var("IN_DOCKER")
249            .map(|val| val == "true")
250            .unwrap_or(false)
251        {
252            "config/".to_string()
253        } else {
254            env::var("CONFIG_DIR").unwrap_or_else(|_| "./config".to_string())
255        };
256
257        let conf_dir = format!("{}/", conf_dir.trim_end_matches('/'));
258        let config_file_name =
259            env::var("CONFIG_FILE_NAME").unwrap_or_else(|_| "config.json".to_string());
260
261        format!("{conf_dir}{config_file_name}")
262    }
263
264    /// Gets the API key from environment variable (panics if not set or too short)
265    pub fn get_api_key() -> SecretString {
266        let api_key = SecretString::new(&env::var("API_KEY").expect("API_KEY must be set"));
267
268        if !api_key.has_minimum_length(MINIMUM_SECRET_VALUE_LENGTH) {
269            panic!(
270                "Security error: API_KEY must be at least {MINIMUM_SECRET_VALUE_LENGTH} characters long"
271            );
272        }
273
274        api_key
275    }
276
277    /// Gets the API key from environment variable or returns None if not set or invalid
278    pub fn get_api_key_optional() -> Option<SecretString> {
279        env::var("API_KEY")
280            .ok()
281            .map(|key| SecretString::new(&key))
282            .filter(|key| key.has_minimum_length(MINIMUM_SECRET_VALUE_LENGTH))
283    }
284
285    /// Gets the rate limit requests per second from environment variable or default
286    pub fn get_rate_limit_requests_per_second() -> u64 {
287        env::var("RATE_LIMIT_REQUESTS_PER_SECOND")
288            .unwrap_or_else(|_| "100".to_string())
289            .parse()
290            .unwrap_or(100)
291    }
292
293    /// Gets the rate limit burst size from environment variable or default
294    pub fn get_rate_limit_burst_size() -> u32 {
295        env::var("RATE_LIMIT_BURST_SIZE")
296            .unwrap_or_else(|_| "300".to_string())
297            .parse()
298            .unwrap_or(300)
299    }
300
301    /// Gets the metrics port from environment variable or default
302    pub fn get_metrics_port() -> u16 {
303        env::var("METRICS_PORT")
304            .unwrap_or_else(|_| "8081".to_string())
305            .parse()
306            .unwrap_or(8081)
307    }
308
309    /// Gets the enable swagger setting from environment variable or default
310    pub fn get_enable_swagger() -> bool {
311        env::var("ENABLE_SWAGGER")
312            .map(|v| v.to_lowercase() == "true")
313            .unwrap_or(false)
314    }
315
316    /// Gets the Redis connection timeout from environment variable or default
317    pub fn get_redis_connection_timeout_ms() -> u64 {
318        env::var("REDIS_CONNECTION_TIMEOUT_MS")
319            .unwrap_or_else(|_| "10000".to_string())
320            .parse()
321            .unwrap_or(10000)
322    }
323
324    /// Gets the Redis key prefix from environment variable or default
325    pub fn get_redis_key_prefix() -> String {
326        env::var("REDIS_KEY_PREFIX").unwrap_or_else(|_| "oz-relayer".to_string())
327    }
328
329    /// Gets the Redis pool max size from environment variable or default
330    /// Returns default (500) if value is 0 or invalid
331    pub fn get_redis_pool_max_size() -> usize {
332        env::var("REDIS_POOL_MAX_SIZE")
333            .unwrap_or_else(|_| "500".to_string())
334            .parse()
335            .ok()
336            .filter(|&v| v > 0)
337            .unwrap_or(500)
338    }
339
340    /// Gets the Redis reader pool max size from environment variable.
341    /// Returns 1000 if not set or invalid.
342    pub fn get_redis_reader_pool_max_size() -> usize {
343        env::var("REDIS_READER_POOL_MAX_SIZE")
344            .ok()
345            .and_then(|v| v.parse().ok())
346            .filter(|&v| v > 0)
347            .unwrap_or(1000)
348    }
349
350    /// Gets the Redis pool timeout from environment variable or default
351    /// Returns default (10000) if value is 0 or invalid
352    pub fn get_redis_pool_timeout_ms() -> u64 {
353        env::var("REDIS_POOL_TIMEOUT_MS")
354            .unwrap_or_else(|_| "10000".to_string())
355            .parse()
356            .ok()
357            .filter(|&v| v > 0)
358            .unwrap_or(10000)
359    }
360
361    /// Gets the RPC timeout from environment variable or default
362    pub fn get_rpc_timeout_ms() -> u64 {
363        env::var("RPC_TIMEOUT_MS")
364            .unwrap_or_else(|_| "10000".to_string())
365            .parse()
366            .unwrap_or(10000)
367    }
368
369    /// Gets the provider max retries from environment variable or default
370    pub fn get_provider_max_retries() -> u8 {
371        env::var("PROVIDER_MAX_RETRIES")
372            .unwrap_or_else(|_| "3".to_string())
373            .parse()
374            .unwrap_or(3)
375    }
376
377    /// Gets the provider retry base delay from environment variable or default
378    pub fn get_provider_retry_base_delay_ms() -> u64 {
379        env::var("PROVIDER_RETRY_BASE_DELAY_MS")
380            .unwrap_or_else(|_| "100".to_string())
381            .parse()
382            .unwrap_or(100)
383    }
384
385    /// Gets the provider retry max delay from environment variable or default
386    pub fn get_provider_retry_max_delay_ms() -> u64 {
387        env::var("PROVIDER_RETRY_MAX_DELAY_MS")
388            .unwrap_or_else(|_| "2000".to_string())
389            .parse()
390            .unwrap_or(2000)
391    }
392
393    /// Gets the provider max failovers from environment variable or default
394    pub fn get_provider_max_failovers() -> u8 {
395        env::var("PROVIDER_MAX_FAILOVERS")
396            .unwrap_or_else(|_| "3".to_string())
397            .parse()
398            .unwrap_or(3)
399    }
400
401    /// Gets the provider failure threshold from environment variable or default
402    pub fn get_provider_failure_threshold() -> u32 {
403        env::var("PROVIDER_FAILURE_THRESHOLD")
404            .or_else(|_| env::var("RPC_FAILURE_THRESHOLD")) // Support legacy env var
405            .unwrap_or_else(|_| DEFAULT_PROVIDER_FAILURE_THRESHOLD.to_string())
406            .parse()
407            .unwrap_or(DEFAULT_PROVIDER_FAILURE_THRESHOLD)
408    }
409
410    /// Gets the provider pause duration in seconds from environment variable or default
411    ///
412    /// Defaults to 60 seconds (1 minute) for faster recovery while still providing
413    /// a reasonable cooldown period for failed providers.
414    pub fn get_provider_pause_duration_secs() -> u64 {
415        env::var("PROVIDER_PAUSE_DURATION_SECS")
416            .or_else(|_| env::var("RPC_PAUSE_DURATION_SECS")) // Support legacy env var
417            .unwrap_or_else(|_| DEFAULT_PROVIDER_PAUSE_DURATION_SECS.to_string())
418            .parse()
419            .unwrap_or(DEFAULT_PROVIDER_PAUSE_DURATION_SECS)
420    }
421
422    /// Gets the provider failure expiration duration in seconds from environment variable or default
423    ///
424    /// Defaults to 60 seconds (1 minute). Failures older than this are considered stale
425    /// and reset, allowing providers to naturally recover over time.
426    pub fn get_provider_failure_expiration_secs() -> u64 {
427        env::var("PROVIDER_FAILURE_EXPIRATION_SECS")
428            .unwrap_or_else(|_| DEFAULT_PROVIDER_FAILURE_EXPIRATION_SECS.to_string())
429            .parse()
430            .unwrap_or(DEFAULT_PROVIDER_FAILURE_EXPIRATION_SECS)
431    }
432
433    /// Gets the repository storage type from environment variable or default
434    pub fn get_repository_storage_type() -> RepositoryStorageType {
435        env::var("REPOSITORY_STORAGE_TYPE")
436            .unwrap_or_else(|_| "in_memory".to_string())
437            .parse()
438            .unwrap_or(RepositoryStorageType::InMemory)
439    }
440
441    /// Gets the reset storage on start setting from environment variable or default
442    pub fn get_reset_storage_on_start() -> bool {
443        env::var("RESET_STORAGE_ON_START")
444            .map(|v| v.to_lowercase() == "true")
445            .unwrap_or(false)
446    }
447
448    /// Gets the storage encryption key from environment variable or None
449    pub fn get_storage_encryption_key() -> Option<SecretString> {
450        env::var("STORAGE_ENCRYPTION_KEY")
451            .map(|v| SecretString::new(&v))
452            .ok()
453    }
454
455    /// Gets the transaction expiration hours from environment variable or default
456    /// Supports fractional values (e.g., 0.1 = 6 minutes).
457    pub fn get_transaction_expiration_hours() -> f64 {
458        env::var("TRANSACTION_EXPIRATION_HOURS")
459            .unwrap_or_else(|_| "4".to_string())
460            .parse()
461            .unwrap_or(4.0)
462    }
463
464    /// Gets the allowed RPC hosts from environment variable or default (empty list)
465    pub fn get_rpc_allowed_hosts() -> Vec<String> {
466        env::var("RPC_ALLOWED_HOSTS")
467            .ok()
468            .map(|s| {
469                s.split(',')
470                    .map(|host| host.trim().to_string())
471                    .filter(|host| !host.is_empty())
472                    .collect()
473            })
474            .unwrap_or_default()
475    }
476
477    /// Gets the block private IPs setting from environment variable or default (false)
478    pub fn get_rpc_block_private_ips() -> bool {
479        env::var("RPC_BLOCK_PRIVATE_IPS")
480            .map(|v| v.to_lowercase() == "true")
481            .unwrap_or(false)
482    }
483
484    /// Gets the relayer concurrency limit from environment variable or default (100)
485    pub fn get_relayer_concurrency_limit() -> usize {
486        env::var("RELAYER_CONCURRENCY_LIMIT")
487            .unwrap_or_else(|_| "100".to_string())
488            .parse()
489            .unwrap_or(100)
490    }
491
492    /// Gets the max connections from environment variable or default (256)
493    pub fn get_max_connections() -> usize {
494        env::var("MAX_CONNECTIONS")
495            .unwrap_or_else(|_| "256".to_string())
496            .parse()
497            .unwrap_or(256)
498    }
499
500    /// Gets the connection backlog from environment variable or default (511)
501    ///
502    /// TCP listen backlog controls the size of the queue for pending connections.
503    /// Higher values allow more connections to be queued during traffic bursts,
504    /// preventing connection drops. Default of 511.
505    pub fn get_connection_backlog() -> u32 {
506        env::var("CONNECTION_BACKLOG")
507            .unwrap_or_else(|_| "511".to_string())
508            .parse()
509            .unwrap_or(511)
510    }
511
512    /// Gets the request timeout in seconds from environment variable or default (30)
513    ///
514    /// This is a security measure to prevent resource exhaustion attacks (DoS).
515    /// It limits how long a request handler can run, preventing slowloris-style
516    /// attacks and ensuring resources are freed promptly.
517    pub fn get_request_timeout_seconds() -> u64 {
518        env::var("REQUEST_TIMEOUT_SECONDS")
519            .unwrap_or_else(|_| "30".to_string())
520            .parse()
521            .unwrap_or(30)
522    }
523
524    // =========================================================================
525    // Stellar Contract Address Getters (raw env var reads)
526    // =========================================================================
527
528    pub fn get_stellar_mainnet_fee_forwarder_address() -> Option<String> {
529        env::var("STELLAR_MAINNET_FEE_FORWARDER_ADDRESS").ok()
530    }
531
532    pub fn get_stellar_testnet_fee_forwarder_address() -> Option<String> {
533        env::var("STELLAR_TESTNET_FEE_FORWARDER_ADDRESS").ok()
534    }
535
536    pub fn get_stellar_mainnet_soroswap_router_address() -> Option<String> {
537        env::var("STELLAR_MAINNET_SOROSWAP_ROUTER_ADDRESS").ok()
538    }
539
540    pub fn get_stellar_testnet_soroswap_router_address() -> Option<String> {
541        env::var("STELLAR_TESTNET_SOROSWAP_ROUTER_ADDRESS").ok()
542    }
543
544    pub fn get_stellar_mainnet_soroswap_factory_address() -> Option<String> {
545        env::var("STELLAR_MAINNET_SOROSWAP_FACTORY_ADDRESS").ok()
546    }
547
548    pub fn get_stellar_testnet_soroswap_factory_address() -> Option<String> {
549        env::var("STELLAR_TESTNET_SOROSWAP_FACTORY_ADDRESS").ok()
550    }
551
552    pub fn get_stellar_mainnet_soroswap_native_wrapper_address() -> Option<String> {
553        env::var("STELLAR_MAINNET_SOROSWAP_NATIVE_WRAPPER_ADDRESS").ok()
554    }
555
556    pub fn get_stellar_testnet_soroswap_native_wrapper_address() -> Option<String> {
557        env::var("STELLAR_TESTNET_SOROSWAP_NATIVE_WRAPPER_ADDRESS").ok()
558    }
559
560    // =========================================================================
561    // Stellar Contract Address Resolvers
562    // =========================================================================
563    // For mainnet: env var override → hardcoded default from constants.
564    // For testnet: env var only (no hardcoded defaults).
565
566    /// Resolves the FeeForwarder contract address for the given network.
567    pub fn resolve_stellar_fee_forwarder_address(is_testnet: bool) -> Option<String> {
568        if is_testnet {
569            Self::get_stellar_testnet_fee_forwarder_address()
570        } else {
571            Self::get_stellar_mainnet_fee_forwarder_address()
572                .or_else(|| non_empty_const(STELLAR_FEE_FORWARDER_MAINNET))
573        }
574    }
575
576    /// Resolves the Soroswap router contract address for the given network.
577    pub fn resolve_stellar_soroswap_router_address(is_testnet: bool) -> Option<String> {
578        if is_testnet {
579            Self::get_stellar_testnet_soroswap_router_address()
580        } else {
581            Self::get_stellar_mainnet_soroswap_router_address()
582                .or_else(|| Some(STELLAR_SOROSWAP_MAINNET_ROUTER.to_string()))
583        }
584    }
585
586    /// Resolves the Soroswap factory contract address for the given network.
587    pub fn resolve_stellar_soroswap_factory_address(is_testnet: bool) -> Option<String> {
588        if is_testnet {
589            Self::get_stellar_testnet_soroswap_factory_address()
590        } else {
591            Self::get_stellar_mainnet_soroswap_factory_address()
592                .or_else(|| Some(STELLAR_SOROSWAP_MAINNET_FACTORY.to_string()))
593        }
594    }
595
596    /// Resolves the Soroswap native wrapper token address for the given network.
597    pub fn resolve_stellar_soroswap_native_wrapper_address(is_testnet: bool) -> Option<String> {
598        if is_testnet {
599            Self::get_stellar_testnet_soroswap_native_wrapper_address()
600        } else {
601            Self::get_stellar_mainnet_soroswap_native_wrapper_address()
602                .or_else(|| Some(STELLAR_SOROSWAP_MAINNET_NATIVE_WRAPPER.to_string()))
603        }
604    }
605
606    /// Get worker concurrency from environment variable or use default
607    ///
608    /// Environment variable format: `BACKGROUND_WORKER_{WORKER_NAME}_CONCURRENCY`
609    /// Example: `BACKGROUND_WORKER_TRANSACTION_REQUEST_CONCURRENCY=20`
610    pub fn get_worker_concurrency(worker_name: &str, default: usize) -> usize {
611        let env_var = format!(
612            "BACKGROUND_WORKER_{}_CONCURRENCY",
613            worker_name.to_uppercase()
614        );
615        env::var(&env_var)
616            .ok()
617            .and_then(|v| v.parse().ok())
618            .unwrap_or(default)
619    }
620}
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625    use lazy_static::lazy_static;
626    use std::env;
627    use std::sync::Mutex;
628
629    // Use a mutex to ensure tests don't run in parallel when modifying env vars
630    lazy_static! {
631        static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
632    }
633
634    fn setup() {
635        // Clear all environment variables first
636        env::remove_var("HOST");
637        env::remove_var("APP_PORT");
638        env::remove_var("REDIS_URL");
639        env::remove_var("CONFIG_DIR");
640        env::remove_var("CONFIG_FILE_NAME");
641        env::remove_var("CONFIG_FILE_PATH");
642        env::remove_var("API_KEY");
643        env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
644        env::remove_var("RATE_LIMIT_BURST_SIZE");
645        env::remove_var("METRICS_PORT");
646        env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
647        env::remove_var("RPC_TIMEOUT_MS");
648        env::remove_var("PROVIDER_MAX_RETRIES");
649        env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
650        env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
651        env::remove_var("PROVIDER_MAX_FAILOVERS");
652        env::remove_var("REPOSITORY_STORAGE_TYPE");
653        env::remove_var("RESET_STORAGE_ON_START");
654        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
655        env::remove_var("REDIS_READER_URL");
656        // Set required variables for most tests
657        env::set_var("REDIS_URL", "redis://localhost:6379");
658        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
659        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
660    }
661
662    #[test]
663    fn test_default_values() {
664        let _lock = match ENV_MUTEX.lock() {
665            Ok(guard) => guard,
666            Err(poisoned) => poisoned.into_inner(),
667        };
668        setup();
669
670        let config = ServerConfig::from_env();
671
672        assert_eq!(config.host, "0.0.0.0");
673        assert_eq!(config.port, 8080);
674        assert_eq!(config.redis_url, "redis://localhost:6379");
675        assert_eq!(config.config_file_path, "./config/config.json");
676        assert_eq!(
677            config.api_key,
678            SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
679        );
680        assert_eq!(config.rate_limit_requests_per_second, 100);
681        assert_eq!(config.rate_limit_burst_size, 300);
682        assert_eq!(config.metrics_port, 8081);
683        assert_eq!(config.redis_connection_timeout_ms, 5000);
684        assert_eq!(config.rpc_timeout_ms, 10000);
685        assert_eq!(config.provider_max_retries, 3);
686        assert_eq!(config.provider_retry_base_delay_ms, 100);
687        assert_eq!(config.provider_retry_max_delay_ms, 2000);
688        assert_eq!(config.provider_max_failovers, 3);
689        assert_eq!(config.provider_failure_threshold, 3);
690        assert_eq!(config.provider_pause_duration_secs, 60);
691        assert_eq!(
692            config.repository_storage_type,
693            RepositoryStorageType::InMemory
694        );
695        assert!(!config.reset_storage_on_start);
696        assert_eq!(config.transaction_expiration_hours, 4.0);
697    }
698
699    #[test]
700    fn test_invalid_port_values() {
701        let _lock = match ENV_MUTEX.lock() {
702            Ok(guard) => guard,
703            Err(poisoned) => poisoned.into_inner(),
704        };
705        setup();
706        env::set_var("REDIS_URL", "redis://localhost:6379");
707        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
708        env::set_var("APP_PORT", "not_a_number");
709        env::set_var("METRICS_PORT", "also_not_a_number");
710        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "invalid");
711        env::set_var("RATE_LIMIT_BURST_SIZE", "invalid");
712        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "invalid");
713        env::set_var("RPC_TIMEOUT_MS", "invalid");
714        env::set_var("PROVIDER_MAX_RETRIES", "invalid");
715        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "invalid");
716        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "invalid");
717        env::set_var("PROVIDER_MAX_FAILOVERS", "invalid");
718        env::set_var("REPOSITORY_STORAGE_TYPE", "invalid");
719        env::set_var("RESET_STORAGE_ON_START", "invalid");
720        env::set_var("TRANSACTION_EXPIRATION_HOURS", "invalid");
721        let config = ServerConfig::from_env();
722
723        // Should fall back to defaults when parsing fails
724        assert_eq!(config.port, 8080);
725        assert_eq!(config.metrics_port, 8081);
726        assert_eq!(config.rate_limit_requests_per_second, 100);
727        assert_eq!(config.rate_limit_burst_size, 300);
728        assert_eq!(config.redis_connection_timeout_ms, 10000);
729        assert_eq!(config.rpc_timeout_ms, 10000);
730        assert_eq!(config.provider_max_retries, 3);
731        assert_eq!(config.provider_retry_base_delay_ms, 100);
732        assert_eq!(config.provider_retry_max_delay_ms, 2000);
733        assert_eq!(config.provider_max_failovers, 3);
734        assert_eq!(
735            config.repository_storage_type,
736            RepositoryStorageType::InMemory
737        );
738        assert!(!config.reset_storage_on_start);
739        assert_eq!(config.transaction_expiration_hours, 4.0);
740    }
741
742    #[test]
743    fn test_custom_values() {
744        let _lock = match ENV_MUTEX.lock() {
745            Ok(guard) => guard,
746            Err(poisoned) => poisoned.into_inner(),
747        };
748        setup();
749
750        env::set_var("HOST", "127.0.0.1");
751        env::set_var("APP_PORT", "9090");
752        env::set_var("REDIS_URL", "redis://custom:6379");
753        env::set_var("CONFIG_DIR", "custom");
754        env::set_var("CONFIG_FILE_NAME", "path.json");
755        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
756        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "200");
757        env::set_var("RATE_LIMIT_BURST_SIZE", "500");
758        env::set_var("METRICS_PORT", "9091");
759        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "10000");
760        env::set_var("RPC_TIMEOUT_MS", "33333");
761        env::set_var("PROVIDER_MAX_RETRIES", "5");
762        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
763        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "3000");
764        env::set_var("PROVIDER_MAX_FAILOVERS", "4");
765        env::set_var("REPOSITORY_STORAGE_TYPE", "in_memory");
766        env::set_var("RESET_STORAGE_ON_START", "true");
767        env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
768        let config = ServerConfig::from_env();
769
770        assert_eq!(config.host, "127.0.0.1");
771        assert_eq!(config.port, 9090);
772        assert_eq!(config.redis_url, "redis://custom:6379");
773        assert_eq!(config.config_file_path, "custom/path.json");
774        assert_eq!(
775            config.api_key,
776            SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
777        );
778        assert_eq!(config.rate_limit_requests_per_second, 200);
779        assert_eq!(config.rate_limit_burst_size, 500);
780        assert_eq!(config.metrics_port, 9091);
781        assert_eq!(config.redis_connection_timeout_ms, 10000);
782        assert_eq!(config.rpc_timeout_ms, 33333);
783        assert_eq!(config.provider_max_retries, 5);
784        assert_eq!(config.provider_retry_base_delay_ms, 200);
785        assert_eq!(config.provider_retry_max_delay_ms, 3000);
786        assert_eq!(config.provider_max_failovers, 4);
787        assert_eq!(
788            config.repository_storage_type,
789            RepositoryStorageType::InMemory
790        );
791        assert!(config.reset_storage_on_start);
792        assert_eq!(config.transaction_expiration_hours, 6.0);
793    }
794
795    #[test]
796    #[should_panic(expected = "Security error: API_KEY must be at least 32 characters long")]
797    fn test_invalid_api_key_length() {
798        let _lock = match ENV_MUTEX.lock() {
799            Ok(guard) => guard,
800            Err(poisoned) => poisoned.into_inner(),
801        };
802        setup();
803        env::set_var("REDIS_URL", "redis://localhost:6379");
804        env::set_var("API_KEY", "insufficient_length");
805        env::set_var("APP_PORT", "8080");
806        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "100");
807        env::set_var("RATE_LIMIT_BURST_SIZE", "300");
808        env::set_var("METRICS_PORT", "9091");
809        env::set_var("TRANSACTION_EXPIRATION_HOURS", "4");
810
811        let _ = ServerConfig::from_env();
812
813        panic!("Test should have panicked before reaching here");
814    }
815
816    // Tests for individual getter methods
817    #[test]
818    fn test_individual_getters_with_defaults() {
819        let _lock = match ENV_MUTEX.lock() {
820            Ok(guard) => guard,
821            Err(poisoned) => poisoned.into_inner(),
822        };
823
824        // Clear all environment variables to test defaults
825        env::remove_var("HOST");
826        env::remove_var("APP_PORT");
827        env::remove_var("REDIS_URL");
828        env::remove_var("CONFIG_DIR");
829        env::remove_var("CONFIG_FILE_NAME");
830        env::remove_var("API_KEY");
831        env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
832        env::remove_var("RATE_LIMIT_BURST_SIZE");
833        env::remove_var("METRICS_PORT");
834        env::remove_var("ENABLE_SWAGGER");
835        env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
836        env::remove_var("REDIS_KEY_PREFIX");
837        env::remove_var("REDIS_READER_URL");
838        env::remove_var("RPC_TIMEOUT_MS");
839        env::remove_var("PROVIDER_MAX_RETRIES");
840        env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
841        env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
842        env::remove_var("PROVIDER_MAX_FAILOVERS");
843        env::remove_var("REPOSITORY_STORAGE_TYPE");
844        env::remove_var("RESET_STORAGE_ON_START");
845        env::remove_var("STORAGE_ENCRYPTION_KEY");
846        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
847        env::remove_var("REDIS_POOL_MAX_SIZE");
848        env::remove_var("REDIS_POOL_TIMEOUT_MS");
849
850        // Test individual getters with defaults
851        assert_eq!(ServerConfig::get_host(), "0.0.0.0");
852        assert_eq!(ServerConfig::get_port(), 8080);
853        assert_eq!(ServerConfig::get_redis_url_optional(), None);
854        assert_eq!(ServerConfig::get_config_file_path(), "./config/config.json");
855        assert_eq!(ServerConfig::get_api_key_optional(), None);
856        assert_eq!(ServerConfig::get_rate_limit_requests_per_second(), 100);
857        assert_eq!(ServerConfig::get_rate_limit_burst_size(), 300);
858        assert_eq!(ServerConfig::get_metrics_port(), 8081);
859        assert!(!ServerConfig::get_enable_swagger());
860        assert_eq!(ServerConfig::get_redis_connection_timeout_ms(), 10000);
861        assert_eq!(ServerConfig::get_redis_key_prefix(), "oz-relayer");
862        assert_eq!(ServerConfig::get_rpc_timeout_ms(), 10000);
863        assert_eq!(ServerConfig::get_provider_max_retries(), 3);
864        assert_eq!(ServerConfig::get_provider_retry_base_delay_ms(), 100);
865        assert_eq!(ServerConfig::get_provider_retry_max_delay_ms(), 2000);
866        assert_eq!(ServerConfig::get_provider_max_failovers(), 3);
867        assert_eq!(
868            ServerConfig::get_repository_storage_type(),
869            RepositoryStorageType::InMemory
870        );
871        assert!(!ServerConfig::get_reset_storage_on_start());
872        assert!(ServerConfig::get_storage_encryption_key().is_none());
873        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 4.0);
874        assert_eq!(ServerConfig::get_redis_pool_max_size(), 500);
875        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 10000);
876    }
877
878    #[test]
879    fn test_individual_getters_with_custom_values() {
880        let _lock = match ENV_MUTEX.lock() {
881            Ok(guard) => guard,
882            Err(poisoned) => poisoned.into_inner(),
883        };
884
885        // Set custom values
886        env::set_var("HOST", "192.168.1.1");
887        env::set_var("APP_PORT", "9999");
888        env::set_var("REDIS_URL", "redis://custom:6379");
889        env::set_var("CONFIG_DIR", "/custom/config");
890        env::set_var("CONFIG_FILE_NAME", "custom.json");
891        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
892        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "500");
893        env::set_var("RATE_LIMIT_BURST_SIZE", "1000");
894        env::set_var("METRICS_PORT", "9999");
895        env::set_var("ENABLE_SWAGGER", "true");
896        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
897        env::set_var("REDIS_KEY_PREFIX", "custom-prefix");
898        env::set_var("RPC_TIMEOUT_MS", "15000");
899        env::set_var("PROVIDER_MAX_RETRIES", "5");
900        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
901        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "5000");
902        env::set_var("PROVIDER_MAX_FAILOVERS", "10");
903        env::set_var("REPOSITORY_STORAGE_TYPE", "redis");
904        env::set_var("RESET_STORAGE_ON_START", "true");
905        env::set_var("STORAGE_ENCRYPTION_KEY", "my-encryption-key");
906        env::set_var("TRANSACTION_EXPIRATION_HOURS", "12");
907        env::set_var("REDIS_POOL_MAX_SIZE", "200");
908        env::set_var("REDIS_POOL_TIMEOUT_MS", "20000");
909
910        // Test individual getters with custom values
911        assert_eq!(ServerConfig::get_host(), "192.168.1.1");
912        assert_eq!(ServerConfig::get_port(), 9999);
913        assert_eq!(
914            ServerConfig::get_redis_url_optional(),
915            Some("redis://custom:6379".to_string())
916        );
917        assert_eq!(
918            ServerConfig::get_config_file_path(),
919            "/custom/config/custom.json"
920        );
921        assert!(ServerConfig::get_api_key_optional().is_some());
922        assert_eq!(ServerConfig::get_rate_limit_requests_per_second(), 500);
923        assert_eq!(ServerConfig::get_rate_limit_burst_size(), 1000);
924        assert_eq!(ServerConfig::get_metrics_port(), 9999);
925        assert!(ServerConfig::get_enable_swagger());
926        assert_eq!(ServerConfig::get_redis_connection_timeout_ms(), 5000);
927        assert_eq!(ServerConfig::get_redis_key_prefix(), "custom-prefix");
928        assert_eq!(ServerConfig::get_rpc_timeout_ms(), 15000);
929        assert_eq!(ServerConfig::get_provider_max_retries(), 5);
930        assert_eq!(ServerConfig::get_provider_retry_base_delay_ms(), 200);
931        assert_eq!(ServerConfig::get_provider_retry_max_delay_ms(), 5000);
932        assert_eq!(ServerConfig::get_provider_max_failovers(), 10);
933        assert_eq!(
934            ServerConfig::get_repository_storage_type(),
935            RepositoryStorageType::Redis
936        );
937        assert!(ServerConfig::get_reset_storage_on_start());
938        assert!(ServerConfig::get_storage_encryption_key().is_some());
939        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 12.0);
940        assert_eq!(ServerConfig::get_redis_pool_max_size(), 200);
941        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 20000);
942    }
943
944    #[test]
945    fn test_get_redis_pool_max_size() {
946        let _lock = match ENV_MUTEX.lock() {
947            Ok(guard) => guard,
948            Err(poisoned) => poisoned.into_inner(),
949        };
950        // Test default value when env var is not set
951        env::remove_var("REDIS_POOL_MAX_SIZE");
952        assert_eq!(ServerConfig::get_redis_pool_max_size(), 500);
953
954        // Test custom value
955        env::set_var("REDIS_POOL_MAX_SIZE", "100");
956        assert_eq!(ServerConfig::get_redis_pool_max_size(), 100);
957
958        // Test invalid value returns default
959        env::set_var("REDIS_POOL_MAX_SIZE", "not_a_number");
960        assert_eq!(ServerConfig::get_redis_pool_max_size(), 500);
961
962        // Test zero value returns default (invalid)
963        env::set_var("REDIS_POOL_MAX_SIZE", "0");
964        assert_eq!(ServerConfig::get_redis_pool_max_size(), 500);
965
966        // Test large value
967        env::set_var("REDIS_POOL_MAX_SIZE", "10000");
968        assert_eq!(ServerConfig::get_redis_pool_max_size(), 10000);
969
970        // Cleanup
971        env::remove_var("REDIS_POOL_MAX_SIZE");
972    }
973
974    #[test]
975    fn test_get_redis_pool_timeout_ms() {
976        let _lock = match ENV_MUTEX.lock() {
977            Ok(guard) => guard,
978            Err(poisoned) => poisoned.into_inner(),
979        };
980
981        // Test default value when env var is not set
982        env::remove_var("REDIS_POOL_TIMEOUT_MS");
983        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 10000);
984
985        // Test custom value
986        env::set_var("REDIS_POOL_TIMEOUT_MS", "15000");
987        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 15000);
988
989        // Test invalid value returns default
990        env::set_var("REDIS_POOL_TIMEOUT_MS", "not_a_number");
991        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 10000);
992
993        // Test zero value returns default (invalid)
994        env::set_var("REDIS_POOL_TIMEOUT_MS", "0");
995        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 10000);
996
997        // Test large value
998        env::set_var("REDIS_POOL_TIMEOUT_MS", "60000");
999        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 60000);
1000
1001        // Cleanup
1002        env::remove_var("REDIS_POOL_TIMEOUT_MS");
1003    }
1004
1005    #[test]
1006    fn test_fractional_transaction_expiration_hours() {
1007        let _lock = match ENV_MUTEX.lock() {
1008            Ok(guard) => guard,
1009            Err(poisoned) => poisoned.into_inner(),
1010        };
1011        setup();
1012
1013        // Test fractional hours (0.1 hours = 6 minutes)
1014        env::set_var("TRANSACTION_EXPIRATION_HOURS", "0.1");
1015        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 0.1);
1016
1017        // Test another fractional value
1018        env::set_var("TRANSACTION_EXPIRATION_HOURS", "0.5");
1019        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 0.5);
1020
1021        // Test integer value still works
1022        env::set_var("TRANSACTION_EXPIRATION_HOURS", "24");
1023        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 24.0);
1024
1025        // Cleanup
1026        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1027    }
1028
1029    #[test]
1030    #[should_panic(expected = "REDIS_URL must be set")]
1031    fn test_get_redis_url_panics_when_not_set() {
1032        let _lock = match ENV_MUTEX.lock() {
1033            Ok(guard) => guard,
1034            Err(poisoned) => poisoned.into_inner(),
1035        };
1036
1037        env::remove_var("REDIS_URL");
1038        let _ = ServerConfig::get_redis_url();
1039    }
1040
1041    #[test]
1042    #[should_panic(expected = "API_KEY must be set")]
1043    fn test_get_api_key_panics_when_not_set() {
1044        let _lock = match ENV_MUTEX.lock() {
1045            Ok(guard) => guard,
1046            Err(poisoned) => poisoned.into_inner(),
1047        };
1048
1049        env::remove_var("API_KEY");
1050        let _ = ServerConfig::get_api_key();
1051    }
1052
1053    #[test]
1054    fn test_optional_getters_return_none_safely() {
1055        let _lock = match ENV_MUTEX.lock() {
1056            Ok(guard) => guard,
1057            Err(poisoned) => poisoned.into_inner(),
1058        };
1059
1060        env::remove_var("REDIS_URL");
1061        env::remove_var("API_KEY");
1062        env::remove_var("STORAGE_ENCRYPTION_KEY");
1063
1064        assert!(ServerConfig::get_redis_url_optional().is_none());
1065        assert!(ServerConfig::get_api_key_optional().is_none());
1066        assert!(ServerConfig::get_storage_encryption_key().is_none());
1067    }
1068
1069    #[test]
1070    fn test_refactored_from_env_equivalence() {
1071        let _lock = match ENV_MUTEX.lock() {
1072            Ok(guard) => guard,
1073            Err(poisoned) => poisoned.into_inner(),
1074        };
1075        setup();
1076
1077        // Set custom values to test both default and custom paths
1078        env::set_var("HOST", "custom-host");
1079        env::set_var("APP_PORT", "7777");
1080        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "250");
1081        env::set_var("METRICS_PORT", "7778");
1082        env::set_var("ENABLE_SWAGGER", "true");
1083        env::set_var("PROVIDER_MAX_RETRIES", "7");
1084        env::set_var("TRANSACTION_EXPIRATION_HOURS", "8");
1085
1086        let config = ServerConfig::from_env();
1087
1088        // Verify the refactored from_env() produces the same results as individual getters
1089        assert_eq!(config.host, ServerConfig::get_host());
1090        assert_eq!(config.port, ServerConfig::get_port());
1091        assert_eq!(config.redis_url, ServerConfig::get_redis_url());
1092        assert_eq!(
1093            config.config_file_path,
1094            ServerConfig::get_config_file_path()
1095        );
1096        assert_eq!(config.api_key, ServerConfig::get_api_key());
1097        assert_eq!(
1098            config.rate_limit_requests_per_second,
1099            ServerConfig::get_rate_limit_requests_per_second()
1100        );
1101        assert_eq!(
1102            config.rate_limit_burst_size,
1103            ServerConfig::get_rate_limit_burst_size()
1104        );
1105        assert_eq!(config.metrics_port, ServerConfig::get_metrics_port());
1106        assert_eq!(config.enable_swagger, ServerConfig::get_enable_swagger());
1107        assert_eq!(
1108            config.redis_connection_timeout_ms,
1109            ServerConfig::get_redis_connection_timeout_ms()
1110        );
1111        assert_eq!(
1112            config.redis_key_prefix,
1113            ServerConfig::get_redis_key_prefix()
1114        );
1115        assert_eq!(config.rpc_timeout_ms, ServerConfig::get_rpc_timeout_ms());
1116        assert_eq!(
1117            config.provider_max_retries,
1118            ServerConfig::get_provider_max_retries()
1119        );
1120        assert_eq!(
1121            config.provider_retry_base_delay_ms,
1122            ServerConfig::get_provider_retry_base_delay_ms()
1123        );
1124        assert_eq!(
1125            config.provider_retry_max_delay_ms,
1126            ServerConfig::get_provider_retry_max_delay_ms()
1127        );
1128        assert_eq!(
1129            config.provider_max_failovers,
1130            ServerConfig::get_provider_max_failovers()
1131        );
1132        assert_eq!(
1133            config.repository_storage_type,
1134            ServerConfig::get_repository_storage_type()
1135        );
1136        assert_eq!(
1137            config.reset_storage_on_start,
1138            ServerConfig::get_reset_storage_on_start()
1139        );
1140        assert_eq!(
1141            config.storage_encryption_key,
1142            ServerConfig::get_storage_encryption_key()
1143        );
1144        assert_eq!(
1145            config.transaction_expiration_hours,
1146            ServerConfig::get_transaction_expiration_hours()
1147        );
1148    }
1149
1150    mod get_worker_concurrency_tests {
1151        use super::*;
1152        use serial_test::serial;
1153
1154        #[test]
1155        #[serial]
1156        fn test_returns_default_when_env_not_set() {
1157            let worker_name = "test_worker";
1158            let env_var = format!(
1159                "BACKGROUND_WORKER_{}_CONCURRENCY",
1160                worker_name.to_uppercase()
1161            );
1162
1163            // Ensure env var is not set
1164            env::remove_var(&env_var);
1165
1166            let default_value = 42;
1167            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1168
1169            assert_eq!(
1170                result, default_value,
1171                "Should return default value when env var is not set"
1172            );
1173        }
1174
1175        #[test]
1176        #[serial]
1177        fn test_returns_env_value_when_set() {
1178            let worker_name = "status_checker";
1179            let env_var = format!(
1180                "BACKGROUND_WORKER_{}_CONCURRENCY",
1181                worker_name.to_uppercase()
1182            );
1183
1184            // Set env var to a specific value
1185            env::set_var(&env_var, "100");
1186
1187            let default_value = 10;
1188            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1189
1190            assert_eq!(result, 100, "Should return env var value when set");
1191
1192            // Cleanup
1193            env::remove_var(&env_var);
1194        }
1195
1196        #[test]
1197        #[serial]
1198        fn test_returns_default_when_env_invalid() {
1199            let worker_name = "invalid_worker";
1200            let env_var = format!(
1201                "BACKGROUND_WORKER_{}_CONCURRENCY",
1202                worker_name.to_uppercase()
1203            );
1204
1205            // Set env var to invalid value
1206            env::set_var(&env_var, "not_a_number");
1207
1208            let default_value = 25;
1209            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1210
1211            assert_eq!(
1212                result, default_value,
1213                "Should return default value when env var is invalid"
1214            );
1215
1216            // Cleanup
1217            env::remove_var(&env_var);
1218        }
1219
1220        #[test]
1221        #[serial]
1222        fn test_returns_default_when_env_empty() {
1223            let worker_name = "empty_worker";
1224            let env_var = format!(
1225                "BACKGROUND_WORKER_{}_CONCURRENCY",
1226                worker_name.to_uppercase()
1227            );
1228
1229            // Set env var to empty string
1230            env::set_var(&env_var, "");
1231
1232            let default_value = 15;
1233            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1234
1235            assert_eq!(
1236                result, default_value,
1237                "Should return default value when env var is empty"
1238            );
1239
1240            // Cleanup
1241            env::remove_var(&env_var);
1242        }
1243
1244        #[test]
1245        #[serial]
1246        fn test_returns_default_when_env_negative() {
1247            let worker_name = "negative_worker";
1248            let env_var = format!(
1249                "BACKGROUND_WORKER_{}_CONCURRENCY",
1250                worker_name.to_uppercase()
1251            );
1252
1253            // Set env var to negative value
1254            env::set_var(&env_var, "-5");
1255
1256            let default_value = 20;
1257            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1258
1259            assert_eq!(
1260                result, default_value,
1261                "Should return default value when env var is negative"
1262            );
1263
1264            // Cleanup
1265            env::remove_var(&env_var);
1266        }
1267
1268        #[test]
1269        #[serial]
1270        fn test_env_var_name_formatting() {
1271            // Test that worker names are properly uppercased
1272            let worker_names = vec![
1273                (
1274                    "transaction_sender",
1275                    "BACKGROUND_WORKER_TRANSACTION_SENDER_CONCURRENCY",
1276                ),
1277                (
1278                    "status_checker_evm",
1279                    "BACKGROUND_WORKER_STATUS_CHECKER_EVM_CONCURRENCY",
1280                ),
1281                (
1282                    "notification_sender",
1283                    "BACKGROUND_WORKER_NOTIFICATION_SENDER_CONCURRENCY",
1284                ),
1285            ];
1286
1287            for (worker_name, expected_env_var) in worker_names {
1288                let actual_env_var = format!(
1289                    "BACKGROUND_WORKER_{}_CONCURRENCY",
1290                    worker_name.to_uppercase()
1291                );
1292                assert_eq!(
1293                    actual_env_var, expected_env_var,
1294                    "Env var name should be correctly formatted for worker: {}",
1295                    worker_name
1296                );
1297            }
1298        }
1299
1300        #[test]
1301        #[serial]
1302        fn test_zero_value() {
1303            let worker_name = "zero_worker";
1304            let env_var = format!(
1305                "BACKGROUND_WORKER_{}_CONCURRENCY",
1306                worker_name.to_uppercase()
1307            );
1308
1309            // Set env var to zero
1310            env::set_var(&env_var, "0");
1311
1312            let default_value = 30;
1313            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1314
1315            assert_eq!(result, 0, "Should accept zero as a valid value");
1316
1317            // Cleanup
1318            env::remove_var(&env_var);
1319        }
1320
1321        #[test]
1322        #[serial]
1323        fn test_large_value() {
1324            let worker_name = "large_worker";
1325            let env_var = format!(
1326                "BACKGROUND_WORKER_{}_CONCURRENCY",
1327                worker_name.to_uppercase()
1328            );
1329
1330            // Set env var to a large value
1331            env::set_var(&env_var, "10000");
1332
1333            let default_value = 50;
1334            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1335
1336            assert_eq!(result, 10000, "Should accept large values");
1337
1338            // Cleanup
1339            env::remove_var(&env_var);
1340        }
1341
1342        #[test]
1343        #[serial]
1344        fn test_whitespace_in_value() {
1345            let worker_name = "whitespace_worker";
1346            let env_var = format!(
1347                "BACKGROUND_WORKER_{}_CONCURRENCY",
1348                worker_name.to_uppercase()
1349            );
1350
1351            // Set env var with leading/trailing whitespace
1352            env::set_var(&env_var, "  75  ");
1353
1354            let default_value = 35;
1355            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1356
1357            // Note: String::parse::<usize>() does NOT trim whitespace, so this will fail to parse
1358            // and return the default value
1359            assert_eq!(
1360                result, default_value,
1361                "Should return default value when value has whitespace"
1362            );
1363
1364            // Cleanup
1365            env::remove_var(&env_var);
1366        }
1367
1368        #[test]
1369        #[serial]
1370        fn test_float_value_returns_default() {
1371            let worker_name = "float_worker";
1372            let env_var = format!(
1373                "BACKGROUND_WORKER_{}_CONCURRENCY",
1374                worker_name.to_uppercase()
1375            );
1376
1377            // Set env var to float value
1378            env::set_var(&env_var, "12.5");
1379
1380            let default_value = 40;
1381            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1382
1383            assert_eq!(
1384                result, default_value,
1385                "Should return default value for float input"
1386            );
1387
1388            // Cleanup
1389            env::remove_var(&env_var);
1390        }
1391    }
1392
1393    mod get_relayer_concurrency_limit_tests {
1394        use super::*;
1395        use serial_test::serial;
1396
1397        #[test]
1398        #[serial]
1399        fn test_returns_default_when_env_not_set() {
1400            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1401            let result = ServerConfig::get_relayer_concurrency_limit();
1402            assert_eq!(result, 100, "Should return default value of 100");
1403        }
1404
1405        #[test]
1406        #[serial]
1407        fn test_returns_env_value_when_set() {
1408            env::set_var("RELAYER_CONCURRENCY_LIMIT", "250");
1409            let result = ServerConfig::get_relayer_concurrency_limit();
1410            assert_eq!(result, 250, "Should return env var value");
1411            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1412        }
1413
1414        #[test]
1415        #[serial]
1416        fn test_returns_default_when_env_invalid() {
1417            env::set_var("RELAYER_CONCURRENCY_LIMIT", "not_a_number");
1418            let result = ServerConfig::get_relayer_concurrency_limit();
1419            assert_eq!(result, 100, "Should return default value when invalid");
1420            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1421        }
1422
1423        #[test]
1424        #[serial]
1425        fn test_returns_default_when_env_empty() {
1426            env::set_var("RELAYER_CONCURRENCY_LIMIT", "");
1427            let result = ServerConfig::get_relayer_concurrency_limit();
1428            assert_eq!(result, 100, "Should return default value when empty");
1429            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1430        }
1431
1432        #[test]
1433        #[serial]
1434        fn test_zero_value() {
1435            env::set_var("RELAYER_CONCURRENCY_LIMIT", "0");
1436            let result = ServerConfig::get_relayer_concurrency_limit();
1437            assert_eq!(result, 0, "Should accept zero as valid value");
1438            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1439        }
1440
1441        #[test]
1442        #[serial]
1443        fn test_large_value() {
1444            env::set_var("RELAYER_CONCURRENCY_LIMIT", "5000");
1445            let result = ServerConfig::get_relayer_concurrency_limit();
1446            assert_eq!(result, 5000, "Should accept large values");
1447            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1448        }
1449
1450        #[test]
1451        #[serial]
1452        fn test_negative_value_returns_default() {
1453            env::set_var("RELAYER_CONCURRENCY_LIMIT", "-10");
1454            let result = ServerConfig::get_relayer_concurrency_limit();
1455            assert_eq!(result, 100, "Should return default for negative value");
1456            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1457        }
1458
1459        #[test]
1460        #[serial]
1461        fn test_float_value_returns_default() {
1462            env::set_var("RELAYER_CONCURRENCY_LIMIT", "100.5");
1463            let result = ServerConfig::get_relayer_concurrency_limit();
1464            assert_eq!(result, 100, "Should return default for float value");
1465            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1466        }
1467
1468        #[test]
1469        #[serial]
1470        fn test_whitespace_value_returns_default() {
1471            env::set_var("RELAYER_CONCURRENCY_LIMIT", "  150  ");
1472            let result = ServerConfig::get_relayer_concurrency_limit();
1473            assert_eq!(
1474                result, 100,
1475                "Should return default when value has whitespace"
1476            );
1477            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1478        }
1479    }
1480
1481    mod get_max_connections_tests {
1482        use super::*;
1483        use serial_test::serial;
1484
1485        #[test]
1486        #[serial]
1487        fn test_returns_default_when_env_not_set() {
1488            env::remove_var("MAX_CONNECTIONS");
1489            let result = ServerConfig::get_max_connections();
1490            assert_eq!(result, 256, "Should return default value of 256");
1491        }
1492
1493        #[test]
1494        #[serial]
1495        fn test_returns_env_value_when_set() {
1496            env::set_var("MAX_CONNECTIONS", "512");
1497            let result = ServerConfig::get_max_connections();
1498            assert_eq!(result, 512, "Should return env var value");
1499            env::remove_var("MAX_CONNECTIONS");
1500        }
1501
1502        #[test]
1503        #[serial]
1504        fn test_returns_default_when_env_invalid() {
1505            env::set_var("MAX_CONNECTIONS", "invalid");
1506            let result = ServerConfig::get_max_connections();
1507            assert_eq!(result, 256, "Should return default value when invalid");
1508            env::remove_var("MAX_CONNECTIONS");
1509        }
1510
1511        #[test]
1512        #[serial]
1513        fn test_returns_default_when_env_empty() {
1514            env::set_var("MAX_CONNECTIONS", "");
1515            let result = ServerConfig::get_max_connections();
1516            assert_eq!(result, 256, "Should return default value when empty");
1517            env::remove_var("MAX_CONNECTIONS");
1518        }
1519
1520        #[test]
1521        #[serial]
1522        fn test_zero_value() {
1523            env::set_var("MAX_CONNECTIONS", "0");
1524            let result = ServerConfig::get_max_connections();
1525            assert_eq!(result, 0, "Should accept zero as valid value");
1526            env::remove_var("MAX_CONNECTIONS");
1527        }
1528
1529        #[test]
1530        #[serial]
1531        fn test_large_value() {
1532            env::set_var("MAX_CONNECTIONS", "10000");
1533            let result = ServerConfig::get_max_connections();
1534            assert_eq!(result, 10000, "Should accept large values");
1535            env::remove_var("MAX_CONNECTIONS");
1536        }
1537
1538        #[test]
1539        #[serial]
1540        fn test_negative_value_returns_default() {
1541            env::set_var("MAX_CONNECTIONS", "-100");
1542            let result = ServerConfig::get_max_connections();
1543            assert_eq!(result, 256, "Should return default for negative value");
1544            env::remove_var("MAX_CONNECTIONS");
1545        }
1546
1547        #[test]
1548        #[serial]
1549        fn test_float_value_returns_default() {
1550            env::set_var("MAX_CONNECTIONS", "256.5");
1551            let result = ServerConfig::get_max_connections();
1552            assert_eq!(result, 256, "Should return default for float value");
1553            env::remove_var("MAX_CONNECTIONS");
1554        }
1555    }
1556
1557    mod get_connection_backlog_tests {
1558        use super::*;
1559        use serial_test::serial;
1560
1561        #[test]
1562        #[serial]
1563        fn test_returns_default_when_env_not_set() {
1564            env::remove_var("CONNECTION_BACKLOG");
1565            let result = ServerConfig::get_connection_backlog();
1566            assert_eq!(result, 511, "Should return default value of 511");
1567        }
1568
1569        #[test]
1570        #[serial]
1571        fn test_returns_env_value_when_set() {
1572            env::set_var("CONNECTION_BACKLOG", "1024");
1573            let result = ServerConfig::get_connection_backlog();
1574            assert_eq!(result, 1024, "Should return env var value");
1575            env::remove_var("CONNECTION_BACKLOG");
1576        }
1577
1578        #[test]
1579        #[serial]
1580        fn test_returns_default_when_env_invalid() {
1581            env::set_var("CONNECTION_BACKLOG", "not_a_number");
1582            let result = ServerConfig::get_connection_backlog();
1583            assert_eq!(result, 511, "Should return default value when invalid");
1584            env::remove_var("CONNECTION_BACKLOG");
1585        }
1586
1587        #[test]
1588        #[serial]
1589        fn test_returns_default_when_env_empty() {
1590            env::set_var("CONNECTION_BACKLOG", "");
1591            let result = ServerConfig::get_connection_backlog();
1592            assert_eq!(result, 511, "Should return default value when empty");
1593            env::remove_var("CONNECTION_BACKLOG");
1594        }
1595
1596        #[test]
1597        #[serial]
1598        fn test_zero_value() {
1599            env::set_var("CONNECTION_BACKLOG", "0");
1600            let result = ServerConfig::get_connection_backlog();
1601            assert_eq!(result, 0, "Should accept zero as valid value");
1602            env::remove_var("CONNECTION_BACKLOG");
1603        }
1604
1605        #[test]
1606        #[serial]
1607        fn test_large_value() {
1608            env::set_var("CONNECTION_BACKLOG", "65535");
1609            let result = ServerConfig::get_connection_backlog();
1610            assert_eq!(result, 65535, "Should accept large values");
1611            env::remove_var("CONNECTION_BACKLOG");
1612        }
1613
1614        #[test]
1615        #[serial]
1616        fn test_negative_value_returns_default() {
1617            env::set_var("CONNECTION_BACKLOG", "-50");
1618            let result = ServerConfig::get_connection_backlog();
1619            assert_eq!(result, 511, "Should return default for negative value");
1620            env::remove_var("CONNECTION_BACKLOG");
1621        }
1622
1623        #[test]
1624        #[serial]
1625        fn test_float_value_returns_default() {
1626            env::set_var("CONNECTION_BACKLOG", "511.5");
1627            let result = ServerConfig::get_connection_backlog();
1628            assert_eq!(result, 511, "Should return default for float value");
1629            env::remove_var("CONNECTION_BACKLOG");
1630        }
1631
1632        #[test]
1633        #[serial]
1634        fn test_common_production_values() {
1635            // Test common production values
1636            let test_cases = vec![
1637                (128, "Small server"),
1638                (511, "Default"),
1639                (1024, "Medium server"),
1640                (2048, "Large server"),
1641            ];
1642
1643            for (value, description) in test_cases {
1644                env::set_var("CONNECTION_BACKLOG", value.to_string());
1645                let result = ServerConfig::get_connection_backlog();
1646                assert_eq!(result, value, "Should accept {}: {}", description, value);
1647            }
1648
1649            env::remove_var("CONNECTION_BACKLOG");
1650        }
1651    }
1652
1653    mod get_request_timeout_seconds_tests {
1654        use super::*;
1655        use serial_test::serial;
1656
1657        #[test]
1658        #[serial]
1659        fn test_returns_default_when_env_not_set() {
1660            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1661            let result = ServerConfig::get_request_timeout_seconds();
1662            assert_eq!(result, 30, "Should return default value of 30");
1663        }
1664
1665        #[test]
1666        #[serial]
1667        fn test_returns_env_value_when_set() {
1668            env::set_var("REQUEST_TIMEOUT_SECONDS", "60");
1669            let result = ServerConfig::get_request_timeout_seconds();
1670            assert_eq!(result, 60, "Should return env var value");
1671            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1672        }
1673
1674        #[test]
1675        #[serial]
1676        fn test_returns_default_when_env_invalid() {
1677            env::set_var("REQUEST_TIMEOUT_SECONDS", "invalid");
1678            let result = ServerConfig::get_request_timeout_seconds();
1679            assert_eq!(result, 30, "Should return default value when invalid");
1680            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1681        }
1682
1683        #[test]
1684        #[serial]
1685        fn test_returns_default_when_env_empty() {
1686            env::set_var("REQUEST_TIMEOUT_SECONDS", "");
1687            let result = ServerConfig::get_request_timeout_seconds();
1688            assert_eq!(result, 30, "Should return default value when empty");
1689            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1690        }
1691
1692        #[test]
1693        #[serial]
1694        fn test_zero_value() {
1695            env::set_var("REQUEST_TIMEOUT_SECONDS", "0");
1696            let result = ServerConfig::get_request_timeout_seconds();
1697            assert_eq!(result, 0, "Should accept zero as valid value");
1698            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1699        }
1700
1701        #[test]
1702        #[serial]
1703        fn test_large_value() {
1704            env::set_var("REQUEST_TIMEOUT_SECONDS", "300");
1705            let result = ServerConfig::get_request_timeout_seconds();
1706            assert_eq!(result, 300, "Should accept large values");
1707            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1708        }
1709
1710        #[test]
1711        #[serial]
1712        fn test_negative_value_returns_default() {
1713            env::set_var("REQUEST_TIMEOUT_SECONDS", "-10");
1714            let result = ServerConfig::get_request_timeout_seconds();
1715            assert_eq!(result, 30, "Should return default for negative value");
1716            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1717        }
1718
1719        #[test]
1720        #[serial]
1721        fn test_float_value_returns_default() {
1722            env::set_var("REQUEST_TIMEOUT_SECONDS", "30.5");
1723            let result = ServerConfig::get_request_timeout_seconds();
1724            assert_eq!(result, 30, "Should return default for float value");
1725            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1726        }
1727
1728        #[test]
1729        #[serial]
1730        fn test_common_timeout_values() {
1731            // Test common timeout values
1732            let test_cases = vec![
1733                (10, "Short timeout"),
1734                (30, "Default timeout"),
1735                (60, "Moderate timeout"),
1736                (120, "Long timeout"),
1737            ];
1738
1739            for (value, description) in test_cases {
1740                env::set_var("REQUEST_TIMEOUT_SECONDS", value.to_string());
1741                let result = ServerConfig::get_request_timeout_seconds();
1742                assert_eq!(result, value, "Should accept {}: {}", description, value);
1743            }
1744
1745            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1746        }
1747    }
1748
1749    mod get_redis_reader_url_tests {
1750        use super::*;
1751        use serial_test::serial;
1752
1753        #[test]
1754        #[serial]
1755        fn test_returns_none_when_env_not_set() {
1756            env::remove_var("REDIS_READER_URL");
1757            let result = ServerConfig::get_redis_reader_url_optional();
1758            assert!(
1759                result.is_none(),
1760                "Should return None when env var is not set"
1761            );
1762        }
1763
1764        #[test]
1765        #[serial]
1766        fn test_returns_value_when_set() {
1767            env::set_var("REDIS_READER_URL", "redis://reader:6379");
1768            let result = ServerConfig::get_redis_reader_url_optional();
1769            assert_eq!(
1770                result,
1771                Some("redis://reader:6379".to_string()),
1772                "Should return the env var value"
1773            );
1774            env::remove_var("REDIS_READER_URL");
1775        }
1776
1777        #[test]
1778        #[serial]
1779        fn test_returns_empty_string_when_set_to_empty() {
1780            env::set_var("REDIS_READER_URL", "");
1781            let result = ServerConfig::get_redis_reader_url_optional();
1782            assert_eq!(
1783                result,
1784                Some("".to_string()),
1785                "Should return empty string when set to empty"
1786            );
1787            env::remove_var("REDIS_READER_URL");
1788        }
1789
1790        #[test]
1791        #[serial]
1792        fn test_aws_elasticache_reader_url() {
1793            // Test with typical AWS ElastiCache reader endpoint format
1794            let reader_url = "redis://my-cluster-ro.xxx.cache.amazonaws.com:6379";
1795            env::set_var("REDIS_READER_URL", reader_url);
1796            let result = ServerConfig::get_redis_reader_url_optional();
1797            assert_eq!(
1798                result,
1799                Some(reader_url.to_string()),
1800                "Should accept AWS ElastiCache reader endpoint"
1801            );
1802            env::remove_var("REDIS_READER_URL");
1803        }
1804
1805        #[test]
1806        #[serial]
1807        fn test_config_includes_redis_reader_url() {
1808            env::set_var("REDIS_URL", "redis://primary:6379");
1809            env::set_var("REDIS_READER_URL", "redis://reader:6379");
1810            env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
1811
1812            let config = ServerConfig::from_env();
1813
1814            assert_eq!(config.redis_url, "redis://primary:6379");
1815            assert_eq!(
1816                config.redis_reader_url,
1817                Some("redis://reader:6379".to_string())
1818            );
1819
1820            env::remove_var("REDIS_URL");
1821            env::remove_var("REDIS_READER_URL");
1822            env::remove_var("API_KEY");
1823        }
1824
1825        #[test]
1826        #[serial]
1827        fn test_config_without_redis_reader_url() {
1828            env::set_var("REDIS_URL", "redis://primary:6379");
1829            env::remove_var("REDIS_READER_URL");
1830            env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
1831
1832            let config = ServerConfig::from_env();
1833
1834            assert_eq!(config.redis_url, "redis://primary:6379");
1835            assert!(
1836                config.redis_reader_url.is_none(),
1837                "redis_reader_url should be None when not set"
1838            );
1839
1840            env::remove_var("REDIS_URL");
1841            env::remove_var("API_KEY");
1842        }
1843    }
1844
1845    mod get_redis_reader_pool_max_size_tests {
1846        use super::*;
1847        use serial_test::serial;
1848
1849        #[test]
1850        #[serial]
1851        fn test_returns_default_when_env_not_set() {
1852            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1853            let result = ServerConfig::get_redis_reader_pool_max_size();
1854            assert_eq!(
1855                result, 1000,
1856                "Should return default 1000 when env var is not set"
1857            );
1858        }
1859
1860        #[test]
1861        #[serial]
1862        fn test_returns_value_when_set() {
1863            env::set_var("REDIS_READER_POOL_MAX_SIZE", "2000");
1864            let result = ServerConfig::get_redis_reader_pool_max_size();
1865            assert_eq!(result, 2000, "Should return the parsed value");
1866            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1867        }
1868
1869        #[test]
1870        #[serial]
1871        fn test_returns_default_when_invalid() {
1872            env::set_var("REDIS_READER_POOL_MAX_SIZE", "not_a_number");
1873            let result = ServerConfig::get_redis_reader_pool_max_size();
1874            assert_eq!(
1875                result, 1000,
1876                "Should return default 1000 for invalid values"
1877            );
1878            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1879        }
1880
1881        #[test]
1882        #[serial]
1883        fn test_returns_default_when_zero() {
1884            env::set_var("REDIS_READER_POOL_MAX_SIZE", "0");
1885            let result = ServerConfig::get_redis_reader_pool_max_size();
1886            assert_eq!(result, 1000, "Should return default 1000 when value is 0");
1887            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1888        }
1889
1890        #[test]
1891        #[serial]
1892        fn test_returns_default_when_negative() {
1893            env::set_var("REDIS_READER_POOL_MAX_SIZE", "-100");
1894            let result = ServerConfig::get_redis_reader_pool_max_size();
1895            assert_eq!(
1896                result, 1000,
1897                "Should return default 1000 for negative values"
1898            );
1899            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1900        }
1901
1902        #[test]
1903        #[serial]
1904        fn test_config_includes_reader_pool_max_size() {
1905            env::set_var("REDIS_URL", "redis://primary:6379");
1906            env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
1907            env::set_var("REDIS_READER_POOL_MAX_SIZE", "750");
1908
1909            let config = ServerConfig::from_env();
1910
1911            assert_eq!(
1912                config.redis_reader_pool_max_size, 750,
1913                "Should include reader pool max size in config"
1914            );
1915
1916            env::remove_var("REDIS_URL");
1917            env::remove_var("API_KEY");
1918            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1919        }
1920
1921        #[test]
1922        #[serial]
1923        fn test_config_uses_default_when_not_set() {
1924            env::set_var("REDIS_URL", "redis://primary:6379");
1925            env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
1926            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1927
1928            let config = ServerConfig::from_env();
1929
1930            assert_eq!(
1931                config.redis_reader_pool_max_size, 1000,
1932                "Should use default 1000 when not set"
1933            );
1934
1935            env::remove_var("REDIS_URL");
1936            env::remove_var("API_KEY");
1937        }
1938    }
1939}