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