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 queue backend from environment variable or default.
265    ///
266    /// Supported values: "redis", "sqs"
267    /// Defaults to "redis" when not set.
268    pub fn get_queue_backend() -> String {
269        env::var("QUEUE_BACKEND").unwrap_or_else(|_| "redis".to_string())
270    }
271
272    /// Gets the SQS queue type from environment variable or default.
273    ///
274    /// Supported values: "auto" (default), "standard", "fifo"
275    /// - `auto`: auto-detect by probing queues at startup
276    /// - `standard` / `fifo`: skip probing, use the specified type directly
277    pub fn get_sqs_queue_type() -> String {
278        env::var("SQS_QUEUE_TYPE").unwrap_or_else(|_| "auto".to_string())
279    }
280
281    /// Gets the AWS region from environment variable.
282    ///
283    /// Required when using SQS queue backend.
284    ///
285    /// # Errors
286    ///
287    /// Returns error if AWS_REGION is not set.
288    pub fn get_aws_region() -> Result<String, String> {
289        env::var("AWS_REGION")
290            .map_err(|_| "AWS_REGION not set. Required for SQS backend.".to_string())
291    }
292
293    /// Gets the AWS account ID from environment variable.
294    ///
295    /// Required when using SQS queue backend and SQS_QUEUE_URL_PREFIX is not provided.
296    ///
297    /// # Errors
298    ///
299    /// Returns error if AWS_ACCOUNT_ID is not set.
300    pub fn get_aws_account_id() -> Result<String, String> {
301        env::var("AWS_ACCOUNT_ID").map_err(|_| {
302            "AWS_ACCOUNT_ID not set. Required when SQS_QUEUE_URL_PREFIX is not provided."
303                .to_string()
304        })
305    }
306
307    /// Gets the API key from environment variable (panics if not set or too short)
308    pub fn get_api_key() -> SecretString {
309        let api_key = SecretString::new(&env::var("API_KEY").expect("API_KEY must be set"));
310
311        if !api_key.has_minimum_length(MINIMUM_SECRET_VALUE_LENGTH) {
312            panic!(
313                "Security error: API_KEY must be at least {MINIMUM_SECRET_VALUE_LENGTH} characters long"
314            );
315        }
316
317        api_key
318    }
319
320    /// Gets the API key from environment variable or returns None if not set or invalid
321    pub fn get_api_key_optional() -> Option<SecretString> {
322        env::var("API_KEY")
323            .ok()
324            .map(|key| SecretString::new(&key))
325            .filter(|key| key.has_minimum_length(MINIMUM_SECRET_VALUE_LENGTH))
326    }
327
328    /// Gets the rate limit requests per second from environment variable or default
329    pub fn get_rate_limit_requests_per_second() -> u64 {
330        env::var("RATE_LIMIT_REQUESTS_PER_SECOND")
331            .unwrap_or_else(|_| "100".to_string())
332            .parse()
333            .unwrap_or(100)
334    }
335
336    /// Gets the rate limit burst size from environment variable or default
337    pub fn get_rate_limit_burst_size() -> u32 {
338        env::var("RATE_LIMIT_BURST_SIZE")
339            .unwrap_or_else(|_| "300".to_string())
340            .parse()
341            .unwrap_or(300)
342    }
343
344    /// Gets the metrics port from environment variable or default
345    pub fn get_metrics_port() -> u16 {
346        env::var("METRICS_PORT")
347            .unwrap_or_else(|_| "8081".to_string())
348            .parse()
349            .unwrap_or(8081)
350    }
351
352    /// Gets the enable swagger setting from environment variable or default
353    pub fn get_enable_swagger() -> bool {
354        env::var("ENABLE_SWAGGER")
355            .map(|v| v.to_lowercase() == "true")
356            .unwrap_or(false)
357    }
358
359    /// Gets the Redis connection timeout from environment variable or default
360    pub fn get_redis_connection_timeout_ms() -> u64 {
361        env::var("REDIS_CONNECTION_TIMEOUT_MS")
362            .unwrap_or_else(|_| "10000".to_string())
363            .parse()
364            .unwrap_or(10000)
365    }
366
367    /// Gets the Redis key prefix from environment variable or default
368    pub fn get_redis_key_prefix() -> String {
369        env::var("REDIS_KEY_PREFIX").unwrap_or_else(|_| "oz-relayer".to_string())
370    }
371
372    /// Gets the Redis pool max size from environment variable or default
373    /// Returns default (500) if value is 0 or invalid
374    pub fn get_redis_pool_max_size() -> usize {
375        env::var("REDIS_POOL_MAX_SIZE")
376            .unwrap_or_else(|_| "500".to_string())
377            .parse()
378            .ok()
379            .filter(|&v| v > 0)
380            .unwrap_or(500)
381    }
382
383    /// Gets the Redis reader pool max size from environment variable.
384    /// Returns 1000 if not set or invalid.
385    pub fn get_redis_reader_pool_max_size() -> usize {
386        env::var("REDIS_READER_POOL_MAX_SIZE")
387            .ok()
388            .and_then(|v| v.parse().ok())
389            .filter(|&v| v > 0)
390            .unwrap_or(1000)
391    }
392
393    /// Gets the Redis pool timeout from environment variable or default
394    /// Returns default (10000) if value is 0 or invalid
395    pub fn get_redis_pool_timeout_ms() -> u64 {
396        env::var("REDIS_POOL_TIMEOUT_MS")
397            .unwrap_or_else(|_| "10000".to_string())
398            .parse()
399            .ok()
400            .filter(|&v| v > 0)
401            .unwrap_or(10000)
402    }
403
404    /// Gets the RPC timeout from environment variable or default
405    pub fn get_rpc_timeout_ms() -> u64 {
406        env::var("RPC_TIMEOUT_MS")
407            .unwrap_or_else(|_| "10000".to_string())
408            .parse()
409            .unwrap_or(10000)
410    }
411
412    /// Gets the provider max retries from environment variable or default
413    pub fn get_provider_max_retries() -> u8 {
414        env::var("PROVIDER_MAX_RETRIES")
415            .unwrap_or_else(|_| "3".to_string())
416            .parse()
417            .unwrap_or(3)
418    }
419
420    /// Gets the provider retry base delay from environment variable or default
421    pub fn get_provider_retry_base_delay_ms() -> u64 {
422        env::var("PROVIDER_RETRY_BASE_DELAY_MS")
423            .unwrap_or_else(|_| "100".to_string())
424            .parse()
425            .unwrap_or(100)
426    }
427
428    /// Gets the provider retry max delay from environment variable or default
429    pub fn get_provider_retry_max_delay_ms() -> u64 {
430        env::var("PROVIDER_RETRY_MAX_DELAY_MS")
431            .unwrap_or_else(|_| "2000".to_string())
432            .parse()
433            .unwrap_or(2000)
434    }
435
436    /// Gets the provider max failovers from environment variable or default
437    pub fn get_provider_max_failovers() -> u8 {
438        env::var("PROVIDER_MAX_FAILOVERS")
439            .unwrap_or_else(|_| "3".to_string())
440            .parse()
441            .unwrap_or(3)
442    }
443
444    /// Gets the provider failure threshold from environment variable or default
445    pub fn get_provider_failure_threshold() -> u32 {
446        env::var("PROVIDER_FAILURE_THRESHOLD")
447            .or_else(|_| env::var("RPC_FAILURE_THRESHOLD")) // Support legacy env var
448            .unwrap_or_else(|_| DEFAULT_PROVIDER_FAILURE_THRESHOLD.to_string())
449            .parse()
450            .unwrap_or(DEFAULT_PROVIDER_FAILURE_THRESHOLD)
451    }
452
453    /// Gets the provider pause duration in seconds from environment variable or default
454    ///
455    /// Defaults to 60 seconds (1 minute) for faster recovery while still providing
456    /// a reasonable cooldown period for failed providers.
457    pub fn get_provider_pause_duration_secs() -> u64 {
458        env::var("PROVIDER_PAUSE_DURATION_SECS")
459            .or_else(|_| env::var("RPC_PAUSE_DURATION_SECS")) // Support legacy env var
460            .unwrap_or_else(|_| DEFAULT_PROVIDER_PAUSE_DURATION_SECS.to_string())
461            .parse()
462            .unwrap_or(DEFAULT_PROVIDER_PAUSE_DURATION_SECS)
463    }
464
465    /// Gets the provider failure expiration duration in seconds from environment variable or default
466    ///
467    /// Defaults to 60 seconds (1 minute). Failures older than this are considered stale
468    /// and reset, allowing providers to naturally recover over time.
469    pub fn get_provider_failure_expiration_secs() -> u64 {
470        env::var("PROVIDER_FAILURE_EXPIRATION_SECS")
471            .unwrap_or_else(|_| DEFAULT_PROVIDER_FAILURE_EXPIRATION_SECS.to_string())
472            .parse()
473            .unwrap_or(DEFAULT_PROVIDER_FAILURE_EXPIRATION_SECS)
474    }
475
476    /// Gets the repository storage type from environment variable or default
477    pub fn get_repository_storage_type() -> RepositoryStorageType {
478        env::var("REPOSITORY_STORAGE_TYPE")
479            .unwrap_or_else(|_| "in_memory".to_string())
480            .parse()
481            .unwrap_or(RepositoryStorageType::InMemory)
482    }
483
484    /// Gets the reset storage on start setting from environment variable or default
485    pub fn get_reset_storage_on_start() -> bool {
486        env::var("RESET_STORAGE_ON_START")
487            .map(|v| v.to_lowercase() == "true")
488            .unwrap_or(false)
489    }
490
491    /// Gets the storage encryption key from environment variable or None
492    pub fn get_storage_encryption_key() -> Option<SecretString> {
493        env::var("STORAGE_ENCRYPTION_KEY")
494            .map(|v| SecretString::new(&v))
495            .ok()
496    }
497
498    /// Gets the transaction expiration hours from environment variable or default
499    /// Supports fractional values (e.g., 0.1 = 6 minutes).
500    pub fn get_transaction_expiration_hours() -> f64 {
501        env::var("TRANSACTION_EXPIRATION_HOURS")
502            .unwrap_or_else(|_| "4".to_string())
503            .parse()
504            .unwrap_or(4.0)
505    }
506
507    /// Gets the allowed RPC hosts from environment variable or default (empty list)
508    pub fn get_rpc_allowed_hosts() -> Vec<String> {
509        env::var("RPC_ALLOWED_HOSTS")
510            .ok()
511            .map(|s| {
512                s.split(',')
513                    .map(|host| host.trim().to_string())
514                    .filter(|host| !host.is_empty())
515                    .collect()
516            })
517            .unwrap_or_default()
518    }
519
520    /// Gets the block private IPs setting from environment variable or default (false)
521    pub fn get_rpc_block_private_ips() -> bool {
522        env::var("RPC_BLOCK_PRIVATE_IPS")
523            .map(|v| v.to_lowercase() == "true")
524            .unwrap_or(false)
525    }
526
527    /// Gets the relayer concurrency limit from environment variable or default (100)
528    pub fn get_relayer_concurrency_limit() -> usize {
529        env::var("RELAYER_CONCURRENCY_LIMIT")
530            .unwrap_or_else(|_| "100".to_string())
531            .parse()
532            .unwrap_or(100)
533    }
534
535    /// Gets the max connections from environment variable or default (256)
536    pub fn get_max_connections() -> usize {
537        env::var("MAX_CONNECTIONS")
538            .unwrap_or_else(|_| "256".to_string())
539            .parse()
540            .unwrap_or(256)
541    }
542
543    /// Gets the connection backlog from environment variable or default (511)
544    ///
545    /// TCP listen backlog controls the size of the queue for pending connections.
546    /// Higher values allow more connections to be queued during traffic bursts,
547    /// preventing connection drops. Default of 511.
548    pub fn get_connection_backlog() -> u32 {
549        env::var("CONNECTION_BACKLOG")
550            .unwrap_or_else(|_| "511".to_string())
551            .parse()
552            .unwrap_or(511)
553    }
554
555    /// Gets the request timeout in seconds from environment variable or default (30)
556    ///
557    /// This is a security measure to prevent resource exhaustion attacks (DoS).
558    /// It limits how long a request handler can run, preventing slowloris-style
559    /// attacks and ensuring resources are freed promptly.
560    pub fn get_request_timeout_seconds() -> u64 {
561        env::var("REQUEST_TIMEOUT_SECONDS")
562            .unwrap_or_else(|_| "30".to_string())
563            .parse()
564            .unwrap_or(30)
565    }
566
567    /// Gets whether distributed mode is enabled from the `DISTRIBUTED_MODE` environment variable.
568    ///
569    /// When `true`, distributed locks are used to coordinate across multiple instances
570    /// (e.g., preventing duplicate cron execution in multi-instance deployments).
571    /// When `false` (default), locks are skipped — appropriate for single-instance deployments.
572    ///
573    /// Defaults to `false`.
574    pub fn get_distributed_mode() -> bool {
575        env::var("DISTRIBUTED_MODE")
576            .map(|v| v.eq_ignore_ascii_case("true") || v == "1")
577            .unwrap_or(false)
578    }
579
580    // =========================================================================
581    // Stellar Contract Address Getters (raw env var reads)
582    // =========================================================================
583
584    pub fn get_stellar_mainnet_fee_forwarder_address() -> Option<String> {
585        env::var("STELLAR_MAINNET_FEE_FORWARDER_ADDRESS").ok()
586    }
587
588    pub fn get_stellar_testnet_fee_forwarder_address() -> Option<String> {
589        env::var("STELLAR_TESTNET_FEE_FORWARDER_ADDRESS").ok()
590    }
591
592    pub fn get_stellar_mainnet_soroswap_router_address() -> Option<String> {
593        env::var("STELLAR_MAINNET_SOROSWAP_ROUTER_ADDRESS").ok()
594    }
595
596    pub fn get_stellar_testnet_soroswap_router_address() -> Option<String> {
597        env::var("STELLAR_TESTNET_SOROSWAP_ROUTER_ADDRESS").ok()
598    }
599
600    pub fn get_stellar_mainnet_soroswap_factory_address() -> Option<String> {
601        env::var("STELLAR_MAINNET_SOROSWAP_FACTORY_ADDRESS").ok()
602    }
603
604    pub fn get_stellar_testnet_soroswap_factory_address() -> Option<String> {
605        env::var("STELLAR_TESTNET_SOROSWAP_FACTORY_ADDRESS").ok()
606    }
607
608    pub fn get_stellar_mainnet_soroswap_native_wrapper_address() -> Option<String> {
609        env::var("STELLAR_MAINNET_SOROSWAP_NATIVE_WRAPPER_ADDRESS").ok()
610    }
611
612    pub fn get_stellar_testnet_soroswap_native_wrapper_address() -> Option<String> {
613        env::var("STELLAR_TESTNET_SOROSWAP_NATIVE_WRAPPER_ADDRESS").ok()
614    }
615
616    // =========================================================================
617    // Stellar Contract Address Resolvers
618    // =========================================================================
619    // For mainnet: env var override → hardcoded default from constants.
620    // For testnet: env var only (no hardcoded defaults).
621
622    /// Resolves the FeeForwarder contract address for the given network.
623    pub fn resolve_stellar_fee_forwarder_address(is_testnet: bool) -> Option<String> {
624        if is_testnet {
625            Self::get_stellar_testnet_fee_forwarder_address()
626        } else {
627            Self::get_stellar_mainnet_fee_forwarder_address()
628                .or_else(|| non_empty_const(STELLAR_FEE_FORWARDER_MAINNET))
629        }
630    }
631
632    /// Resolves the Soroswap router contract address for the given network.
633    pub fn resolve_stellar_soroswap_router_address(is_testnet: bool) -> Option<String> {
634        if is_testnet {
635            Self::get_stellar_testnet_soroswap_router_address()
636        } else {
637            Self::get_stellar_mainnet_soroswap_router_address()
638                .or_else(|| Some(STELLAR_SOROSWAP_MAINNET_ROUTER.to_string()))
639        }
640    }
641
642    /// Resolves the Soroswap factory contract address for the given network.
643    pub fn resolve_stellar_soroswap_factory_address(is_testnet: bool) -> Option<String> {
644        if is_testnet {
645            Self::get_stellar_testnet_soroswap_factory_address()
646        } else {
647            Self::get_stellar_mainnet_soroswap_factory_address()
648                .or_else(|| Some(STELLAR_SOROSWAP_MAINNET_FACTORY.to_string()))
649        }
650    }
651
652    /// Resolves the Soroswap native wrapper token address for the given network.
653    pub fn resolve_stellar_soroswap_native_wrapper_address(is_testnet: bool) -> Option<String> {
654        if is_testnet {
655            Self::get_stellar_testnet_soroswap_native_wrapper_address()
656        } else {
657            Self::get_stellar_mainnet_soroswap_native_wrapper_address()
658                .or_else(|| Some(STELLAR_SOROSWAP_MAINNET_NATIVE_WRAPPER.to_string()))
659        }
660    }
661
662    /// Get worker concurrency from environment variable or use default
663    ///
664    /// Environment variable format: `BACKGROUND_WORKER_{WORKER_NAME}_CONCURRENCY`
665    /// Example: `BACKGROUND_WORKER_TRANSACTION_REQUEST_CONCURRENCY=20`
666    pub fn get_worker_concurrency(worker_name: &str, default: usize) -> usize {
667        let env_var = format!(
668            "BACKGROUND_WORKER_{}_CONCURRENCY",
669            worker_name.to_uppercase()
670        );
671        env::var(&env_var)
672            .ok()
673            .and_then(|v| v.parse().ok())
674            .unwrap_or(default)
675    }
676
677    /// Get SQS wait time from environment variable or use default.
678    ///
679    /// Environment variable format: `SQS_{QUEUE_KEY}_WAIT_TIME_SECONDS`
680    /// Example: `SQS_TRANSACTION_REQUEST_WAIT_TIME_SECONDS=2`
681    ///
682    /// Values are clamped to the SQS maximum of 20 seconds.
683    pub fn get_sqs_wait_time(queue_key: &str, default: u64) -> u64 {
684        let env_var = format!("SQS_{queue_key}_WAIT_TIME_SECONDS");
685        env::var(&env_var)
686            .ok()
687            .and_then(|v| v.parse().ok())
688            .unwrap_or(default)
689            .min(20)
690    }
691
692    /// Get SQS poller count from environment variable or use default.
693    ///
694    /// Environment variable format: `SQS_{QUEUE_KEY}_POLLER_COUNT`
695    /// Example: `SQS_TRANSACTION_REQUEST_POLLER_COUNT=4`
696    ///
697    /// Controls how many concurrent SQS `ReceiveMessage` loops run per queue
698    /// per task. More pollers improve pickup smoothness on bursty queues.
699    /// All pollers share the same concurrency semaphore.
700    pub fn get_sqs_poller_count(queue_key: &str, default: usize) -> usize {
701        let env_var = format!("SQS_{queue_key}_POLLER_COUNT");
702        env::var(&env_var)
703            .ok()
704            .and_then(|v| v.parse().ok())
705            .unwrap_or(default)
706            .max(1)
707    }
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713    use lazy_static::lazy_static;
714    use std::env;
715    use std::sync::Mutex;
716
717    // Use a mutex to ensure tests don't run in parallel when modifying env vars
718    lazy_static! {
719        static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
720    }
721
722    fn setup() {
723        // Clear all environment variables first
724        env::remove_var("HOST");
725        env::remove_var("APP_PORT");
726        env::remove_var("REDIS_URL");
727        env::remove_var("CONFIG_DIR");
728        env::remove_var("CONFIG_FILE_NAME");
729        env::remove_var("CONFIG_FILE_PATH");
730        env::remove_var("API_KEY");
731        env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
732        env::remove_var("RATE_LIMIT_BURST_SIZE");
733        env::remove_var("METRICS_PORT");
734        env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
735        env::remove_var("RPC_TIMEOUT_MS");
736        env::remove_var("PROVIDER_MAX_RETRIES");
737        env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
738        env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
739        env::remove_var("PROVIDER_MAX_FAILOVERS");
740        env::remove_var("REPOSITORY_STORAGE_TYPE");
741        env::remove_var("RESET_STORAGE_ON_START");
742        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
743        env::remove_var("REDIS_READER_URL");
744        // Set required variables for most tests
745        env::set_var("REDIS_URL", "redis://localhost:6379");
746        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
747        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
748    }
749
750    #[test]
751    fn test_default_values() {
752        let _lock = match ENV_MUTEX.lock() {
753            Ok(guard) => guard,
754            Err(poisoned) => poisoned.into_inner(),
755        };
756        setup();
757
758        let config = ServerConfig::from_env();
759
760        assert_eq!(config.host, "0.0.0.0");
761        assert_eq!(config.port, 8080);
762        assert_eq!(config.redis_url, "redis://localhost:6379");
763        assert_eq!(config.config_file_path, "./config/config.json");
764        assert_eq!(
765            config.api_key,
766            SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
767        );
768        assert_eq!(config.rate_limit_requests_per_second, 100);
769        assert_eq!(config.rate_limit_burst_size, 300);
770        assert_eq!(config.metrics_port, 8081);
771        assert_eq!(config.redis_connection_timeout_ms, 5000);
772        assert_eq!(config.rpc_timeout_ms, 10000);
773        assert_eq!(config.provider_max_retries, 3);
774        assert_eq!(config.provider_retry_base_delay_ms, 100);
775        assert_eq!(config.provider_retry_max_delay_ms, 2000);
776        assert_eq!(config.provider_max_failovers, 3);
777        assert_eq!(config.provider_failure_threshold, 3);
778        assert_eq!(config.provider_pause_duration_secs, 60);
779        assert_eq!(
780            config.repository_storage_type,
781            RepositoryStorageType::InMemory
782        );
783        assert!(!config.reset_storage_on_start);
784        assert_eq!(config.transaction_expiration_hours, 4.0);
785    }
786
787    #[test]
788    fn test_invalid_port_values() {
789        let _lock = match ENV_MUTEX.lock() {
790            Ok(guard) => guard,
791            Err(poisoned) => poisoned.into_inner(),
792        };
793        setup();
794        env::set_var("REDIS_URL", "redis://localhost:6379");
795        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
796        env::set_var("APP_PORT", "not_a_number");
797        env::set_var("METRICS_PORT", "also_not_a_number");
798        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "invalid");
799        env::set_var("RATE_LIMIT_BURST_SIZE", "invalid");
800        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "invalid");
801        env::set_var("RPC_TIMEOUT_MS", "invalid");
802        env::set_var("PROVIDER_MAX_RETRIES", "invalid");
803        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "invalid");
804        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "invalid");
805        env::set_var("PROVIDER_MAX_FAILOVERS", "invalid");
806        env::set_var("REPOSITORY_STORAGE_TYPE", "invalid");
807        env::set_var("RESET_STORAGE_ON_START", "invalid");
808        env::set_var("TRANSACTION_EXPIRATION_HOURS", "invalid");
809        let config = ServerConfig::from_env();
810
811        // Should fall back to defaults when parsing fails
812        assert_eq!(config.port, 8080);
813        assert_eq!(config.metrics_port, 8081);
814        assert_eq!(config.rate_limit_requests_per_second, 100);
815        assert_eq!(config.rate_limit_burst_size, 300);
816        assert_eq!(config.redis_connection_timeout_ms, 10000);
817        assert_eq!(config.rpc_timeout_ms, 10000);
818        assert_eq!(config.provider_max_retries, 3);
819        assert_eq!(config.provider_retry_base_delay_ms, 100);
820        assert_eq!(config.provider_retry_max_delay_ms, 2000);
821        assert_eq!(config.provider_max_failovers, 3);
822        assert_eq!(
823            config.repository_storage_type,
824            RepositoryStorageType::InMemory
825        );
826        assert!(!config.reset_storage_on_start);
827        assert_eq!(config.transaction_expiration_hours, 4.0);
828    }
829
830    #[test]
831    fn test_custom_values() {
832        let _lock = match ENV_MUTEX.lock() {
833            Ok(guard) => guard,
834            Err(poisoned) => poisoned.into_inner(),
835        };
836        setup();
837
838        env::set_var("HOST", "127.0.0.1");
839        env::set_var("APP_PORT", "9090");
840        env::set_var("REDIS_URL", "redis://custom:6379");
841        env::set_var("CONFIG_DIR", "custom");
842        env::set_var("CONFIG_FILE_NAME", "path.json");
843        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
844        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "200");
845        env::set_var("RATE_LIMIT_BURST_SIZE", "500");
846        env::set_var("METRICS_PORT", "9091");
847        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "10000");
848        env::set_var("RPC_TIMEOUT_MS", "33333");
849        env::set_var("PROVIDER_MAX_RETRIES", "5");
850        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
851        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "3000");
852        env::set_var("PROVIDER_MAX_FAILOVERS", "4");
853        env::set_var("REPOSITORY_STORAGE_TYPE", "in_memory");
854        env::set_var("RESET_STORAGE_ON_START", "true");
855        env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
856        let config = ServerConfig::from_env();
857
858        assert_eq!(config.host, "127.0.0.1");
859        assert_eq!(config.port, 9090);
860        assert_eq!(config.redis_url, "redis://custom:6379");
861        assert_eq!(config.config_file_path, "custom/path.json");
862        assert_eq!(
863            config.api_key,
864            SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
865        );
866        assert_eq!(config.rate_limit_requests_per_second, 200);
867        assert_eq!(config.rate_limit_burst_size, 500);
868        assert_eq!(config.metrics_port, 9091);
869        assert_eq!(config.redis_connection_timeout_ms, 10000);
870        assert_eq!(config.rpc_timeout_ms, 33333);
871        assert_eq!(config.provider_max_retries, 5);
872        assert_eq!(config.provider_retry_base_delay_ms, 200);
873        assert_eq!(config.provider_retry_max_delay_ms, 3000);
874        assert_eq!(config.provider_max_failovers, 4);
875        assert_eq!(
876            config.repository_storage_type,
877            RepositoryStorageType::InMemory
878        );
879        assert!(config.reset_storage_on_start);
880        assert_eq!(config.transaction_expiration_hours, 6.0);
881    }
882
883    #[test]
884    #[should_panic(expected = "Security error: API_KEY must be at least 32 characters long")]
885    fn test_invalid_api_key_length() {
886        let _lock = match ENV_MUTEX.lock() {
887            Ok(guard) => guard,
888            Err(poisoned) => poisoned.into_inner(),
889        };
890        setup();
891        env::set_var("REDIS_URL", "redis://localhost:6379");
892        env::set_var("API_KEY", "insufficient_length");
893        env::set_var("APP_PORT", "8080");
894        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "100");
895        env::set_var("RATE_LIMIT_BURST_SIZE", "300");
896        env::set_var("METRICS_PORT", "9091");
897        env::set_var("TRANSACTION_EXPIRATION_HOURS", "4");
898
899        let _ = ServerConfig::from_env();
900
901        panic!("Test should have panicked before reaching here");
902    }
903
904    // Tests for individual getter methods
905    #[test]
906    fn test_individual_getters_with_defaults() {
907        let _lock = match ENV_MUTEX.lock() {
908            Ok(guard) => guard,
909            Err(poisoned) => poisoned.into_inner(),
910        };
911
912        // Clear all environment variables to test defaults
913        env::remove_var("HOST");
914        env::remove_var("APP_PORT");
915        env::remove_var("REDIS_URL");
916        env::remove_var("CONFIG_DIR");
917        env::remove_var("CONFIG_FILE_NAME");
918        env::remove_var("API_KEY");
919        env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
920        env::remove_var("RATE_LIMIT_BURST_SIZE");
921        env::remove_var("METRICS_PORT");
922        env::remove_var("ENABLE_SWAGGER");
923        env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
924        env::remove_var("REDIS_KEY_PREFIX");
925        env::remove_var("REDIS_READER_URL");
926        env::remove_var("RPC_TIMEOUT_MS");
927        env::remove_var("PROVIDER_MAX_RETRIES");
928        env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
929        env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
930        env::remove_var("PROVIDER_MAX_FAILOVERS");
931        env::remove_var("REPOSITORY_STORAGE_TYPE");
932        env::remove_var("RESET_STORAGE_ON_START");
933        env::remove_var("STORAGE_ENCRYPTION_KEY");
934        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
935        env::remove_var("REDIS_POOL_MAX_SIZE");
936        env::remove_var("REDIS_POOL_TIMEOUT_MS");
937
938        // Test individual getters with defaults
939        assert_eq!(ServerConfig::get_host(), "0.0.0.0");
940        assert_eq!(ServerConfig::get_port(), 8080);
941        assert_eq!(ServerConfig::get_redis_url_optional(), None);
942        assert_eq!(ServerConfig::get_config_file_path(), "./config/config.json");
943        assert_eq!(ServerConfig::get_api_key_optional(), None);
944        assert_eq!(ServerConfig::get_rate_limit_requests_per_second(), 100);
945        assert_eq!(ServerConfig::get_rate_limit_burst_size(), 300);
946        assert_eq!(ServerConfig::get_metrics_port(), 8081);
947        assert!(!ServerConfig::get_enable_swagger());
948        assert_eq!(ServerConfig::get_redis_connection_timeout_ms(), 10000);
949        assert_eq!(ServerConfig::get_redis_key_prefix(), "oz-relayer");
950        assert_eq!(ServerConfig::get_rpc_timeout_ms(), 10000);
951        assert_eq!(ServerConfig::get_provider_max_retries(), 3);
952        assert_eq!(ServerConfig::get_provider_retry_base_delay_ms(), 100);
953        assert_eq!(ServerConfig::get_provider_retry_max_delay_ms(), 2000);
954        assert_eq!(ServerConfig::get_provider_max_failovers(), 3);
955        assert_eq!(
956            ServerConfig::get_repository_storage_type(),
957            RepositoryStorageType::InMemory
958        );
959        assert!(!ServerConfig::get_reset_storage_on_start());
960        assert!(ServerConfig::get_storage_encryption_key().is_none());
961        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 4.0);
962        assert_eq!(ServerConfig::get_redis_pool_max_size(), 500);
963        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 10000);
964    }
965
966    #[test]
967    fn test_individual_getters_with_custom_values() {
968        let _lock = match ENV_MUTEX.lock() {
969            Ok(guard) => guard,
970            Err(poisoned) => poisoned.into_inner(),
971        };
972
973        // Set custom values
974        env::set_var("HOST", "192.168.1.1");
975        env::set_var("APP_PORT", "9999");
976        env::set_var("REDIS_URL", "redis://custom:6379");
977        env::set_var("CONFIG_DIR", "/custom/config");
978        env::set_var("CONFIG_FILE_NAME", "custom.json");
979        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
980        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "500");
981        env::set_var("RATE_LIMIT_BURST_SIZE", "1000");
982        env::set_var("METRICS_PORT", "9999");
983        env::set_var("ENABLE_SWAGGER", "true");
984        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
985        env::set_var("REDIS_KEY_PREFIX", "custom-prefix");
986        env::set_var("RPC_TIMEOUT_MS", "15000");
987        env::set_var("PROVIDER_MAX_RETRIES", "5");
988        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
989        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "5000");
990        env::set_var("PROVIDER_MAX_FAILOVERS", "10");
991        env::set_var("REPOSITORY_STORAGE_TYPE", "redis");
992        env::set_var("RESET_STORAGE_ON_START", "true");
993        env::set_var("STORAGE_ENCRYPTION_KEY", "my-encryption-key");
994        env::set_var("TRANSACTION_EXPIRATION_HOURS", "12");
995        env::set_var("REDIS_POOL_MAX_SIZE", "200");
996        env::set_var("REDIS_POOL_TIMEOUT_MS", "20000");
997
998        // Test individual getters with custom values
999        assert_eq!(ServerConfig::get_host(), "192.168.1.1");
1000        assert_eq!(ServerConfig::get_port(), 9999);
1001        assert_eq!(
1002            ServerConfig::get_redis_url_optional(),
1003            Some("redis://custom:6379".to_string())
1004        );
1005        assert_eq!(
1006            ServerConfig::get_config_file_path(),
1007            "/custom/config/custom.json"
1008        );
1009        assert!(ServerConfig::get_api_key_optional().is_some());
1010        assert_eq!(ServerConfig::get_rate_limit_requests_per_second(), 500);
1011        assert_eq!(ServerConfig::get_rate_limit_burst_size(), 1000);
1012        assert_eq!(ServerConfig::get_metrics_port(), 9999);
1013        assert!(ServerConfig::get_enable_swagger());
1014        assert_eq!(ServerConfig::get_redis_connection_timeout_ms(), 5000);
1015        assert_eq!(ServerConfig::get_redis_key_prefix(), "custom-prefix");
1016        assert_eq!(ServerConfig::get_rpc_timeout_ms(), 15000);
1017        assert_eq!(ServerConfig::get_provider_max_retries(), 5);
1018        assert_eq!(ServerConfig::get_provider_retry_base_delay_ms(), 200);
1019        assert_eq!(ServerConfig::get_provider_retry_max_delay_ms(), 5000);
1020        assert_eq!(ServerConfig::get_provider_max_failovers(), 10);
1021        assert_eq!(
1022            ServerConfig::get_repository_storage_type(),
1023            RepositoryStorageType::Redis
1024        );
1025        assert!(ServerConfig::get_reset_storage_on_start());
1026        assert!(ServerConfig::get_storage_encryption_key().is_some());
1027        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 12.0);
1028        assert_eq!(ServerConfig::get_redis_pool_max_size(), 200);
1029        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 20000);
1030    }
1031
1032    #[test]
1033    fn test_get_redis_pool_max_size() {
1034        let _lock = match ENV_MUTEX.lock() {
1035            Ok(guard) => guard,
1036            Err(poisoned) => poisoned.into_inner(),
1037        };
1038        // Test default value when env var is not set
1039        env::remove_var("REDIS_POOL_MAX_SIZE");
1040        assert_eq!(ServerConfig::get_redis_pool_max_size(), 500);
1041
1042        // Test custom value
1043        env::set_var("REDIS_POOL_MAX_SIZE", "100");
1044        assert_eq!(ServerConfig::get_redis_pool_max_size(), 100);
1045
1046        // Test invalid value returns default
1047        env::set_var("REDIS_POOL_MAX_SIZE", "not_a_number");
1048        assert_eq!(ServerConfig::get_redis_pool_max_size(), 500);
1049
1050        // Test zero value returns default (invalid)
1051        env::set_var("REDIS_POOL_MAX_SIZE", "0");
1052        assert_eq!(ServerConfig::get_redis_pool_max_size(), 500);
1053
1054        // Test large value
1055        env::set_var("REDIS_POOL_MAX_SIZE", "10000");
1056        assert_eq!(ServerConfig::get_redis_pool_max_size(), 10000);
1057
1058        // Cleanup
1059        env::remove_var("REDIS_POOL_MAX_SIZE");
1060    }
1061
1062    #[test]
1063    fn test_get_redis_pool_timeout_ms() {
1064        let _lock = match ENV_MUTEX.lock() {
1065            Ok(guard) => guard,
1066            Err(poisoned) => poisoned.into_inner(),
1067        };
1068
1069        // Test default value when env var is not set
1070        env::remove_var("REDIS_POOL_TIMEOUT_MS");
1071        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 10000);
1072
1073        // Test custom value
1074        env::set_var("REDIS_POOL_TIMEOUT_MS", "15000");
1075        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 15000);
1076
1077        // Test invalid value returns default
1078        env::set_var("REDIS_POOL_TIMEOUT_MS", "not_a_number");
1079        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 10000);
1080
1081        // Test zero value returns default (invalid)
1082        env::set_var("REDIS_POOL_TIMEOUT_MS", "0");
1083        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 10000);
1084
1085        // Test large value
1086        env::set_var("REDIS_POOL_TIMEOUT_MS", "60000");
1087        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 60000);
1088
1089        // Cleanup
1090        env::remove_var("REDIS_POOL_TIMEOUT_MS");
1091    }
1092
1093    #[test]
1094    fn test_fractional_transaction_expiration_hours() {
1095        let _lock = match ENV_MUTEX.lock() {
1096            Ok(guard) => guard,
1097            Err(poisoned) => poisoned.into_inner(),
1098        };
1099        setup();
1100
1101        // Test fractional hours (0.1 hours = 6 minutes)
1102        env::set_var("TRANSACTION_EXPIRATION_HOURS", "0.1");
1103        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 0.1);
1104
1105        // Test another fractional value
1106        env::set_var("TRANSACTION_EXPIRATION_HOURS", "0.5");
1107        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 0.5);
1108
1109        // Test integer value still works
1110        env::set_var("TRANSACTION_EXPIRATION_HOURS", "24");
1111        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 24.0);
1112
1113        // Cleanup
1114        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1115    }
1116
1117    #[test]
1118    #[should_panic(expected = "REDIS_URL must be set")]
1119    fn test_get_redis_url_panics_when_not_set() {
1120        let _lock = match ENV_MUTEX.lock() {
1121            Ok(guard) => guard,
1122            Err(poisoned) => poisoned.into_inner(),
1123        };
1124
1125        env::remove_var("REDIS_URL");
1126        let _ = ServerConfig::get_redis_url();
1127    }
1128
1129    #[test]
1130    #[should_panic(expected = "API_KEY must be set")]
1131    fn test_get_api_key_panics_when_not_set() {
1132        let _lock = match ENV_MUTEX.lock() {
1133            Ok(guard) => guard,
1134            Err(poisoned) => poisoned.into_inner(),
1135        };
1136
1137        env::remove_var("API_KEY");
1138        let _ = ServerConfig::get_api_key();
1139    }
1140
1141    #[test]
1142    fn test_optional_getters_return_none_safely() {
1143        let _lock = match ENV_MUTEX.lock() {
1144            Ok(guard) => guard,
1145            Err(poisoned) => poisoned.into_inner(),
1146        };
1147
1148        env::remove_var("REDIS_URL");
1149        env::remove_var("API_KEY");
1150        env::remove_var("STORAGE_ENCRYPTION_KEY");
1151
1152        assert!(ServerConfig::get_redis_url_optional().is_none());
1153        assert!(ServerConfig::get_api_key_optional().is_none());
1154        assert!(ServerConfig::get_storage_encryption_key().is_none());
1155    }
1156
1157    #[test]
1158    fn test_refactored_from_env_equivalence() {
1159        let _lock = match ENV_MUTEX.lock() {
1160            Ok(guard) => guard,
1161            Err(poisoned) => poisoned.into_inner(),
1162        };
1163        setup();
1164
1165        // Set custom values to test both default and custom paths
1166        env::set_var("HOST", "custom-host");
1167        env::set_var("APP_PORT", "7777");
1168        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "250");
1169        env::set_var("METRICS_PORT", "7778");
1170        env::set_var("ENABLE_SWAGGER", "true");
1171        env::set_var("PROVIDER_MAX_RETRIES", "7");
1172        env::set_var("TRANSACTION_EXPIRATION_HOURS", "8");
1173
1174        let config = ServerConfig::from_env();
1175
1176        // Verify the refactored from_env() produces the same results as individual getters
1177        assert_eq!(config.host, ServerConfig::get_host());
1178        assert_eq!(config.port, ServerConfig::get_port());
1179        assert_eq!(config.redis_url, ServerConfig::get_redis_url());
1180        assert_eq!(
1181            config.config_file_path,
1182            ServerConfig::get_config_file_path()
1183        );
1184        assert_eq!(config.api_key, ServerConfig::get_api_key());
1185        assert_eq!(
1186            config.rate_limit_requests_per_second,
1187            ServerConfig::get_rate_limit_requests_per_second()
1188        );
1189        assert_eq!(
1190            config.rate_limit_burst_size,
1191            ServerConfig::get_rate_limit_burst_size()
1192        );
1193        assert_eq!(config.metrics_port, ServerConfig::get_metrics_port());
1194        assert_eq!(config.enable_swagger, ServerConfig::get_enable_swagger());
1195        assert_eq!(
1196            config.redis_connection_timeout_ms,
1197            ServerConfig::get_redis_connection_timeout_ms()
1198        );
1199        assert_eq!(
1200            config.redis_key_prefix,
1201            ServerConfig::get_redis_key_prefix()
1202        );
1203        assert_eq!(config.rpc_timeout_ms, ServerConfig::get_rpc_timeout_ms());
1204        assert_eq!(
1205            config.provider_max_retries,
1206            ServerConfig::get_provider_max_retries()
1207        );
1208        assert_eq!(
1209            config.provider_retry_base_delay_ms,
1210            ServerConfig::get_provider_retry_base_delay_ms()
1211        );
1212        assert_eq!(
1213            config.provider_retry_max_delay_ms,
1214            ServerConfig::get_provider_retry_max_delay_ms()
1215        );
1216        assert_eq!(
1217            config.provider_max_failovers,
1218            ServerConfig::get_provider_max_failovers()
1219        );
1220        assert_eq!(
1221            config.repository_storage_type,
1222            ServerConfig::get_repository_storage_type()
1223        );
1224        assert_eq!(
1225            config.reset_storage_on_start,
1226            ServerConfig::get_reset_storage_on_start()
1227        );
1228        assert_eq!(
1229            config.storage_encryption_key,
1230            ServerConfig::get_storage_encryption_key()
1231        );
1232        assert_eq!(
1233            config.transaction_expiration_hours,
1234            ServerConfig::get_transaction_expiration_hours()
1235        );
1236    }
1237
1238    mod get_worker_concurrency_tests {
1239        use super::*;
1240        use serial_test::serial;
1241
1242        #[test]
1243        #[serial]
1244        fn test_returns_default_when_env_not_set() {
1245            let worker_name = "test_worker";
1246            let env_var = format!(
1247                "BACKGROUND_WORKER_{}_CONCURRENCY",
1248                worker_name.to_uppercase()
1249            );
1250
1251            // Ensure env var is not set
1252            env::remove_var(&env_var);
1253
1254            let default_value = 42;
1255            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1256
1257            assert_eq!(
1258                result, default_value,
1259                "Should return default value when env var is not set"
1260            );
1261        }
1262
1263        #[test]
1264        #[serial]
1265        fn test_returns_env_value_when_set() {
1266            let worker_name = "status_checker";
1267            let env_var = format!(
1268                "BACKGROUND_WORKER_{}_CONCURRENCY",
1269                worker_name.to_uppercase()
1270            );
1271
1272            // Set env var to a specific value
1273            env::set_var(&env_var, "100");
1274
1275            let default_value = 10;
1276            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1277
1278            assert_eq!(result, 100, "Should return env var value when set");
1279
1280            // Cleanup
1281            env::remove_var(&env_var);
1282        }
1283
1284        #[test]
1285        #[serial]
1286        fn test_returns_default_when_env_invalid() {
1287            let worker_name = "invalid_worker";
1288            let env_var = format!(
1289                "BACKGROUND_WORKER_{}_CONCURRENCY",
1290                worker_name.to_uppercase()
1291            );
1292
1293            // Set env var to invalid value
1294            env::set_var(&env_var, "not_a_number");
1295
1296            let default_value = 25;
1297            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1298
1299            assert_eq!(
1300                result, default_value,
1301                "Should return default value when env var is invalid"
1302            );
1303
1304            // Cleanup
1305            env::remove_var(&env_var);
1306        }
1307
1308        #[test]
1309        #[serial]
1310        fn test_returns_default_when_env_empty() {
1311            let worker_name = "empty_worker";
1312            let env_var = format!(
1313                "BACKGROUND_WORKER_{}_CONCURRENCY",
1314                worker_name.to_uppercase()
1315            );
1316
1317            // Set env var to empty string
1318            env::set_var(&env_var, "");
1319
1320            let default_value = 15;
1321            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1322
1323            assert_eq!(
1324                result, default_value,
1325                "Should return default value when env var is empty"
1326            );
1327
1328            // Cleanup
1329            env::remove_var(&env_var);
1330        }
1331
1332        #[test]
1333        #[serial]
1334        fn test_returns_default_when_env_negative() {
1335            let worker_name = "negative_worker";
1336            let env_var = format!(
1337                "BACKGROUND_WORKER_{}_CONCURRENCY",
1338                worker_name.to_uppercase()
1339            );
1340
1341            // Set env var to negative value
1342            env::set_var(&env_var, "-5");
1343
1344            let default_value = 20;
1345            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1346
1347            assert_eq!(
1348                result, default_value,
1349                "Should return default value when env var is negative"
1350            );
1351
1352            // Cleanup
1353            env::remove_var(&env_var);
1354        }
1355
1356        #[test]
1357        #[serial]
1358        fn test_env_var_name_formatting() {
1359            // Test that worker names are properly uppercased
1360            let worker_names = vec![
1361                (
1362                    "transaction_sender",
1363                    "BACKGROUND_WORKER_TRANSACTION_SENDER_CONCURRENCY",
1364                ),
1365                (
1366                    "status_checker_evm",
1367                    "BACKGROUND_WORKER_STATUS_CHECKER_EVM_CONCURRENCY",
1368                ),
1369                (
1370                    "notification_sender",
1371                    "BACKGROUND_WORKER_NOTIFICATION_SENDER_CONCURRENCY",
1372                ),
1373            ];
1374
1375            for (worker_name, expected_env_var) in worker_names {
1376                let actual_env_var = format!(
1377                    "BACKGROUND_WORKER_{}_CONCURRENCY",
1378                    worker_name.to_uppercase()
1379                );
1380                assert_eq!(
1381                    actual_env_var, expected_env_var,
1382                    "Env var name should be correctly formatted for worker: {worker_name}"
1383                );
1384            }
1385        }
1386
1387        #[test]
1388        #[serial]
1389        fn test_zero_value() {
1390            let worker_name = "zero_worker";
1391            let env_var = format!(
1392                "BACKGROUND_WORKER_{}_CONCURRENCY",
1393                worker_name.to_uppercase()
1394            );
1395
1396            // Set env var to zero
1397            env::set_var(&env_var, "0");
1398
1399            let default_value = 30;
1400            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1401
1402            assert_eq!(result, 0, "Should accept zero as a valid value");
1403
1404            // Cleanup
1405            env::remove_var(&env_var);
1406        }
1407
1408        #[test]
1409        #[serial]
1410        fn test_large_value() {
1411            let worker_name = "large_worker";
1412            let env_var = format!(
1413                "BACKGROUND_WORKER_{}_CONCURRENCY",
1414                worker_name.to_uppercase()
1415            );
1416
1417            // Set env var to a large value
1418            env::set_var(&env_var, "10000");
1419
1420            let default_value = 50;
1421            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1422
1423            assert_eq!(result, 10000, "Should accept large values");
1424
1425            // Cleanup
1426            env::remove_var(&env_var);
1427        }
1428
1429        #[test]
1430        #[serial]
1431        fn test_whitespace_in_value() {
1432            let worker_name = "whitespace_worker";
1433            let env_var = format!(
1434                "BACKGROUND_WORKER_{}_CONCURRENCY",
1435                worker_name.to_uppercase()
1436            );
1437
1438            // Set env var with leading/trailing whitespace
1439            env::set_var(&env_var, "  75  ");
1440
1441            let default_value = 35;
1442            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1443
1444            // Note: String::parse::<usize>() does NOT trim whitespace, so this will fail to parse
1445            // and return the default value
1446            assert_eq!(
1447                result, default_value,
1448                "Should return default value when value has whitespace"
1449            );
1450
1451            // Cleanup
1452            env::remove_var(&env_var);
1453        }
1454
1455        #[test]
1456        #[serial]
1457        fn test_float_value_returns_default() {
1458            let worker_name = "float_worker";
1459            let env_var = format!(
1460                "BACKGROUND_WORKER_{}_CONCURRENCY",
1461                worker_name.to_uppercase()
1462            );
1463
1464            // Set env var to float value
1465            env::set_var(&env_var, "12.5");
1466
1467            let default_value = 40;
1468            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1469
1470            assert_eq!(
1471                result, default_value,
1472                "Should return default value for float input"
1473            );
1474
1475            // Cleanup
1476            env::remove_var(&env_var);
1477        }
1478    }
1479
1480    mod get_relayer_concurrency_limit_tests {
1481        use super::*;
1482        use serial_test::serial;
1483
1484        #[test]
1485        #[serial]
1486        fn test_returns_default_when_env_not_set() {
1487            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1488            let result = ServerConfig::get_relayer_concurrency_limit();
1489            assert_eq!(result, 100, "Should return default value of 100");
1490        }
1491
1492        #[test]
1493        #[serial]
1494        fn test_returns_env_value_when_set() {
1495            env::set_var("RELAYER_CONCURRENCY_LIMIT", "250");
1496            let result = ServerConfig::get_relayer_concurrency_limit();
1497            assert_eq!(result, 250, "Should return env var value");
1498            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1499        }
1500
1501        #[test]
1502        #[serial]
1503        fn test_returns_default_when_env_invalid() {
1504            env::set_var("RELAYER_CONCURRENCY_LIMIT", "not_a_number");
1505            let result = ServerConfig::get_relayer_concurrency_limit();
1506            assert_eq!(result, 100, "Should return default value when invalid");
1507            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1508        }
1509
1510        #[test]
1511        #[serial]
1512        fn test_returns_default_when_env_empty() {
1513            env::set_var("RELAYER_CONCURRENCY_LIMIT", "");
1514            let result = ServerConfig::get_relayer_concurrency_limit();
1515            assert_eq!(result, 100, "Should return default value when empty");
1516            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1517        }
1518
1519        #[test]
1520        #[serial]
1521        fn test_zero_value() {
1522            env::set_var("RELAYER_CONCURRENCY_LIMIT", "0");
1523            let result = ServerConfig::get_relayer_concurrency_limit();
1524            assert_eq!(result, 0, "Should accept zero as valid value");
1525            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1526        }
1527
1528        #[test]
1529        #[serial]
1530        fn test_large_value() {
1531            env::set_var("RELAYER_CONCURRENCY_LIMIT", "5000");
1532            let result = ServerConfig::get_relayer_concurrency_limit();
1533            assert_eq!(result, 5000, "Should accept large values");
1534            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1535        }
1536
1537        #[test]
1538        #[serial]
1539        fn test_negative_value_returns_default() {
1540            env::set_var("RELAYER_CONCURRENCY_LIMIT", "-10");
1541            let result = ServerConfig::get_relayer_concurrency_limit();
1542            assert_eq!(result, 100, "Should return default for negative value");
1543            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1544        }
1545
1546        #[test]
1547        #[serial]
1548        fn test_float_value_returns_default() {
1549            env::set_var("RELAYER_CONCURRENCY_LIMIT", "100.5");
1550            let result = ServerConfig::get_relayer_concurrency_limit();
1551            assert_eq!(result, 100, "Should return default for float value");
1552            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1553        }
1554
1555        #[test]
1556        #[serial]
1557        fn test_whitespace_value_returns_default() {
1558            env::set_var("RELAYER_CONCURRENCY_LIMIT", "  150  ");
1559            let result = ServerConfig::get_relayer_concurrency_limit();
1560            assert_eq!(
1561                result, 100,
1562                "Should return default when value has whitespace"
1563            );
1564            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1565        }
1566    }
1567
1568    mod get_max_connections_tests {
1569        use super::*;
1570        use serial_test::serial;
1571
1572        #[test]
1573        #[serial]
1574        fn test_returns_default_when_env_not_set() {
1575            env::remove_var("MAX_CONNECTIONS");
1576            let result = ServerConfig::get_max_connections();
1577            assert_eq!(result, 256, "Should return default value of 256");
1578        }
1579
1580        #[test]
1581        #[serial]
1582        fn test_returns_env_value_when_set() {
1583            env::set_var("MAX_CONNECTIONS", "512");
1584            let result = ServerConfig::get_max_connections();
1585            assert_eq!(result, 512, "Should return env var value");
1586            env::remove_var("MAX_CONNECTIONS");
1587        }
1588
1589        #[test]
1590        #[serial]
1591        fn test_returns_default_when_env_invalid() {
1592            env::set_var("MAX_CONNECTIONS", "invalid");
1593            let result = ServerConfig::get_max_connections();
1594            assert_eq!(result, 256, "Should return default value when invalid");
1595            env::remove_var("MAX_CONNECTIONS");
1596        }
1597
1598        #[test]
1599        #[serial]
1600        fn test_returns_default_when_env_empty() {
1601            env::set_var("MAX_CONNECTIONS", "");
1602            let result = ServerConfig::get_max_connections();
1603            assert_eq!(result, 256, "Should return default value when empty");
1604            env::remove_var("MAX_CONNECTIONS");
1605        }
1606
1607        #[test]
1608        #[serial]
1609        fn test_zero_value() {
1610            env::set_var("MAX_CONNECTIONS", "0");
1611            let result = ServerConfig::get_max_connections();
1612            assert_eq!(result, 0, "Should accept zero as valid value");
1613            env::remove_var("MAX_CONNECTIONS");
1614        }
1615
1616        #[test]
1617        #[serial]
1618        fn test_large_value() {
1619            env::set_var("MAX_CONNECTIONS", "10000");
1620            let result = ServerConfig::get_max_connections();
1621            assert_eq!(result, 10000, "Should accept large values");
1622            env::remove_var("MAX_CONNECTIONS");
1623        }
1624
1625        #[test]
1626        #[serial]
1627        fn test_negative_value_returns_default() {
1628            env::set_var("MAX_CONNECTIONS", "-100");
1629            let result = ServerConfig::get_max_connections();
1630            assert_eq!(result, 256, "Should return default for negative value");
1631            env::remove_var("MAX_CONNECTIONS");
1632        }
1633
1634        #[test]
1635        #[serial]
1636        fn test_float_value_returns_default() {
1637            env::set_var("MAX_CONNECTIONS", "256.5");
1638            let result = ServerConfig::get_max_connections();
1639            assert_eq!(result, 256, "Should return default for float value");
1640            env::remove_var("MAX_CONNECTIONS");
1641        }
1642    }
1643
1644    mod get_connection_backlog_tests {
1645        use super::*;
1646        use serial_test::serial;
1647
1648        #[test]
1649        #[serial]
1650        fn test_returns_default_when_env_not_set() {
1651            env::remove_var("CONNECTION_BACKLOG");
1652            let result = ServerConfig::get_connection_backlog();
1653            assert_eq!(result, 511, "Should return default value of 511");
1654        }
1655
1656        #[test]
1657        #[serial]
1658        fn test_returns_env_value_when_set() {
1659            env::set_var("CONNECTION_BACKLOG", "1024");
1660            let result = ServerConfig::get_connection_backlog();
1661            assert_eq!(result, 1024, "Should return env var value");
1662            env::remove_var("CONNECTION_BACKLOG");
1663        }
1664
1665        #[test]
1666        #[serial]
1667        fn test_returns_default_when_env_invalid() {
1668            env::set_var("CONNECTION_BACKLOG", "not_a_number");
1669            let result = ServerConfig::get_connection_backlog();
1670            assert_eq!(result, 511, "Should return default value when invalid");
1671            env::remove_var("CONNECTION_BACKLOG");
1672        }
1673
1674        #[test]
1675        #[serial]
1676        fn test_returns_default_when_env_empty() {
1677            env::set_var("CONNECTION_BACKLOG", "");
1678            let result = ServerConfig::get_connection_backlog();
1679            assert_eq!(result, 511, "Should return default value when empty");
1680            env::remove_var("CONNECTION_BACKLOG");
1681        }
1682
1683        #[test]
1684        #[serial]
1685        fn test_zero_value() {
1686            env::set_var("CONNECTION_BACKLOG", "0");
1687            let result = ServerConfig::get_connection_backlog();
1688            assert_eq!(result, 0, "Should accept zero as valid value");
1689            env::remove_var("CONNECTION_BACKLOG");
1690        }
1691
1692        #[test]
1693        #[serial]
1694        fn test_large_value() {
1695            env::set_var("CONNECTION_BACKLOG", "65535");
1696            let result = ServerConfig::get_connection_backlog();
1697            assert_eq!(result, 65535, "Should accept large values");
1698            env::remove_var("CONNECTION_BACKLOG");
1699        }
1700
1701        #[test]
1702        #[serial]
1703        fn test_negative_value_returns_default() {
1704            env::set_var("CONNECTION_BACKLOG", "-50");
1705            let result = ServerConfig::get_connection_backlog();
1706            assert_eq!(result, 511, "Should return default for negative value");
1707            env::remove_var("CONNECTION_BACKLOG");
1708        }
1709
1710        #[test]
1711        #[serial]
1712        fn test_float_value_returns_default() {
1713            env::set_var("CONNECTION_BACKLOG", "511.5");
1714            let result = ServerConfig::get_connection_backlog();
1715            assert_eq!(result, 511, "Should return default for float value");
1716            env::remove_var("CONNECTION_BACKLOG");
1717        }
1718
1719        #[test]
1720        #[serial]
1721        fn test_common_production_values() {
1722            // Test common production values
1723            let test_cases = vec![
1724                (128, "Small server"),
1725                (511, "Default"),
1726                (1024, "Medium server"),
1727                (2048, "Large server"),
1728            ];
1729
1730            for (value, description) in test_cases {
1731                env::set_var("CONNECTION_BACKLOG", value.to_string());
1732                let result = ServerConfig::get_connection_backlog();
1733                assert_eq!(result, value, "Should accept {description}: {value}");
1734            }
1735
1736            env::remove_var("CONNECTION_BACKLOG");
1737        }
1738    }
1739
1740    mod get_request_timeout_seconds_tests {
1741        use super::*;
1742        use serial_test::serial;
1743
1744        #[test]
1745        #[serial]
1746        fn test_returns_default_when_env_not_set() {
1747            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1748            let result = ServerConfig::get_request_timeout_seconds();
1749            assert_eq!(result, 30, "Should return default value of 30");
1750        }
1751
1752        #[test]
1753        #[serial]
1754        fn test_returns_env_value_when_set() {
1755            env::set_var("REQUEST_TIMEOUT_SECONDS", "60");
1756            let result = ServerConfig::get_request_timeout_seconds();
1757            assert_eq!(result, 60, "Should return env var value");
1758            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1759        }
1760
1761        #[test]
1762        #[serial]
1763        fn test_returns_default_when_env_invalid() {
1764            env::set_var("REQUEST_TIMEOUT_SECONDS", "invalid");
1765            let result = ServerConfig::get_request_timeout_seconds();
1766            assert_eq!(result, 30, "Should return default value when invalid");
1767            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1768        }
1769
1770        #[test]
1771        #[serial]
1772        fn test_returns_default_when_env_empty() {
1773            env::set_var("REQUEST_TIMEOUT_SECONDS", "");
1774            let result = ServerConfig::get_request_timeout_seconds();
1775            assert_eq!(result, 30, "Should return default value when empty");
1776            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1777        }
1778
1779        #[test]
1780        #[serial]
1781        fn test_zero_value() {
1782            env::set_var("REQUEST_TIMEOUT_SECONDS", "0");
1783            let result = ServerConfig::get_request_timeout_seconds();
1784            assert_eq!(result, 0, "Should accept zero as valid value");
1785            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1786        }
1787
1788        #[test]
1789        #[serial]
1790        fn test_large_value() {
1791            env::set_var("REQUEST_TIMEOUT_SECONDS", "300");
1792            let result = ServerConfig::get_request_timeout_seconds();
1793            assert_eq!(result, 300, "Should accept large values");
1794            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1795        }
1796
1797        #[test]
1798        #[serial]
1799        fn test_negative_value_returns_default() {
1800            env::set_var("REQUEST_TIMEOUT_SECONDS", "-10");
1801            let result = ServerConfig::get_request_timeout_seconds();
1802            assert_eq!(result, 30, "Should return default for negative value");
1803            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1804        }
1805
1806        #[test]
1807        #[serial]
1808        fn test_float_value_returns_default() {
1809            env::set_var("REQUEST_TIMEOUT_SECONDS", "30.5");
1810            let result = ServerConfig::get_request_timeout_seconds();
1811            assert_eq!(result, 30, "Should return default for float value");
1812            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1813        }
1814
1815        #[test]
1816        #[serial]
1817        fn test_common_timeout_values() {
1818            // Test common timeout values
1819            let test_cases = vec![
1820                (10, "Short timeout"),
1821                (30, "Default timeout"),
1822                (60, "Moderate timeout"),
1823                (120, "Long timeout"),
1824            ];
1825
1826            for (value, description) in test_cases {
1827                env::set_var("REQUEST_TIMEOUT_SECONDS", value.to_string());
1828                let result = ServerConfig::get_request_timeout_seconds();
1829                assert_eq!(result, value, "Should accept {description}: {value}");
1830            }
1831
1832            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1833        }
1834    }
1835
1836    mod get_redis_reader_url_tests {
1837        use super::*;
1838        use serial_test::serial;
1839
1840        #[test]
1841        #[serial]
1842        fn test_returns_none_when_env_not_set() {
1843            env::remove_var("REDIS_READER_URL");
1844            let result = ServerConfig::get_redis_reader_url_optional();
1845            assert!(
1846                result.is_none(),
1847                "Should return None when env var is not set"
1848            );
1849        }
1850
1851        #[test]
1852        #[serial]
1853        fn test_returns_value_when_set() {
1854            env::set_var("REDIS_READER_URL", "redis://reader:6379");
1855            let result = ServerConfig::get_redis_reader_url_optional();
1856            assert_eq!(
1857                result,
1858                Some("redis://reader:6379".to_string()),
1859                "Should return the env var value"
1860            );
1861            env::remove_var("REDIS_READER_URL");
1862        }
1863
1864        #[test]
1865        #[serial]
1866        fn test_returns_empty_string_when_set_to_empty() {
1867            env::set_var("REDIS_READER_URL", "");
1868            let result = ServerConfig::get_redis_reader_url_optional();
1869            assert_eq!(
1870                result,
1871                Some("".to_string()),
1872                "Should return empty string when set to empty"
1873            );
1874            env::remove_var("REDIS_READER_URL");
1875        }
1876
1877        #[test]
1878        #[serial]
1879        fn test_aws_elasticache_reader_url() {
1880            // Test with typical AWS ElastiCache reader endpoint format
1881            let reader_url = "redis://my-cluster-ro.xxx.cache.amazonaws.com:6379";
1882            env::set_var("REDIS_READER_URL", reader_url);
1883            let result = ServerConfig::get_redis_reader_url_optional();
1884            assert_eq!(
1885                result,
1886                Some(reader_url.to_string()),
1887                "Should accept AWS ElastiCache reader endpoint"
1888            );
1889            env::remove_var("REDIS_READER_URL");
1890        }
1891
1892        #[test]
1893        #[serial]
1894        fn test_config_includes_redis_reader_url() {
1895            env::set_var("REDIS_URL", "redis://primary:6379");
1896            env::set_var("REDIS_READER_URL", "redis://reader:6379");
1897            env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
1898
1899            let config = ServerConfig::from_env();
1900
1901            assert_eq!(config.redis_url, "redis://primary:6379");
1902            assert_eq!(
1903                config.redis_reader_url,
1904                Some("redis://reader:6379".to_string())
1905            );
1906
1907            env::remove_var("REDIS_URL");
1908            env::remove_var("REDIS_READER_URL");
1909            env::remove_var("API_KEY");
1910        }
1911
1912        #[test]
1913        #[serial]
1914        fn test_config_without_redis_reader_url() {
1915            env::set_var("REDIS_URL", "redis://primary:6379");
1916            env::remove_var("REDIS_READER_URL");
1917            env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
1918
1919            let config = ServerConfig::from_env();
1920
1921            assert_eq!(config.redis_url, "redis://primary:6379");
1922            assert!(
1923                config.redis_reader_url.is_none(),
1924                "redis_reader_url should be None when not set"
1925            );
1926
1927            env::remove_var("REDIS_URL");
1928            env::remove_var("API_KEY");
1929        }
1930    }
1931
1932    mod get_sqs_queue_type_tests {
1933        use super::*;
1934        use serial_test::serial;
1935
1936        #[test]
1937        #[serial]
1938        fn test_returns_auto_when_env_not_set() {
1939            env::remove_var("SQS_QUEUE_TYPE");
1940            let result = ServerConfig::get_sqs_queue_type();
1941            assert_eq!(result, "auto", "Should default to 'auto'");
1942        }
1943
1944        #[test]
1945        #[serial]
1946        fn test_returns_fifo_when_set() {
1947            env::set_var("SQS_QUEUE_TYPE", "fifo");
1948            let result = ServerConfig::get_sqs_queue_type();
1949            assert_eq!(result, "fifo");
1950            env::remove_var("SQS_QUEUE_TYPE");
1951        }
1952
1953        #[test]
1954        #[serial]
1955        fn test_returns_standard_when_set() {
1956            env::set_var("SQS_QUEUE_TYPE", "standard");
1957            let result = ServerConfig::get_sqs_queue_type();
1958            assert_eq!(result, "standard");
1959            env::remove_var("SQS_QUEUE_TYPE");
1960        }
1961
1962        #[test]
1963        #[serial]
1964        fn test_returns_raw_value_for_unknown() {
1965            env::set_var("SQS_QUEUE_TYPE", "unknown");
1966            let result = ServerConfig::get_sqs_queue_type();
1967            assert_eq!(result, "unknown");
1968            env::remove_var("SQS_QUEUE_TYPE");
1969        }
1970    }
1971
1972    mod get_redis_reader_pool_max_size_tests {
1973        use super::*;
1974        use serial_test::serial;
1975
1976        #[test]
1977        #[serial]
1978        fn test_returns_default_when_env_not_set() {
1979            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1980            let result = ServerConfig::get_redis_reader_pool_max_size();
1981            assert_eq!(
1982                result, 1000,
1983                "Should return default 1000 when env var is not set"
1984            );
1985        }
1986
1987        #[test]
1988        #[serial]
1989        fn test_returns_value_when_set() {
1990            env::set_var("REDIS_READER_POOL_MAX_SIZE", "2000");
1991            let result = ServerConfig::get_redis_reader_pool_max_size();
1992            assert_eq!(result, 2000, "Should return the parsed value");
1993            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1994        }
1995
1996        #[test]
1997        #[serial]
1998        fn test_returns_default_when_invalid() {
1999            env::set_var("REDIS_READER_POOL_MAX_SIZE", "not_a_number");
2000            let result = ServerConfig::get_redis_reader_pool_max_size();
2001            assert_eq!(
2002                result, 1000,
2003                "Should return default 1000 for invalid values"
2004            );
2005            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
2006        }
2007
2008        #[test]
2009        #[serial]
2010        fn test_returns_default_when_zero() {
2011            env::set_var("REDIS_READER_POOL_MAX_SIZE", "0");
2012            let result = ServerConfig::get_redis_reader_pool_max_size();
2013            assert_eq!(result, 1000, "Should return default 1000 when value is 0");
2014            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
2015        }
2016
2017        #[test]
2018        #[serial]
2019        fn test_returns_default_when_negative() {
2020            env::set_var("REDIS_READER_POOL_MAX_SIZE", "-100");
2021            let result = ServerConfig::get_redis_reader_pool_max_size();
2022            assert_eq!(
2023                result, 1000,
2024                "Should return default 1000 for negative values"
2025            );
2026            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
2027        }
2028
2029        #[test]
2030        #[serial]
2031        fn test_config_includes_reader_pool_max_size() {
2032            env::set_var("REDIS_URL", "redis://primary:6379");
2033            env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
2034            env::set_var("REDIS_READER_POOL_MAX_SIZE", "750");
2035
2036            let config = ServerConfig::from_env();
2037
2038            assert_eq!(
2039                config.redis_reader_pool_max_size, 750,
2040                "Should include reader pool max size in config"
2041            );
2042
2043            env::remove_var("REDIS_URL");
2044            env::remove_var("API_KEY");
2045            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
2046        }
2047
2048        #[test]
2049        #[serial]
2050        fn test_config_uses_default_when_not_set() {
2051            env::set_var("REDIS_URL", "redis://primary:6379");
2052            env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
2053            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
2054
2055            let config = ServerConfig::from_env();
2056
2057            assert_eq!(
2058                config.redis_reader_pool_max_size, 1000,
2059                "Should use default 1000 when not set"
2060            );
2061
2062            env::remove_var("REDIS_URL");
2063            env::remove_var("API_KEY");
2064        }
2065    }
2066
2067    mod get_sqs_wait_time_tests {
2068        use super::*;
2069        use serial_test::serial;
2070
2071        #[test]
2072        #[serial]
2073        fn test_returns_default_when_env_not_set() {
2074            env::remove_var("SQS_TEST_QUEUE_WAIT_TIME_SECONDS");
2075            let result = ServerConfig::get_sqs_wait_time("TEST_QUEUE", 5);
2076            assert_eq!(result, 5, "Should return default when env var is not set");
2077        }
2078
2079        #[test]
2080        #[serial]
2081        fn test_returns_parsed_value() {
2082            env::set_var("SQS_TEST_QUEUE_WAIT_TIME_SECONDS", "10");
2083            let result = ServerConfig::get_sqs_wait_time("TEST_QUEUE", 5);
2084            assert_eq!(result, 10, "Should return parsed value");
2085            env::remove_var("SQS_TEST_QUEUE_WAIT_TIME_SECONDS");
2086        }
2087
2088        #[test]
2089        #[serial]
2090        fn test_returns_default_when_invalid() {
2091            env::set_var("SQS_TEST_QUEUE_WAIT_TIME_SECONDS", "not_a_number");
2092            let result = ServerConfig::get_sqs_wait_time("TEST_QUEUE", 5);
2093            assert_eq!(result, 5, "Should return default for non-numeric input");
2094            env::remove_var("SQS_TEST_QUEUE_WAIT_TIME_SECONDS");
2095        }
2096
2097        #[test]
2098        #[serial]
2099        fn test_clamps_to_sqs_maximum_of_20() {
2100            env::set_var("SQS_TEST_QUEUE_WAIT_TIME_SECONDS", "30");
2101            let result = ServerConfig::get_sqs_wait_time("TEST_QUEUE", 5);
2102            assert_eq!(result, 20, "Should clamp to SQS maximum of 20 seconds");
2103            env::remove_var("SQS_TEST_QUEUE_WAIT_TIME_SECONDS");
2104        }
2105
2106        #[test]
2107        #[serial]
2108        fn test_allows_zero() {
2109            env::set_var("SQS_TEST_QUEUE_WAIT_TIME_SECONDS", "0");
2110            let result = ServerConfig::get_sqs_wait_time("TEST_QUEUE", 5);
2111            assert_eq!(result, 0, "Should allow zero (short polling)");
2112            env::remove_var("SQS_TEST_QUEUE_WAIT_TIME_SECONDS");
2113        }
2114    }
2115
2116    mod get_sqs_poller_count_tests {
2117        use super::*;
2118        use serial_test::serial;
2119
2120        #[test]
2121        #[serial]
2122        fn test_returns_default_when_env_not_set() {
2123            env::remove_var("SQS_TEST_QUEUE_POLLER_COUNT");
2124            let result = ServerConfig::get_sqs_poller_count("TEST_QUEUE", 2);
2125            assert_eq!(result, 2, "Should return default when env var is not set");
2126        }
2127
2128        #[test]
2129        #[serial]
2130        fn test_returns_parsed_value() {
2131            env::set_var("SQS_TEST_QUEUE_POLLER_COUNT", "4");
2132            let result = ServerConfig::get_sqs_poller_count("TEST_QUEUE", 2);
2133            assert_eq!(result, 4, "Should return parsed value");
2134            env::remove_var("SQS_TEST_QUEUE_POLLER_COUNT");
2135        }
2136
2137        #[test]
2138        #[serial]
2139        fn test_returns_default_when_invalid() {
2140            env::set_var("SQS_TEST_QUEUE_POLLER_COUNT", "not_a_number");
2141            let result = ServerConfig::get_sqs_poller_count("TEST_QUEUE", 2);
2142            assert_eq!(result, 2, "Should return default for non-numeric input");
2143            env::remove_var("SQS_TEST_QUEUE_POLLER_COUNT");
2144        }
2145
2146        #[test]
2147        #[serial]
2148        fn test_clamps_zero_to_minimum_of_1() {
2149            env::set_var("SQS_TEST_QUEUE_POLLER_COUNT", "0");
2150            let result = ServerConfig::get_sqs_poller_count("TEST_QUEUE", 2);
2151            assert_eq!(result, 1, "Should clamp zero to minimum of 1");
2152            env::remove_var("SQS_TEST_QUEUE_POLLER_COUNT");
2153        }
2154
2155        #[test]
2156        #[serial]
2157        fn test_default_also_clamped_to_minimum_of_1() {
2158            env::remove_var("SQS_TEST_QUEUE_POLLER_COUNT");
2159            let result = ServerConfig::get_sqs_poller_count("TEST_QUEUE", 0);
2160            assert_eq!(
2161                result, 1,
2162                "Default of 0 should also be clamped to minimum of 1"
2163            );
2164        }
2165    }
2166}