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