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
678#[cfg(test)]
679mod tests {
680    use super::*;
681    use lazy_static::lazy_static;
682    use std::env;
683    use std::sync::Mutex;
684
685    // Use a mutex to ensure tests don't run in parallel when modifying env vars
686    lazy_static! {
687        static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
688    }
689
690    fn setup() {
691        // Clear all environment variables first
692        env::remove_var("HOST");
693        env::remove_var("APP_PORT");
694        env::remove_var("REDIS_URL");
695        env::remove_var("CONFIG_DIR");
696        env::remove_var("CONFIG_FILE_NAME");
697        env::remove_var("CONFIG_FILE_PATH");
698        env::remove_var("API_KEY");
699        env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
700        env::remove_var("RATE_LIMIT_BURST_SIZE");
701        env::remove_var("METRICS_PORT");
702        env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
703        env::remove_var("RPC_TIMEOUT_MS");
704        env::remove_var("PROVIDER_MAX_RETRIES");
705        env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
706        env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
707        env::remove_var("PROVIDER_MAX_FAILOVERS");
708        env::remove_var("REPOSITORY_STORAGE_TYPE");
709        env::remove_var("RESET_STORAGE_ON_START");
710        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
711        env::remove_var("REDIS_READER_URL");
712        // Set required variables for most tests
713        env::set_var("REDIS_URL", "redis://localhost:6379");
714        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
715        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
716    }
717
718    #[test]
719    fn test_default_values() {
720        let _lock = match ENV_MUTEX.lock() {
721            Ok(guard) => guard,
722            Err(poisoned) => poisoned.into_inner(),
723        };
724        setup();
725
726        let config = ServerConfig::from_env();
727
728        assert_eq!(config.host, "0.0.0.0");
729        assert_eq!(config.port, 8080);
730        assert_eq!(config.redis_url, "redis://localhost:6379");
731        assert_eq!(config.config_file_path, "./config/config.json");
732        assert_eq!(
733            config.api_key,
734            SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
735        );
736        assert_eq!(config.rate_limit_requests_per_second, 100);
737        assert_eq!(config.rate_limit_burst_size, 300);
738        assert_eq!(config.metrics_port, 8081);
739        assert_eq!(config.redis_connection_timeout_ms, 5000);
740        assert_eq!(config.rpc_timeout_ms, 10000);
741        assert_eq!(config.provider_max_retries, 3);
742        assert_eq!(config.provider_retry_base_delay_ms, 100);
743        assert_eq!(config.provider_retry_max_delay_ms, 2000);
744        assert_eq!(config.provider_max_failovers, 3);
745        assert_eq!(config.provider_failure_threshold, 3);
746        assert_eq!(config.provider_pause_duration_secs, 60);
747        assert_eq!(
748            config.repository_storage_type,
749            RepositoryStorageType::InMemory
750        );
751        assert!(!config.reset_storage_on_start);
752        assert_eq!(config.transaction_expiration_hours, 4.0);
753    }
754
755    #[test]
756    fn test_invalid_port_values() {
757        let _lock = match ENV_MUTEX.lock() {
758            Ok(guard) => guard,
759            Err(poisoned) => poisoned.into_inner(),
760        };
761        setup();
762        env::set_var("REDIS_URL", "redis://localhost:6379");
763        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
764        env::set_var("APP_PORT", "not_a_number");
765        env::set_var("METRICS_PORT", "also_not_a_number");
766        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "invalid");
767        env::set_var("RATE_LIMIT_BURST_SIZE", "invalid");
768        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "invalid");
769        env::set_var("RPC_TIMEOUT_MS", "invalid");
770        env::set_var("PROVIDER_MAX_RETRIES", "invalid");
771        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "invalid");
772        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "invalid");
773        env::set_var("PROVIDER_MAX_FAILOVERS", "invalid");
774        env::set_var("REPOSITORY_STORAGE_TYPE", "invalid");
775        env::set_var("RESET_STORAGE_ON_START", "invalid");
776        env::set_var("TRANSACTION_EXPIRATION_HOURS", "invalid");
777        let config = ServerConfig::from_env();
778
779        // Should fall back to defaults when parsing fails
780        assert_eq!(config.port, 8080);
781        assert_eq!(config.metrics_port, 8081);
782        assert_eq!(config.rate_limit_requests_per_second, 100);
783        assert_eq!(config.rate_limit_burst_size, 300);
784        assert_eq!(config.redis_connection_timeout_ms, 10000);
785        assert_eq!(config.rpc_timeout_ms, 10000);
786        assert_eq!(config.provider_max_retries, 3);
787        assert_eq!(config.provider_retry_base_delay_ms, 100);
788        assert_eq!(config.provider_retry_max_delay_ms, 2000);
789        assert_eq!(config.provider_max_failovers, 3);
790        assert_eq!(
791            config.repository_storage_type,
792            RepositoryStorageType::InMemory
793        );
794        assert!(!config.reset_storage_on_start);
795        assert_eq!(config.transaction_expiration_hours, 4.0);
796    }
797
798    #[test]
799    fn test_custom_values() {
800        let _lock = match ENV_MUTEX.lock() {
801            Ok(guard) => guard,
802            Err(poisoned) => poisoned.into_inner(),
803        };
804        setup();
805
806        env::set_var("HOST", "127.0.0.1");
807        env::set_var("APP_PORT", "9090");
808        env::set_var("REDIS_URL", "redis://custom:6379");
809        env::set_var("CONFIG_DIR", "custom");
810        env::set_var("CONFIG_FILE_NAME", "path.json");
811        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
812        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "200");
813        env::set_var("RATE_LIMIT_BURST_SIZE", "500");
814        env::set_var("METRICS_PORT", "9091");
815        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "10000");
816        env::set_var("RPC_TIMEOUT_MS", "33333");
817        env::set_var("PROVIDER_MAX_RETRIES", "5");
818        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
819        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "3000");
820        env::set_var("PROVIDER_MAX_FAILOVERS", "4");
821        env::set_var("REPOSITORY_STORAGE_TYPE", "in_memory");
822        env::set_var("RESET_STORAGE_ON_START", "true");
823        env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
824        let config = ServerConfig::from_env();
825
826        assert_eq!(config.host, "127.0.0.1");
827        assert_eq!(config.port, 9090);
828        assert_eq!(config.redis_url, "redis://custom:6379");
829        assert_eq!(config.config_file_path, "custom/path.json");
830        assert_eq!(
831            config.api_key,
832            SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
833        );
834        assert_eq!(config.rate_limit_requests_per_second, 200);
835        assert_eq!(config.rate_limit_burst_size, 500);
836        assert_eq!(config.metrics_port, 9091);
837        assert_eq!(config.redis_connection_timeout_ms, 10000);
838        assert_eq!(config.rpc_timeout_ms, 33333);
839        assert_eq!(config.provider_max_retries, 5);
840        assert_eq!(config.provider_retry_base_delay_ms, 200);
841        assert_eq!(config.provider_retry_max_delay_ms, 3000);
842        assert_eq!(config.provider_max_failovers, 4);
843        assert_eq!(
844            config.repository_storage_type,
845            RepositoryStorageType::InMemory
846        );
847        assert!(config.reset_storage_on_start);
848        assert_eq!(config.transaction_expiration_hours, 6.0);
849    }
850
851    #[test]
852    #[should_panic(expected = "Security error: API_KEY must be at least 32 characters long")]
853    fn test_invalid_api_key_length() {
854        let _lock = match ENV_MUTEX.lock() {
855            Ok(guard) => guard,
856            Err(poisoned) => poisoned.into_inner(),
857        };
858        setup();
859        env::set_var("REDIS_URL", "redis://localhost:6379");
860        env::set_var("API_KEY", "insufficient_length");
861        env::set_var("APP_PORT", "8080");
862        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "100");
863        env::set_var("RATE_LIMIT_BURST_SIZE", "300");
864        env::set_var("METRICS_PORT", "9091");
865        env::set_var("TRANSACTION_EXPIRATION_HOURS", "4");
866
867        let _ = ServerConfig::from_env();
868
869        panic!("Test should have panicked before reaching here");
870    }
871
872    // Tests for individual getter methods
873    #[test]
874    fn test_individual_getters_with_defaults() {
875        let _lock = match ENV_MUTEX.lock() {
876            Ok(guard) => guard,
877            Err(poisoned) => poisoned.into_inner(),
878        };
879
880        // Clear all environment variables to test defaults
881        env::remove_var("HOST");
882        env::remove_var("APP_PORT");
883        env::remove_var("REDIS_URL");
884        env::remove_var("CONFIG_DIR");
885        env::remove_var("CONFIG_FILE_NAME");
886        env::remove_var("API_KEY");
887        env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
888        env::remove_var("RATE_LIMIT_BURST_SIZE");
889        env::remove_var("METRICS_PORT");
890        env::remove_var("ENABLE_SWAGGER");
891        env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
892        env::remove_var("REDIS_KEY_PREFIX");
893        env::remove_var("REDIS_READER_URL");
894        env::remove_var("RPC_TIMEOUT_MS");
895        env::remove_var("PROVIDER_MAX_RETRIES");
896        env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
897        env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
898        env::remove_var("PROVIDER_MAX_FAILOVERS");
899        env::remove_var("REPOSITORY_STORAGE_TYPE");
900        env::remove_var("RESET_STORAGE_ON_START");
901        env::remove_var("STORAGE_ENCRYPTION_KEY");
902        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
903        env::remove_var("REDIS_POOL_MAX_SIZE");
904        env::remove_var("REDIS_POOL_TIMEOUT_MS");
905
906        // Test individual getters with defaults
907        assert_eq!(ServerConfig::get_host(), "0.0.0.0");
908        assert_eq!(ServerConfig::get_port(), 8080);
909        assert_eq!(ServerConfig::get_redis_url_optional(), None);
910        assert_eq!(ServerConfig::get_config_file_path(), "./config/config.json");
911        assert_eq!(ServerConfig::get_api_key_optional(), None);
912        assert_eq!(ServerConfig::get_rate_limit_requests_per_second(), 100);
913        assert_eq!(ServerConfig::get_rate_limit_burst_size(), 300);
914        assert_eq!(ServerConfig::get_metrics_port(), 8081);
915        assert!(!ServerConfig::get_enable_swagger());
916        assert_eq!(ServerConfig::get_redis_connection_timeout_ms(), 10000);
917        assert_eq!(ServerConfig::get_redis_key_prefix(), "oz-relayer");
918        assert_eq!(ServerConfig::get_rpc_timeout_ms(), 10000);
919        assert_eq!(ServerConfig::get_provider_max_retries(), 3);
920        assert_eq!(ServerConfig::get_provider_retry_base_delay_ms(), 100);
921        assert_eq!(ServerConfig::get_provider_retry_max_delay_ms(), 2000);
922        assert_eq!(ServerConfig::get_provider_max_failovers(), 3);
923        assert_eq!(
924            ServerConfig::get_repository_storage_type(),
925            RepositoryStorageType::InMemory
926        );
927        assert!(!ServerConfig::get_reset_storage_on_start());
928        assert!(ServerConfig::get_storage_encryption_key().is_none());
929        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 4.0);
930        assert_eq!(ServerConfig::get_redis_pool_max_size(), 500);
931        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 10000);
932    }
933
934    #[test]
935    fn test_individual_getters_with_custom_values() {
936        let _lock = match ENV_MUTEX.lock() {
937            Ok(guard) => guard,
938            Err(poisoned) => poisoned.into_inner(),
939        };
940
941        // Set custom values
942        env::set_var("HOST", "192.168.1.1");
943        env::set_var("APP_PORT", "9999");
944        env::set_var("REDIS_URL", "redis://custom:6379");
945        env::set_var("CONFIG_DIR", "/custom/config");
946        env::set_var("CONFIG_FILE_NAME", "custom.json");
947        env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
948        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "500");
949        env::set_var("RATE_LIMIT_BURST_SIZE", "1000");
950        env::set_var("METRICS_PORT", "9999");
951        env::set_var("ENABLE_SWAGGER", "true");
952        env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
953        env::set_var("REDIS_KEY_PREFIX", "custom-prefix");
954        env::set_var("RPC_TIMEOUT_MS", "15000");
955        env::set_var("PROVIDER_MAX_RETRIES", "5");
956        env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
957        env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "5000");
958        env::set_var("PROVIDER_MAX_FAILOVERS", "10");
959        env::set_var("REPOSITORY_STORAGE_TYPE", "redis");
960        env::set_var("RESET_STORAGE_ON_START", "true");
961        env::set_var("STORAGE_ENCRYPTION_KEY", "my-encryption-key");
962        env::set_var("TRANSACTION_EXPIRATION_HOURS", "12");
963        env::set_var("REDIS_POOL_MAX_SIZE", "200");
964        env::set_var("REDIS_POOL_TIMEOUT_MS", "20000");
965
966        // Test individual getters with custom values
967        assert_eq!(ServerConfig::get_host(), "192.168.1.1");
968        assert_eq!(ServerConfig::get_port(), 9999);
969        assert_eq!(
970            ServerConfig::get_redis_url_optional(),
971            Some("redis://custom:6379".to_string())
972        );
973        assert_eq!(
974            ServerConfig::get_config_file_path(),
975            "/custom/config/custom.json"
976        );
977        assert!(ServerConfig::get_api_key_optional().is_some());
978        assert_eq!(ServerConfig::get_rate_limit_requests_per_second(), 500);
979        assert_eq!(ServerConfig::get_rate_limit_burst_size(), 1000);
980        assert_eq!(ServerConfig::get_metrics_port(), 9999);
981        assert!(ServerConfig::get_enable_swagger());
982        assert_eq!(ServerConfig::get_redis_connection_timeout_ms(), 5000);
983        assert_eq!(ServerConfig::get_redis_key_prefix(), "custom-prefix");
984        assert_eq!(ServerConfig::get_rpc_timeout_ms(), 15000);
985        assert_eq!(ServerConfig::get_provider_max_retries(), 5);
986        assert_eq!(ServerConfig::get_provider_retry_base_delay_ms(), 200);
987        assert_eq!(ServerConfig::get_provider_retry_max_delay_ms(), 5000);
988        assert_eq!(ServerConfig::get_provider_max_failovers(), 10);
989        assert_eq!(
990            ServerConfig::get_repository_storage_type(),
991            RepositoryStorageType::Redis
992        );
993        assert!(ServerConfig::get_reset_storage_on_start());
994        assert!(ServerConfig::get_storage_encryption_key().is_some());
995        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 12.0);
996        assert_eq!(ServerConfig::get_redis_pool_max_size(), 200);
997        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 20000);
998    }
999
1000    #[test]
1001    fn test_get_redis_pool_max_size() {
1002        let _lock = match ENV_MUTEX.lock() {
1003            Ok(guard) => guard,
1004            Err(poisoned) => poisoned.into_inner(),
1005        };
1006        // Test default value when env var is not set
1007        env::remove_var("REDIS_POOL_MAX_SIZE");
1008        assert_eq!(ServerConfig::get_redis_pool_max_size(), 500);
1009
1010        // Test custom value
1011        env::set_var("REDIS_POOL_MAX_SIZE", "100");
1012        assert_eq!(ServerConfig::get_redis_pool_max_size(), 100);
1013
1014        // Test invalid value returns default
1015        env::set_var("REDIS_POOL_MAX_SIZE", "not_a_number");
1016        assert_eq!(ServerConfig::get_redis_pool_max_size(), 500);
1017
1018        // Test zero value returns default (invalid)
1019        env::set_var("REDIS_POOL_MAX_SIZE", "0");
1020        assert_eq!(ServerConfig::get_redis_pool_max_size(), 500);
1021
1022        // Test large value
1023        env::set_var("REDIS_POOL_MAX_SIZE", "10000");
1024        assert_eq!(ServerConfig::get_redis_pool_max_size(), 10000);
1025
1026        // Cleanup
1027        env::remove_var("REDIS_POOL_MAX_SIZE");
1028    }
1029
1030    #[test]
1031    fn test_get_redis_pool_timeout_ms() {
1032        let _lock = match ENV_MUTEX.lock() {
1033            Ok(guard) => guard,
1034            Err(poisoned) => poisoned.into_inner(),
1035        };
1036
1037        // Test default value when env var is not set
1038        env::remove_var("REDIS_POOL_TIMEOUT_MS");
1039        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 10000);
1040
1041        // Test custom value
1042        env::set_var("REDIS_POOL_TIMEOUT_MS", "15000");
1043        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 15000);
1044
1045        // Test invalid value returns default
1046        env::set_var("REDIS_POOL_TIMEOUT_MS", "not_a_number");
1047        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 10000);
1048
1049        // Test zero value returns default (invalid)
1050        env::set_var("REDIS_POOL_TIMEOUT_MS", "0");
1051        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 10000);
1052
1053        // Test large value
1054        env::set_var("REDIS_POOL_TIMEOUT_MS", "60000");
1055        assert_eq!(ServerConfig::get_redis_pool_timeout_ms(), 60000);
1056
1057        // Cleanup
1058        env::remove_var("REDIS_POOL_TIMEOUT_MS");
1059    }
1060
1061    #[test]
1062    fn test_fractional_transaction_expiration_hours() {
1063        let _lock = match ENV_MUTEX.lock() {
1064            Ok(guard) => guard,
1065            Err(poisoned) => poisoned.into_inner(),
1066        };
1067        setup();
1068
1069        // Test fractional hours (0.1 hours = 6 minutes)
1070        env::set_var("TRANSACTION_EXPIRATION_HOURS", "0.1");
1071        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 0.1);
1072
1073        // Test another fractional value
1074        env::set_var("TRANSACTION_EXPIRATION_HOURS", "0.5");
1075        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 0.5);
1076
1077        // Test integer value still works
1078        env::set_var("TRANSACTION_EXPIRATION_HOURS", "24");
1079        assert_eq!(ServerConfig::get_transaction_expiration_hours(), 24.0);
1080
1081        // Cleanup
1082        env::remove_var("TRANSACTION_EXPIRATION_HOURS");
1083    }
1084
1085    #[test]
1086    #[should_panic(expected = "REDIS_URL must be set")]
1087    fn test_get_redis_url_panics_when_not_set() {
1088        let _lock = match ENV_MUTEX.lock() {
1089            Ok(guard) => guard,
1090            Err(poisoned) => poisoned.into_inner(),
1091        };
1092
1093        env::remove_var("REDIS_URL");
1094        let _ = ServerConfig::get_redis_url();
1095    }
1096
1097    #[test]
1098    #[should_panic(expected = "API_KEY must be set")]
1099    fn test_get_api_key_panics_when_not_set() {
1100        let _lock = match ENV_MUTEX.lock() {
1101            Ok(guard) => guard,
1102            Err(poisoned) => poisoned.into_inner(),
1103        };
1104
1105        env::remove_var("API_KEY");
1106        let _ = ServerConfig::get_api_key();
1107    }
1108
1109    #[test]
1110    fn test_optional_getters_return_none_safely() {
1111        let _lock = match ENV_MUTEX.lock() {
1112            Ok(guard) => guard,
1113            Err(poisoned) => poisoned.into_inner(),
1114        };
1115
1116        env::remove_var("REDIS_URL");
1117        env::remove_var("API_KEY");
1118        env::remove_var("STORAGE_ENCRYPTION_KEY");
1119
1120        assert!(ServerConfig::get_redis_url_optional().is_none());
1121        assert!(ServerConfig::get_api_key_optional().is_none());
1122        assert!(ServerConfig::get_storage_encryption_key().is_none());
1123    }
1124
1125    #[test]
1126    fn test_refactored_from_env_equivalence() {
1127        let _lock = match ENV_MUTEX.lock() {
1128            Ok(guard) => guard,
1129            Err(poisoned) => poisoned.into_inner(),
1130        };
1131        setup();
1132
1133        // Set custom values to test both default and custom paths
1134        env::set_var("HOST", "custom-host");
1135        env::set_var("APP_PORT", "7777");
1136        env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "250");
1137        env::set_var("METRICS_PORT", "7778");
1138        env::set_var("ENABLE_SWAGGER", "true");
1139        env::set_var("PROVIDER_MAX_RETRIES", "7");
1140        env::set_var("TRANSACTION_EXPIRATION_HOURS", "8");
1141
1142        let config = ServerConfig::from_env();
1143
1144        // Verify the refactored from_env() produces the same results as individual getters
1145        assert_eq!(config.host, ServerConfig::get_host());
1146        assert_eq!(config.port, ServerConfig::get_port());
1147        assert_eq!(config.redis_url, ServerConfig::get_redis_url());
1148        assert_eq!(
1149            config.config_file_path,
1150            ServerConfig::get_config_file_path()
1151        );
1152        assert_eq!(config.api_key, ServerConfig::get_api_key());
1153        assert_eq!(
1154            config.rate_limit_requests_per_second,
1155            ServerConfig::get_rate_limit_requests_per_second()
1156        );
1157        assert_eq!(
1158            config.rate_limit_burst_size,
1159            ServerConfig::get_rate_limit_burst_size()
1160        );
1161        assert_eq!(config.metrics_port, ServerConfig::get_metrics_port());
1162        assert_eq!(config.enable_swagger, ServerConfig::get_enable_swagger());
1163        assert_eq!(
1164            config.redis_connection_timeout_ms,
1165            ServerConfig::get_redis_connection_timeout_ms()
1166        );
1167        assert_eq!(
1168            config.redis_key_prefix,
1169            ServerConfig::get_redis_key_prefix()
1170        );
1171        assert_eq!(config.rpc_timeout_ms, ServerConfig::get_rpc_timeout_ms());
1172        assert_eq!(
1173            config.provider_max_retries,
1174            ServerConfig::get_provider_max_retries()
1175        );
1176        assert_eq!(
1177            config.provider_retry_base_delay_ms,
1178            ServerConfig::get_provider_retry_base_delay_ms()
1179        );
1180        assert_eq!(
1181            config.provider_retry_max_delay_ms,
1182            ServerConfig::get_provider_retry_max_delay_ms()
1183        );
1184        assert_eq!(
1185            config.provider_max_failovers,
1186            ServerConfig::get_provider_max_failovers()
1187        );
1188        assert_eq!(
1189            config.repository_storage_type,
1190            ServerConfig::get_repository_storage_type()
1191        );
1192        assert_eq!(
1193            config.reset_storage_on_start,
1194            ServerConfig::get_reset_storage_on_start()
1195        );
1196        assert_eq!(
1197            config.storage_encryption_key,
1198            ServerConfig::get_storage_encryption_key()
1199        );
1200        assert_eq!(
1201            config.transaction_expiration_hours,
1202            ServerConfig::get_transaction_expiration_hours()
1203        );
1204    }
1205
1206    mod get_worker_concurrency_tests {
1207        use super::*;
1208        use serial_test::serial;
1209
1210        #[test]
1211        #[serial]
1212        fn test_returns_default_when_env_not_set() {
1213            let worker_name = "test_worker";
1214            let env_var = format!(
1215                "BACKGROUND_WORKER_{}_CONCURRENCY",
1216                worker_name.to_uppercase()
1217            );
1218
1219            // Ensure env var is not set
1220            env::remove_var(&env_var);
1221
1222            let default_value = 42;
1223            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1224
1225            assert_eq!(
1226                result, default_value,
1227                "Should return default value when env var is not set"
1228            );
1229        }
1230
1231        #[test]
1232        #[serial]
1233        fn test_returns_env_value_when_set() {
1234            let worker_name = "status_checker";
1235            let env_var = format!(
1236                "BACKGROUND_WORKER_{}_CONCURRENCY",
1237                worker_name.to_uppercase()
1238            );
1239
1240            // Set env var to a specific value
1241            env::set_var(&env_var, "100");
1242
1243            let default_value = 10;
1244            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1245
1246            assert_eq!(result, 100, "Should return env var value when set");
1247
1248            // Cleanup
1249            env::remove_var(&env_var);
1250        }
1251
1252        #[test]
1253        #[serial]
1254        fn test_returns_default_when_env_invalid() {
1255            let worker_name = "invalid_worker";
1256            let env_var = format!(
1257                "BACKGROUND_WORKER_{}_CONCURRENCY",
1258                worker_name.to_uppercase()
1259            );
1260
1261            // Set env var to invalid value
1262            env::set_var(&env_var, "not_a_number");
1263
1264            let default_value = 25;
1265            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1266
1267            assert_eq!(
1268                result, default_value,
1269                "Should return default value when env var is invalid"
1270            );
1271
1272            // Cleanup
1273            env::remove_var(&env_var);
1274        }
1275
1276        #[test]
1277        #[serial]
1278        fn test_returns_default_when_env_empty() {
1279            let worker_name = "empty_worker";
1280            let env_var = format!(
1281                "BACKGROUND_WORKER_{}_CONCURRENCY",
1282                worker_name.to_uppercase()
1283            );
1284
1285            // Set env var to empty string
1286            env::set_var(&env_var, "");
1287
1288            let default_value = 15;
1289            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1290
1291            assert_eq!(
1292                result, default_value,
1293                "Should return default value when env var is empty"
1294            );
1295
1296            // Cleanup
1297            env::remove_var(&env_var);
1298        }
1299
1300        #[test]
1301        #[serial]
1302        fn test_returns_default_when_env_negative() {
1303            let worker_name = "negative_worker";
1304            let env_var = format!(
1305                "BACKGROUND_WORKER_{}_CONCURRENCY",
1306                worker_name.to_uppercase()
1307            );
1308
1309            // Set env var to negative value
1310            env::set_var(&env_var, "-5");
1311
1312            let default_value = 20;
1313            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1314
1315            assert_eq!(
1316                result, default_value,
1317                "Should return default value when env var is negative"
1318            );
1319
1320            // Cleanup
1321            env::remove_var(&env_var);
1322        }
1323
1324        #[test]
1325        #[serial]
1326        fn test_env_var_name_formatting() {
1327            // Test that worker names are properly uppercased
1328            let worker_names = vec![
1329                (
1330                    "transaction_sender",
1331                    "BACKGROUND_WORKER_TRANSACTION_SENDER_CONCURRENCY",
1332                ),
1333                (
1334                    "status_checker_evm",
1335                    "BACKGROUND_WORKER_STATUS_CHECKER_EVM_CONCURRENCY",
1336                ),
1337                (
1338                    "notification_sender",
1339                    "BACKGROUND_WORKER_NOTIFICATION_SENDER_CONCURRENCY",
1340                ),
1341            ];
1342
1343            for (worker_name, expected_env_var) in worker_names {
1344                let actual_env_var = format!(
1345                    "BACKGROUND_WORKER_{}_CONCURRENCY",
1346                    worker_name.to_uppercase()
1347                );
1348                assert_eq!(
1349                    actual_env_var, expected_env_var,
1350                    "Env var name should be correctly formatted for worker: {worker_name}"
1351                );
1352            }
1353        }
1354
1355        #[test]
1356        #[serial]
1357        fn test_zero_value() {
1358            let worker_name = "zero_worker";
1359            let env_var = format!(
1360                "BACKGROUND_WORKER_{}_CONCURRENCY",
1361                worker_name.to_uppercase()
1362            );
1363
1364            // Set env var to zero
1365            env::set_var(&env_var, "0");
1366
1367            let default_value = 30;
1368            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1369
1370            assert_eq!(result, 0, "Should accept zero as a valid value");
1371
1372            // Cleanup
1373            env::remove_var(&env_var);
1374        }
1375
1376        #[test]
1377        #[serial]
1378        fn test_large_value() {
1379            let worker_name = "large_worker";
1380            let env_var = format!(
1381                "BACKGROUND_WORKER_{}_CONCURRENCY",
1382                worker_name.to_uppercase()
1383            );
1384
1385            // Set env var to a large value
1386            env::set_var(&env_var, "10000");
1387
1388            let default_value = 50;
1389            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1390
1391            assert_eq!(result, 10000, "Should accept large values");
1392
1393            // Cleanup
1394            env::remove_var(&env_var);
1395        }
1396
1397        #[test]
1398        #[serial]
1399        fn test_whitespace_in_value() {
1400            let worker_name = "whitespace_worker";
1401            let env_var = format!(
1402                "BACKGROUND_WORKER_{}_CONCURRENCY",
1403                worker_name.to_uppercase()
1404            );
1405
1406            // Set env var with leading/trailing whitespace
1407            env::set_var(&env_var, "  75  ");
1408
1409            let default_value = 35;
1410            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1411
1412            // Note: String::parse::<usize>() does NOT trim whitespace, so this will fail to parse
1413            // and return the default value
1414            assert_eq!(
1415                result, default_value,
1416                "Should return default value when value has whitespace"
1417            );
1418
1419            // Cleanup
1420            env::remove_var(&env_var);
1421        }
1422
1423        #[test]
1424        #[serial]
1425        fn test_float_value_returns_default() {
1426            let worker_name = "float_worker";
1427            let env_var = format!(
1428                "BACKGROUND_WORKER_{}_CONCURRENCY",
1429                worker_name.to_uppercase()
1430            );
1431
1432            // Set env var to float value
1433            env::set_var(&env_var, "12.5");
1434
1435            let default_value = 40;
1436            let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
1437
1438            assert_eq!(
1439                result, default_value,
1440                "Should return default value for float input"
1441            );
1442
1443            // Cleanup
1444            env::remove_var(&env_var);
1445        }
1446    }
1447
1448    mod get_relayer_concurrency_limit_tests {
1449        use super::*;
1450        use serial_test::serial;
1451
1452        #[test]
1453        #[serial]
1454        fn test_returns_default_when_env_not_set() {
1455            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1456            let result = ServerConfig::get_relayer_concurrency_limit();
1457            assert_eq!(result, 100, "Should return default value of 100");
1458        }
1459
1460        #[test]
1461        #[serial]
1462        fn test_returns_env_value_when_set() {
1463            env::set_var("RELAYER_CONCURRENCY_LIMIT", "250");
1464            let result = ServerConfig::get_relayer_concurrency_limit();
1465            assert_eq!(result, 250, "Should return env var value");
1466            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1467        }
1468
1469        #[test]
1470        #[serial]
1471        fn test_returns_default_when_env_invalid() {
1472            env::set_var("RELAYER_CONCURRENCY_LIMIT", "not_a_number");
1473            let result = ServerConfig::get_relayer_concurrency_limit();
1474            assert_eq!(result, 100, "Should return default value when invalid");
1475            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1476        }
1477
1478        #[test]
1479        #[serial]
1480        fn test_returns_default_when_env_empty() {
1481            env::set_var("RELAYER_CONCURRENCY_LIMIT", "");
1482            let result = ServerConfig::get_relayer_concurrency_limit();
1483            assert_eq!(result, 100, "Should return default value when empty");
1484            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1485        }
1486
1487        #[test]
1488        #[serial]
1489        fn test_zero_value() {
1490            env::set_var("RELAYER_CONCURRENCY_LIMIT", "0");
1491            let result = ServerConfig::get_relayer_concurrency_limit();
1492            assert_eq!(result, 0, "Should accept zero as valid value");
1493            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1494        }
1495
1496        #[test]
1497        #[serial]
1498        fn test_large_value() {
1499            env::set_var("RELAYER_CONCURRENCY_LIMIT", "5000");
1500            let result = ServerConfig::get_relayer_concurrency_limit();
1501            assert_eq!(result, 5000, "Should accept large values");
1502            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1503        }
1504
1505        #[test]
1506        #[serial]
1507        fn test_negative_value_returns_default() {
1508            env::set_var("RELAYER_CONCURRENCY_LIMIT", "-10");
1509            let result = ServerConfig::get_relayer_concurrency_limit();
1510            assert_eq!(result, 100, "Should return default for negative value");
1511            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1512        }
1513
1514        #[test]
1515        #[serial]
1516        fn test_float_value_returns_default() {
1517            env::set_var("RELAYER_CONCURRENCY_LIMIT", "100.5");
1518            let result = ServerConfig::get_relayer_concurrency_limit();
1519            assert_eq!(result, 100, "Should return default for float value");
1520            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1521        }
1522
1523        #[test]
1524        #[serial]
1525        fn test_whitespace_value_returns_default() {
1526            env::set_var("RELAYER_CONCURRENCY_LIMIT", "  150  ");
1527            let result = ServerConfig::get_relayer_concurrency_limit();
1528            assert_eq!(
1529                result, 100,
1530                "Should return default when value has whitespace"
1531            );
1532            env::remove_var("RELAYER_CONCURRENCY_LIMIT");
1533        }
1534    }
1535
1536    mod get_max_connections_tests {
1537        use super::*;
1538        use serial_test::serial;
1539
1540        #[test]
1541        #[serial]
1542        fn test_returns_default_when_env_not_set() {
1543            env::remove_var("MAX_CONNECTIONS");
1544            let result = ServerConfig::get_max_connections();
1545            assert_eq!(result, 256, "Should return default value of 256");
1546        }
1547
1548        #[test]
1549        #[serial]
1550        fn test_returns_env_value_when_set() {
1551            env::set_var("MAX_CONNECTIONS", "512");
1552            let result = ServerConfig::get_max_connections();
1553            assert_eq!(result, 512, "Should return env var value");
1554            env::remove_var("MAX_CONNECTIONS");
1555        }
1556
1557        #[test]
1558        #[serial]
1559        fn test_returns_default_when_env_invalid() {
1560            env::set_var("MAX_CONNECTIONS", "invalid");
1561            let result = ServerConfig::get_max_connections();
1562            assert_eq!(result, 256, "Should return default value when invalid");
1563            env::remove_var("MAX_CONNECTIONS");
1564        }
1565
1566        #[test]
1567        #[serial]
1568        fn test_returns_default_when_env_empty() {
1569            env::set_var("MAX_CONNECTIONS", "");
1570            let result = ServerConfig::get_max_connections();
1571            assert_eq!(result, 256, "Should return default value when empty");
1572            env::remove_var("MAX_CONNECTIONS");
1573        }
1574
1575        #[test]
1576        #[serial]
1577        fn test_zero_value() {
1578            env::set_var("MAX_CONNECTIONS", "0");
1579            let result = ServerConfig::get_max_connections();
1580            assert_eq!(result, 0, "Should accept zero as valid value");
1581            env::remove_var("MAX_CONNECTIONS");
1582        }
1583
1584        #[test]
1585        #[serial]
1586        fn test_large_value() {
1587            env::set_var("MAX_CONNECTIONS", "10000");
1588            let result = ServerConfig::get_max_connections();
1589            assert_eq!(result, 10000, "Should accept large values");
1590            env::remove_var("MAX_CONNECTIONS");
1591        }
1592
1593        #[test]
1594        #[serial]
1595        fn test_negative_value_returns_default() {
1596            env::set_var("MAX_CONNECTIONS", "-100");
1597            let result = ServerConfig::get_max_connections();
1598            assert_eq!(result, 256, "Should return default for negative value");
1599            env::remove_var("MAX_CONNECTIONS");
1600        }
1601
1602        #[test]
1603        #[serial]
1604        fn test_float_value_returns_default() {
1605            env::set_var("MAX_CONNECTIONS", "256.5");
1606            let result = ServerConfig::get_max_connections();
1607            assert_eq!(result, 256, "Should return default for float value");
1608            env::remove_var("MAX_CONNECTIONS");
1609        }
1610    }
1611
1612    mod get_connection_backlog_tests {
1613        use super::*;
1614        use serial_test::serial;
1615
1616        #[test]
1617        #[serial]
1618        fn test_returns_default_when_env_not_set() {
1619            env::remove_var("CONNECTION_BACKLOG");
1620            let result = ServerConfig::get_connection_backlog();
1621            assert_eq!(result, 511, "Should return default value of 511");
1622        }
1623
1624        #[test]
1625        #[serial]
1626        fn test_returns_env_value_when_set() {
1627            env::set_var("CONNECTION_BACKLOG", "1024");
1628            let result = ServerConfig::get_connection_backlog();
1629            assert_eq!(result, 1024, "Should return env var value");
1630            env::remove_var("CONNECTION_BACKLOG");
1631        }
1632
1633        #[test]
1634        #[serial]
1635        fn test_returns_default_when_env_invalid() {
1636            env::set_var("CONNECTION_BACKLOG", "not_a_number");
1637            let result = ServerConfig::get_connection_backlog();
1638            assert_eq!(result, 511, "Should return default value when invalid");
1639            env::remove_var("CONNECTION_BACKLOG");
1640        }
1641
1642        #[test]
1643        #[serial]
1644        fn test_returns_default_when_env_empty() {
1645            env::set_var("CONNECTION_BACKLOG", "");
1646            let result = ServerConfig::get_connection_backlog();
1647            assert_eq!(result, 511, "Should return default value when empty");
1648            env::remove_var("CONNECTION_BACKLOG");
1649        }
1650
1651        #[test]
1652        #[serial]
1653        fn test_zero_value() {
1654            env::set_var("CONNECTION_BACKLOG", "0");
1655            let result = ServerConfig::get_connection_backlog();
1656            assert_eq!(result, 0, "Should accept zero as valid value");
1657            env::remove_var("CONNECTION_BACKLOG");
1658        }
1659
1660        #[test]
1661        #[serial]
1662        fn test_large_value() {
1663            env::set_var("CONNECTION_BACKLOG", "65535");
1664            let result = ServerConfig::get_connection_backlog();
1665            assert_eq!(result, 65535, "Should accept large values");
1666            env::remove_var("CONNECTION_BACKLOG");
1667        }
1668
1669        #[test]
1670        #[serial]
1671        fn test_negative_value_returns_default() {
1672            env::set_var("CONNECTION_BACKLOG", "-50");
1673            let result = ServerConfig::get_connection_backlog();
1674            assert_eq!(result, 511, "Should return default for negative value");
1675            env::remove_var("CONNECTION_BACKLOG");
1676        }
1677
1678        #[test]
1679        #[serial]
1680        fn test_float_value_returns_default() {
1681            env::set_var("CONNECTION_BACKLOG", "511.5");
1682            let result = ServerConfig::get_connection_backlog();
1683            assert_eq!(result, 511, "Should return default for float value");
1684            env::remove_var("CONNECTION_BACKLOG");
1685        }
1686
1687        #[test]
1688        #[serial]
1689        fn test_common_production_values() {
1690            // Test common production values
1691            let test_cases = vec![
1692                (128, "Small server"),
1693                (511, "Default"),
1694                (1024, "Medium server"),
1695                (2048, "Large server"),
1696            ];
1697
1698            for (value, description) in test_cases {
1699                env::set_var("CONNECTION_BACKLOG", value.to_string());
1700                let result = ServerConfig::get_connection_backlog();
1701                assert_eq!(result, value, "Should accept {description}: {value}");
1702            }
1703
1704            env::remove_var("CONNECTION_BACKLOG");
1705        }
1706    }
1707
1708    mod get_request_timeout_seconds_tests {
1709        use super::*;
1710        use serial_test::serial;
1711
1712        #[test]
1713        #[serial]
1714        fn test_returns_default_when_env_not_set() {
1715            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1716            let result = ServerConfig::get_request_timeout_seconds();
1717            assert_eq!(result, 30, "Should return default value of 30");
1718        }
1719
1720        #[test]
1721        #[serial]
1722        fn test_returns_env_value_when_set() {
1723            env::set_var("REQUEST_TIMEOUT_SECONDS", "60");
1724            let result = ServerConfig::get_request_timeout_seconds();
1725            assert_eq!(result, 60, "Should return env var value");
1726            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1727        }
1728
1729        #[test]
1730        #[serial]
1731        fn test_returns_default_when_env_invalid() {
1732            env::set_var("REQUEST_TIMEOUT_SECONDS", "invalid");
1733            let result = ServerConfig::get_request_timeout_seconds();
1734            assert_eq!(result, 30, "Should return default value when invalid");
1735            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1736        }
1737
1738        #[test]
1739        #[serial]
1740        fn test_returns_default_when_env_empty() {
1741            env::set_var("REQUEST_TIMEOUT_SECONDS", "");
1742            let result = ServerConfig::get_request_timeout_seconds();
1743            assert_eq!(result, 30, "Should return default value when empty");
1744            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1745        }
1746
1747        #[test]
1748        #[serial]
1749        fn test_zero_value() {
1750            env::set_var("REQUEST_TIMEOUT_SECONDS", "0");
1751            let result = ServerConfig::get_request_timeout_seconds();
1752            assert_eq!(result, 0, "Should accept zero as valid value");
1753            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1754        }
1755
1756        #[test]
1757        #[serial]
1758        fn test_large_value() {
1759            env::set_var("REQUEST_TIMEOUT_SECONDS", "300");
1760            let result = ServerConfig::get_request_timeout_seconds();
1761            assert_eq!(result, 300, "Should accept large values");
1762            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1763        }
1764
1765        #[test]
1766        #[serial]
1767        fn test_negative_value_returns_default() {
1768            env::set_var("REQUEST_TIMEOUT_SECONDS", "-10");
1769            let result = ServerConfig::get_request_timeout_seconds();
1770            assert_eq!(result, 30, "Should return default for negative value");
1771            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1772        }
1773
1774        #[test]
1775        #[serial]
1776        fn test_float_value_returns_default() {
1777            env::set_var("REQUEST_TIMEOUT_SECONDS", "30.5");
1778            let result = ServerConfig::get_request_timeout_seconds();
1779            assert_eq!(result, 30, "Should return default for float value");
1780            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1781        }
1782
1783        #[test]
1784        #[serial]
1785        fn test_common_timeout_values() {
1786            // Test common timeout values
1787            let test_cases = vec![
1788                (10, "Short timeout"),
1789                (30, "Default timeout"),
1790                (60, "Moderate timeout"),
1791                (120, "Long timeout"),
1792            ];
1793
1794            for (value, description) in test_cases {
1795                env::set_var("REQUEST_TIMEOUT_SECONDS", value.to_string());
1796                let result = ServerConfig::get_request_timeout_seconds();
1797                assert_eq!(result, value, "Should accept {description}: {value}");
1798            }
1799
1800            env::remove_var("REQUEST_TIMEOUT_SECONDS");
1801        }
1802    }
1803
1804    mod get_redis_reader_url_tests {
1805        use super::*;
1806        use serial_test::serial;
1807
1808        #[test]
1809        #[serial]
1810        fn test_returns_none_when_env_not_set() {
1811            env::remove_var("REDIS_READER_URL");
1812            let result = ServerConfig::get_redis_reader_url_optional();
1813            assert!(
1814                result.is_none(),
1815                "Should return None when env var is not set"
1816            );
1817        }
1818
1819        #[test]
1820        #[serial]
1821        fn test_returns_value_when_set() {
1822            env::set_var("REDIS_READER_URL", "redis://reader:6379");
1823            let result = ServerConfig::get_redis_reader_url_optional();
1824            assert_eq!(
1825                result,
1826                Some("redis://reader:6379".to_string()),
1827                "Should return the env var value"
1828            );
1829            env::remove_var("REDIS_READER_URL");
1830        }
1831
1832        #[test]
1833        #[serial]
1834        fn test_returns_empty_string_when_set_to_empty() {
1835            env::set_var("REDIS_READER_URL", "");
1836            let result = ServerConfig::get_redis_reader_url_optional();
1837            assert_eq!(
1838                result,
1839                Some("".to_string()),
1840                "Should return empty string when set to empty"
1841            );
1842            env::remove_var("REDIS_READER_URL");
1843        }
1844
1845        #[test]
1846        #[serial]
1847        fn test_aws_elasticache_reader_url() {
1848            // Test with typical AWS ElastiCache reader endpoint format
1849            let reader_url = "redis://my-cluster-ro.xxx.cache.amazonaws.com:6379";
1850            env::set_var("REDIS_READER_URL", reader_url);
1851            let result = ServerConfig::get_redis_reader_url_optional();
1852            assert_eq!(
1853                result,
1854                Some(reader_url.to_string()),
1855                "Should accept AWS ElastiCache reader endpoint"
1856            );
1857            env::remove_var("REDIS_READER_URL");
1858        }
1859
1860        #[test]
1861        #[serial]
1862        fn test_config_includes_redis_reader_url() {
1863            env::set_var("REDIS_URL", "redis://primary:6379");
1864            env::set_var("REDIS_READER_URL", "redis://reader:6379");
1865            env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
1866
1867            let config = ServerConfig::from_env();
1868
1869            assert_eq!(config.redis_url, "redis://primary:6379");
1870            assert_eq!(
1871                config.redis_reader_url,
1872                Some("redis://reader:6379".to_string())
1873            );
1874
1875            env::remove_var("REDIS_URL");
1876            env::remove_var("REDIS_READER_URL");
1877            env::remove_var("API_KEY");
1878        }
1879
1880        #[test]
1881        #[serial]
1882        fn test_config_without_redis_reader_url() {
1883            env::set_var("REDIS_URL", "redis://primary:6379");
1884            env::remove_var("REDIS_READER_URL");
1885            env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
1886
1887            let config = ServerConfig::from_env();
1888
1889            assert_eq!(config.redis_url, "redis://primary:6379");
1890            assert!(
1891                config.redis_reader_url.is_none(),
1892                "redis_reader_url should be None when not set"
1893            );
1894
1895            env::remove_var("REDIS_URL");
1896            env::remove_var("API_KEY");
1897        }
1898    }
1899
1900    mod get_sqs_queue_type_tests {
1901        use super::*;
1902        use serial_test::serial;
1903
1904        #[test]
1905        #[serial]
1906        fn test_returns_auto_when_env_not_set() {
1907            env::remove_var("SQS_QUEUE_TYPE");
1908            let result = ServerConfig::get_sqs_queue_type();
1909            assert_eq!(result, "auto", "Should default to 'auto'");
1910        }
1911
1912        #[test]
1913        #[serial]
1914        fn test_returns_fifo_when_set() {
1915            env::set_var("SQS_QUEUE_TYPE", "fifo");
1916            let result = ServerConfig::get_sqs_queue_type();
1917            assert_eq!(result, "fifo");
1918            env::remove_var("SQS_QUEUE_TYPE");
1919        }
1920
1921        #[test]
1922        #[serial]
1923        fn test_returns_standard_when_set() {
1924            env::set_var("SQS_QUEUE_TYPE", "standard");
1925            let result = ServerConfig::get_sqs_queue_type();
1926            assert_eq!(result, "standard");
1927            env::remove_var("SQS_QUEUE_TYPE");
1928        }
1929
1930        #[test]
1931        #[serial]
1932        fn test_returns_raw_value_for_unknown() {
1933            env::set_var("SQS_QUEUE_TYPE", "unknown");
1934            let result = ServerConfig::get_sqs_queue_type();
1935            assert_eq!(result, "unknown");
1936            env::remove_var("SQS_QUEUE_TYPE");
1937        }
1938    }
1939
1940    mod get_redis_reader_pool_max_size_tests {
1941        use super::*;
1942        use serial_test::serial;
1943
1944        #[test]
1945        #[serial]
1946        fn test_returns_default_when_env_not_set() {
1947            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1948            let result = ServerConfig::get_redis_reader_pool_max_size();
1949            assert_eq!(
1950                result, 1000,
1951                "Should return default 1000 when env var is not set"
1952            );
1953        }
1954
1955        #[test]
1956        #[serial]
1957        fn test_returns_value_when_set() {
1958            env::set_var("REDIS_READER_POOL_MAX_SIZE", "2000");
1959            let result = ServerConfig::get_redis_reader_pool_max_size();
1960            assert_eq!(result, 2000, "Should return the parsed value");
1961            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1962        }
1963
1964        #[test]
1965        #[serial]
1966        fn test_returns_default_when_invalid() {
1967            env::set_var("REDIS_READER_POOL_MAX_SIZE", "not_a_number");
1968            let result = ServerConfig::get_redis_reader_pool_max_size();
1969            assert_eq!(
1970                result, 1000,
1971                "Should return default 1000 for invalid values"
1972            );
1973            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1974        }
1975
1976        #[test]
1977        #[serial]
1978        fn test_returns_default_when_zero() {
1979            env::set_var("REDIS_READER_POOL_MAX_SIZE", "0");
1980            let result = ServerConfig::get_redis_reader_pool_max_size();
1981            assert_eq!(result, 1000, "Should return default 1000 when value is 0");
1982            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1983        }
1984
1985        #[test]
1986        #[serial]
1987        fn test_returns_default_when_negative() {
1988            env::set_var("REDIS_READER_POOL_MAX_SIZE", "-100");
1989            let result = ServerConfig::get_redis_reader_pool_max_size();
1990            assert_eq!(
1991                result, 1000,
1992                "Should return default 1000 for negative values"
1993            );
1994            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
1995        }
1996
1997        #[test]
1998        #[serial]
1999        fn test_config_includes_reader_pool_max_size() {
2000            env::set_var("REDIS_URL", "redis://primary:6379");
2001            env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
2002            env::set_var("REDIS_READER_POOL_MAX_SIZE", "750");
2003
2004            let config = ServerConfig::from_env();
2005
2006            assert_eq!(
2007                config.redis_reader_pool_max_size, 750,
2008                "Should include reader pool max size in config"
2009            );
2010
2011            env::remove_var("REDIS_URL");
2012            env::remove_var("API_KEY");
2013            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
2014        }
2015
2016        #[test]
2017        #[serial]
2018        fn test_config_uses_default_when_not_set() {
2019            env::set_var("REDIS_URL", "redis://primary:6379");
2020            env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
2021            env::remove_var("REDIS_READER_POOL_MAX_SIZE");
2022
2023            let config = ServerConfig::from_env();
2024
2025            assert_eq!(
2026                config.redis_reader_pool_max_size, 1000,
2027                "Should use default 1000 when not set"
2028            );
2029
2030            env::remove_var("REDIS_URL");
2031            env::remove_var("API_KEY");
2032        }
2033    }
2034}