1use std::{env, str::FromStr};
3use strum::Display;
4
5use crate::{constants::MINIMUM_SECRET_VALUE_LENGTH, models::SecretString};
6
7#[derive(Debug, Clone, PartialEq, Eq, Display)]
8pub enum RepositoryStorageType {
9 InMemory,
10 Redis,
11}
12
13impl FromStr for RepositoryStorageType {
14 type Err = String;
15
16 fn from_str(s: &str) -> Result<Self, Self::Err> {
17 match s.to_lowercase().as_str() {
18 "inmemory" | "in_memory" => Ok(Self::InMemory),
19 "redis" => Ok(Self::Redis),
20 _ => Err(format!("Invalid repository storage type: {s}")),
21 }
22 }
23}
24
25#[derive(Debug, Clone)]
26pub struct ServerConfig {
27 pub host: String,
29 pub port: u16,
31 pub redis_url: String,
33 pub config_file_path: String,
35 pub api_key: SecretString,
37 pub rate_limit_requests_per_second: u64,
39 pub rate_limit_burst_size: u32,
41 pub metrics_port: u16,
43 pub enable_swagger: bool,
45 pub redis_connection_timeout_ms: u64,
47 pub redis_key_prefix: String,
49 pub rpc_timeout_ms: u64,
51 pub provider_max_retries: u8,
53 pub provider_retry_base_delay_ms: u64,
55 pub provider_retry_max_delay_ms: u64,
57 pub provider_max_failovers: u8,
59 pub repository_storage_type: RepositoryStorageType,
61 pub reset_storage_on_start: bool,
63 pub storage_encryption_key: Option<SecretString>,
65 pub transaction_expiration_hours: u64,
67}
68
69impl ServerConfig {
70 pub fn from_env() -> Self {
92 Self {
93 host: Self::get_host(),
94 port: Self::get_port(),
95 redis_url: Self::get_redis_url(), config_file_path: Self::get_config_file_path(),
97 api_key: Self::get_api_key(), rate_limit_requests_per_second: Self::get_rate_limit_requests_per_second(),
99 rate_limit_burst_size: Self::get_rate_limit_burst_size(),
100 metrics_port: Self::get_metrics_port(),
101 enable_swagger: Self::get_enable_swagger(),
102 redis_connection_timeout_ms: Self::get_redis_connection_timeout_ms(),
103 redis_key_prefix: Self::get_redis_key_prefix(),
104 rpc_timeout_ms: Self::get_rpc_timeout_ms(),
105 provider_max_retries: Self::get_provider_max_retries(),
106 provider_retry_base_delay_ms: Self::get_provider_retry_base_delay_ms(),
107 provider_retry_max_delay_ms: Self::get_provider_retry_max_delay_ms(),
108 provider_max_failovers: Self::get_provider_max_failovers(),
109 repository_storage_type: Self::get_repository_storage_type(),
110 reset_storage_on_start: Self::get_reset_storage_on_start(),
111 storage_encryption_key: Self::get_storage_encryption_key(),
112 transaction_expiration_hours: Self::get_transaction_expiration_hours(),
113 }
114 }
115
116 pub fn get_host() -> String {
120 env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string())
121 }
122
123 pub fn get_port() -> u16 {
125 env::var("APP_PORT")
126 .unwrap_or_else(|_| "8080".to_string())
127 .parse()
128 .unwrap_or(8080)
129 }
130
131 pub fn get_redis_url() -> String {
133 env::var("REDIS_URL").expect("REDIS_URL must be set")
134 }
135
136 pub fn get_redis_url_optional() -> Option<String> {
138 env::var("REDIS_URL").ok()
139 }
140
141 pub fn get_config_file_path() -> String {
143 let conf_dir = if env::var("IN_DOCKER")
144 .map(|val| val == "true")
145 .unwrap_or(false)
146 {
147 "config/".to_string()
148 } else {
149 env::var("CONFIG_DIR").unwrap_or_else(|_| "./config".to_string())
150 };
151
152 let conf_dir = format!("{}/", conf_dir.trim_end_matches('/'));
153 let config_file_name =
154 env::var("CONFIG_FILE_NAME").unwrap_or_else(|_| "config.json".to_string());
155
156 format!("{conf_dir}{config_file_name}")
157 }
158
159 pub fn get_api_key() -> SecretString {
161 let api_key = SecretString::new(&env::var("API_KEY").expect("API_KEY must be set"));
162
163 if !api_key.has_minimum_length(MINIMUM_SECRET_VALUE_LENGTH) {
164 panic!(
165 "Security error: API_KEY must be at least {MINIMUM_SECRET_VALUE_LENGTH} characters long"
166 );
167 }
168
169 api_key
170 }
171
172 pub fn get_api_key_optional() -> Option<SecretString> {
174 env::var("API_KEY")
175 .ok()
176 .map(|key| SecretString::new(&key))
177 .filter(|key| key.has_minimum_length(MINIMUM_SECRET_VALUE_LENGTH))
178 }
179
180 pub fn get_rate_limit_requests_per_second() -> u64 {
182 env::var("RATE_LIMIT_REQUESTS_PER_SECOND")
183 .unwrap_or_else(|_| "100".to_string())
184 .parse()
185 .unwrap_or(100)
186 }
187
188 pub fn get_rate_limit_burst_size() -> u32 {
190 env::var("RATE_LIMIT_BURST_SIZE")
191 .unwrap_or_else(|_| "300".to_string())
192 .parse()
193 .unwrap_or(300)
194 }
195
196 pub fn get_metrics_port() -> u16 {
198 env::var("METRICS_PORT")
199 .unwrap_or_else(|_| "8081".to_string())
200 .parse()
201 .unwrap_or(8081)
202 }
203
204 pub fn get_enable_swagger() -> bool {
206 env::var("ENABLE_SWAGGER")
207 .map(|v| v.to_lowercase() == "true")
208 .unwrap_or(false)
209 }
210
211 pub fn get_redis_connection_timeout_ms() -> u64 {
213 env::var("REDIS_CONNECTION_TIMEOUT_MS")
214 .unwrap_or_else(|_| "10000".to_string())
215 .parse()
216 .unwrap_or(10000)
217 }
218
219 pub fn get_redis_key_prefix() -> String {
221 env::var("REDIS_KEY_PREFIX").unwrap_or_else(|_| "oz-relayer".to_string())
222 }
223
224 pub fn get_rpc_timeout_ms() -> u64 {
226 env::var("RPC_TIMEOUT_MS")
227 .unwrap_or_else(|_| "10000".to_string())
228 .parse()
229 .unwrap_or(10000)
230 }
231
232 pub fn get_provider_max_retries() -> u8 {
234 env::var("PROVIDER_MAX_RETRIES")
235 .unwrap_or_else(|_| "3".to_string())
236 .parse()
237 .unwrap_or(3)
238 }
239
240 pub fn get_provider_retry_base_delay_ms() -> u64 {
242 env::var("PROVIDER_RETRY_BASE_DELAY_MS")
243 .unwrap_or_else(|_| "100".to_string())
244 .parse()
245 .unwrap_or(100)
246 }
247
248 pub fn get_provider_retry_max_delay_ms() -> u64 {
250 env::var("PROVIDER_RETRY_MAX_DELAY_MS")
251 .unwrap_or_else(|_| "2000".to_string())
252 .parse()
253 .unwrap_or(2000)
254 }
255
256 pub fn get_provider_max_failovers() -> u8 {
258 env::var("PROVIDER_MAX_FAILOVERS")
259 .unwrap_or_else(|_| "3".to_string())
260 .parse()
261 .unwrap_or(3)
262 }
263
264 pub fn get_repository_storage_type() -> RepositoryStorageType {
266 env::var("REPOSITORY_STORAGE_TYPE")
267 .unwrap_or_else(|_| "in_memory".to_string())
268 .parse()
269 .unwrap_or(RepositoryStorageType::InMemory)
270 }
271
272 pub fn get_reset_storage_on_start() -> bool {
274 env::var("RESET_STORAGE_ON_START")
275 .map(|v| v.to_lowercase() == "true")
276 .unwrap_or(false)
277 }
278
279 pub fn get_storage_encryption_key() -> Option<SecretString> {
281 env::var("STORAGE_ENCRYPTION_KEY")
282 .map(|v| SecretString::new(&v))
283 .ok()
284 }
285
286 pub fn get_transaction_expiration_hours() -> u64 {
288 env::var("TRANSACTION_EXPIRATION_HOURS")
289 .unwrap_or_else(|_| "4".to_string())
290 .parse()
291 .unwrap_or(4)
292 }
293
294 pub fn get_worker_concurrency(worker_name: &str, default: usize) -> usize {
299 let env_var = format!(
300 "BACKGROUND_WORKER_{}_CONCURRENCY",
301 worker_name.to_uppercase()
302 );
303 env::var(&env_var)
304 .ok()
305 .and_then(|v| v.parse().ok())
306 .unwrap_or(default)
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use lazy_static::lazy_static;
314 use std::env;
315 use std::sync::Mutex;
316
317 lazy_static! {
319 static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
320 }
321
322 fn setup() {
323 env::remove_var("HOST");
325 env::remove_var("APP_PORT");
326 env::remove_var("REDIS_URL");
327 env::remove_var("CONFIG_DIR");
328 env::remove_var("CONFIG_FILE_NAME");
329 env::remove_var("CONFIG_FILE_PATH");
330 env::remove_var("API_KEY");
331 env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
332 env::remove_var("RATE_LIMIT_BURST_SIZE");
333 env::remove_var("METRICS_PORT");
334 env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
335 env::remove_var("RPC_TIMEOUT_MS");
336 env::remove_var("PROVIDER_MAX_RETRIES");
337 env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
338 env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
339 env::remove_var("PROVIDER_MAX_FAILOVERS");
340 env::remove_var("REPOSITORY_STORAGE_TYPE");
341 env::remove_var("RESET_STORAGE_ON_START");
342 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
343 env::set_var("REDIS_URL", "redis://localhost:6379");
345 env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
346 env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
347 }
348
349 #[test]
350 fn test_default_values() {
351 let _lock = match ENV_MUTEX.lock() {
352 Ok(guard) => guard,
353 Err(poisoned) => poisoned.into_inner(),
354 };
355 setup();
356
357 let config = ServerConfig::from_env();
358
359 assert_eq!(config.host, "0.0.0.0");
360 assert_eq!(config.port, 8080);
361 assert_eq!(config.redis_url, "redis://localhost:6379");
362 assert_eq!(config.config_file_path, "./config/config.json");
363 assert_eq!(
364 config.api_key,
365 SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
366 );
367 assert_eq!(config.rate_limit_requests_per_second, 100);
368 assert_eq!(config.rate_limit_burst_size, 300);
369 assert_eq!(config.metrics_port, 8081);
370 assert_eq!(config.redis_connection_timeout_ms, 5000);
371 assert_eq!(config.rpc_timeout_ms, 10000);
372 assert_eq!(config.provider_max_retries, 3);
373 assert_eq!(config.provider_retry_base_delay_ms, 100);
374 assert_eq!(config.provider_retry_max_delay_ms, 2000);
375 assert_eq!(config.provider_max_failovers, 3);
376 assert_eq!(
377 config.repository_storage_type,
378 RepositoryStorageType::InMemory
379 );
380 assert!(!config.reset_storage_on_start);
381 assert_eq!(config.transaction_expiration_hours, 4);
382 }
383
384 #[test]
385 fn test_invalid_port_values() {
386 let _lock = match ENV_MUTEX.lock() {
387 Ok(guard) => guard,
388 Err(poisoned) => poisoned.into_inner(),
389 };
390 setup();
391 env::set_var("REDIS_URL", "redis://localhost:6379");
392 env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
393 env::set_var("APP_PORT", "not_a_number");
394 env::set_var("METRICS_PORT", "also_not_a_number");
395 env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "invalid");
396 env::set_var("RATE_LIMIT_BURST_SIZE", "invalid");
397 env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "invalid");
398 env::set_var("RPC_TIMEOUT_MS", "invalid");
399 env::set_var("PROVIDER_MAX_RETRIES", "invalid");
400 env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "invalid");
401 env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "invalid");
402 env::set_var("PROVIDER_MAX_FAILOVERS", "invalid");
403 env::set_var("REPOSITORY_STORAGE_TYPE", "invalid");
404 env::set_var("RESET_STORAGE_ON_START", "invalid");
405 env::set_var("TRANSACTION_EXPIRATION_HOURS", "invalid");
406 let config = ServerConfig::from_env();
407
408 assert_eq!(config.port, 8080);
410 assert_eq!(config.metrics_port, 8081);
411 assert_eq!(config.rate_limit_requests_per_second, 100);
412 assert_eq!(config.rate_limit_burst_size, 300);
413 assert_eq!(config.redis_connection_timeout_ms, 10000);
414 assert_eq!(config.rpc_timeout_ms, 10000);
415 assert_eq!(config.provider_max_retries, 3);
416 assert_eq!(config.provider_retry_base_delay_ms, 100);
417 assert_eq!(config.provider_retry_max_delay_ms, 2000);
418 assert_eq!(config.provider_max_failovers, 3);
419 assert_eq!(
420 config.repository_storage_type,
421 RepositoryStorageType::InMemory
422 );
423 assert!(!config.reset_storage_on_start);
424 assert_eq!(config.transaction_expiration_hours, 4);
425 }
426
427 #[test]
428 fn test_custom_values() {
429 let _lock = match ENV_MUTEX.lock() {
430 Ok(guard) => guard,
431 Err(poisoned) => poisoned.into_inner(),
432 };
433 setup();
434
435 env::set_var("HOST", "127.0.0.1");
436 env::set_var("APP_PORT", "9090");
437 env::set_var("REDIS_URL", "redis://custom:6379");
438 env::set_var("CONFIG_DIR", "custom");
439 env::set_var("CONFIG_FILE_NAME", "path.json");
440 env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
441 env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "200");
442 env::set_var("RATE_LIMIT_BURST_SIZE", "500");
443 env::set_var("METRICS_PORT", "9091");
444 env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "10000");
445 env::set_var("RPC_TIMEOUT_MS", "33333");
446 env::set_var("PROVIDER_MAX_RETRIES", "5");
447 env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
448 env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "3000");
449 env::set_var("PROVIDER_MAX_FAILOVERS", "4");
450 env::set_var("REPOSITORY_STORAGE_TYPE", "in_memory");
451 env::set_var("RESET_STORAGE_ON_START", "true");
452 env::set_var("TRANSACTION_EXPIRATION_HOURS", "6");
453 let config = ServerConfig::from_env();
454
455 assert_eq!(config.host, "127.0.0.1");
456 assert_eq!(config.port, 9090);
457 assert_eq!(config.redis_url, "redis://custom:6379");
458 assert_eq!(config.config_file_path, "custom/path.json");
459 assert_eq!(
460 config.api_key,
461 SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
462 );
463 assert_eq!(config.rate_limit_requests_per_second, 200);
464 assert_eq!(config.rate_limit_burst_size, 500);
465 assert_eq!(config.metrics_port, 9091);
466 assert_eq!(config.redis_connection_timeout_ms, 10000);
467 assert_eq!(config.rpc_timeout_ms, 33333);
468 assert_eq!(config.provider_max_retries, 5);
469 assert_eq!(config.provider_retry_base_delay_ms, 200);
470 assert_eq!(config.provider_retry_max_delay_ms, 3000);
471 assert_eq!(config.provider_max_failovers, 4);
472 assert_eq!(
473 config.repository_storage_type,
474 RepositoryStorageType::InMemory
475 );
476 assert!(config.reset_storage_on_start);
477 assert_eq!(config.transaction_expiration_hours, 6);
478 }
479
480 #[test]
481 #[should_panic(expected = "Security error: API_KEY must be at least 32 characters long")]
482 fn test_invalid_api_key_length() {
483 let _lock = match ENV_MUTEX.lock() {
484 Ok(guard) => guard,
485 Err(poisoned) => poisoned.into_inner(),
486 };
487 setup();
488 env::set_var("REDIS_URL", "redis://localhost:6379");
489 env::set_var("API_KEY", "insufficient_length");
490 env::set_var("APP_PORT", "8080");
491 env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "100");
492 env::set_var("RATE_LIMIT_BURST_SIZE", "300");
493 env::set_var("METRICS_PORT", "9091");
494 env::set_var("TRANSACTION_EXPIRATION_HOURS", "4");
495
496 let _ = ServerConfig::from_env();
497
498 panic!("Test should have panicked before reaching here");
499 }
500
501 #[test]
503 fn test_individual_getters_with_defaults() {
504 let _lock = match ENV_MUTEX.lock() {
505 Ok(guard) => guard,
506 Err(poisoned) => poisoned.into_inner(),
507 };
508
509 env::remove_var("HOST");
511 env::remove_var("APP_PORT");
512 env::remove_var("REDIS_URL");
513 env::remove_var("CONFIG_DIR");
514 env::remove_var("CONFIG_FILE_NAME");
515 env::remove_var("API_KEY");
516 env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
517 env::remove_var("RATE_LIMIT_BURST_SIZE");
518 env::remove_var("METRICS_PORT");
519 env::remove_var("ENABLE_SWAGGER");
520 env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
521 env::remove_var("REDIS_KEY_PREFIX");
522 env::remove_var("RPC_TIMEOUT_MS");
523 env::remove_var("PROVIDER_MAX_RETRIES");
524 env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
525 env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
526 env::remove_var("PROVIDER_MAX_FAILOVERS");
527 env::remove_var("REPOSITORY_STORAGE_TYPE");
528 env::remove_var("RESET_STORAGE_ON_START");
529 env::remove_var("STORAGE_ENCRYPTION_KEY");
530 env::remove_var("TRANSACTION_EXPIRATION_HOURS");
531
532 assert_eq!(ServerConfig::get_host(), "0.0.0.0");
534 assert_eq!(ServerConfig::get_port(), 8080);
535 assert_eq!(ServerConfig::get_redis_url_optional(), None);
536 assert_eq!(ServerConfig::get_config_file_path(), "./config/config.json");
537 assert_eq!(ServerConfig::get_api_key_optional(), None);
538 assert_eq!(ServerConfig::get_rate_limit_requests_per_second(), 100);
539 assert_eq!(ServerConfig::get_rate_limit_burst_size(), 300);
540 assert_eq!(ServerConfig::get_metrics_port(), 8081);
541 assert!(!ServerConfig::get_enable_swagger());
542 assert_eq!(ServerConfig::get_redis_connection_timeout_ms(), 10000);
543 assert_eq!(ServerConfig::get_redis_key_prefix(), "oz-relayer");
544 assert_eq!(ServerConfig::get_rpc_timeout_ms(), 10000);
545 assert_eq!(ServerConfig::get_provider_max_retries(), 3);
546 assert_eq!(ServerConfig::get_provider_retry_base_delay_ms(), 100);
547 assert_eq!(ServerConfig::get_provider_retry_max_delay_ms(), 2000);
548 assert_eq!(ServerConfig::get_provider_max_failovers(), 3);
549 assert_eq!(
550 ServerConfig::get_repository_storage_type(),
551 RepositoryStorageType::InMemory
552 );
553 assert!(!ServerConfig::get_reset_storage_on_start());
554 assert!(ServerConfig::get_storage_encryption_key().is_none());
555 assert_eq!(ServerConfig::get_transaction_expiration_hours(), 4);
556 }
557
558 #[test]
559 fn test_individual_getters_with_custom_values() {
560 let _lock = match ENV_MUTEX.lock() {
561 Ok(guard) => guard,
562 Err(poisoned) => poisoned.into_inner(),
563 };
564
565 env::set_var("HOST", "192.168.1.1");
567 env::set_var("APP_PORT", "9999");
568 env::set_var("REDIS_URL", "redis://custom:6379");
569 env::set_var("CONFIG_DIR", "/custom/config");
570 env::set_var("CONFIG_FILE_NAME", "custom.json");
571 env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
572 env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "500");
573 env::set_var("RATE_LIMIT_BURST_SIZE", "1000");
574 env::set_var("METRICS_PORT", "9999");
575 env::set_var("ENABLE_SWAGGER", "true");
576 env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
577 env::set_var("REDIS_KEY_PREFIX", "custom-prefix");
578 env::set_var("RPC_TIMEOUT_MS", "15000");
579 env::set_var("PROVIDER_MAX_RETRIES", "5");
580 env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
581 env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "5000");
582 env::set_var("PROVIDER_MAX_FAILOVERS", "10");
583 env::set_var("REPOSITORY_STORAGE_TYPE", "redis");
584 env::set_var("RESET_STORAGE_ON_START", "true");
585 env::set_var("STORAGE_ENCRYPTION_KEY", "my-encryption-key");
586 env::set_var("TRANSACTION_EXPIRATION_HOURS", "12");
587
588 assert_eq!(ServerConfig::get_host(), "192.168.1.1");
590 assert_eq!(ServerConfig::get_port(), 9999);
591 assert_eq!(
592 ServerConfig::get_redis_url_optional(),
593 Some("redis://custom:6379".to_string())
594 );
595 assert_eq!(
596 ServerConfig::get_config_file_path(),
597 "/custom/config/custom.json"
598 );
599 assert!(ServerConfig::get_api_key_optional().is_some());
600 assert_eq!(ServerConfig::get_rate_limit_requests_per_second(), 500);
601 assert_eq!(ServerConfig::get_rate_limit_burst_size(), 1000);
602 assert_eq!(ServerConfig::get_metrics_port(), 9999);
603 assert!(ServerConfig::get_enable_swagger());
604 assert_eq!(ServerConfig::get_redis_connection_timeout_ms(), 5000);
605 assert_eq!(ServerConfig::get_redis_key_prefix(), "custom-prefix");
606 assert_eq!(ServerConfig::get_rpc_timeout_ms(), 15000);
607 assert_eq!(ServerConfig::get_provider_max_retries(), 5);
608 assert_eq!(ServerConfig::get_provider_retry_base_delay_ms(), 200);
609 assert_eq!(ServerConfig::get_provider_retry_max_delay_ms(), 5000);
610 assert_eq!(ServerConfig::get_provider_max_failovers(), 10);
611 assert_eq!(
612 ServerConfig::get_repository_storage_type(),
613 RepositoryStorageType::Redis
614 );
615 assert!(ServerConfig::get_reset_storage_on_start());
616 assert!(ServerConfig::get_storage_encryption_key().is_some());
617 assert_eq!(ServerConfig::get_transaction_expiration_hours(), 12);
618 }
619
620 #[test]
621 #[should_panic(expected = "REDIS_URL must be set")]
622 fn test_get_redis_url_panics_when_not_set() {
623 let _lock = match ENV_MUTEX.lock() {
624 Ok(guard) => guard,
625 Err(poisoned) => poisoned.into_inner(),
626 };
627
628 env::remove_var("REDIS_URL");
629 let _ = ServerConfig::get_redis_url();
630 }
631
632 #[test]
633 #[should_panic(expected = "API_KEY must be set")]
634 fn test_get_api_key_panics_when_not_set() {
635 let _lock = match ENV_MUTEX.lock() {
636 Ok(guard) => guard,
637 Err(poisoned) => poisoned.into_inner(),
638 };
639
640 env::remove_var("API_KEY");
641 let _ = ServerConfig::get_api_key();
642 }
643
644 #[test]
645 fn test_optional_getters_return_none_safely() {
646 let _lock = match ENV_MUTEX.lock() {
647 Ok(guard) => guard,
648 Err(poisoned) => poisoned.into_inner(),
649 };
650
651 env::remove_var("REDIS_URL");
652 env::remove_var("API_KEY");
653 env::remove_var("STORAGE_ENCRYPTION_KEY");
654
655 assert!(ServerConfig::get_redis_url_optional().is_none());
656 assert!(ServerConfig::get_api_key_optional().is_none());
657 assert!(ServerConfig::get_storage_encryption_key().is_none());
658 }
659
660 #[test]
661 fn test_refactored_from_env_equivalence() {
662 let _lock = match ENV_MUTEX.lock() {
663 Ok(guard) => guard,
664 Err(poisoned) => poisoned.into_inner(),
665 };
666 setup();
667
668 env::set_var("HOST", "custom-host");
670 env::set_var("APP_PORT", "7777");
671 env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "250");
672 env::set_var("METRICS_PORT", "7778");
673 env::set_var("ENABLE_SWAGGER", "true");
674 env::set_var("PROVIDER_MAX_RETRIES", "7");
675 env::set_var("TRANSACTION_EXPIRATION_HOURS", "8");
676
677 let config = ServerConfig::from_env();
678
679 assert_eq!(config.host, ServerConfig::get_host());
681 assert_eq!(config.port, ServerConfig::get_port());
682 assert_eq!(config.redis_url, ServerConfig::get_redis_url());
683 assert_eq!(
684 config.config_file_path,
685 ServerConfig::get_config_file_path()
686 );
687 assert_eq!(config.api_key, ServerConfig::get_api_key());
688 assert_eq!(
689 config.rate_limit_requests_per_second,
690 ServerConfig::get_rate_limit_requests_per_second()
691 );
692 assert_eq!(
693 config.rate_limit_burst_size,
694 ServerConfig::get_rate_limit_burst_size()
695 );
696 assert_eq!(config.metrics_port, ServerConfig::get_metrics_port());
697 assert_eq!(config.enable_swagger, ServerConfig::get_enable_swagger());
698 assert_eq!(
699 config.redis_connection_timeout_ms,
700 ServerConfig::get_redis_connection_timeout_ms()
701 );
702 assert_eq!(
703 config.redis_key_prefix,
704 ServerConfig::get_redis_key_prefix()
705 );
706 assert_eq!(config.rpc_timeout_ms, ServerConfig::get_rpc_timeout_ms());
707 assert_eq!(
708 config.provider_max_retries,
709 ServerConfig::get_provider_max_retries()
710 );
711 assert_eq!(
712 config.provider_retry_base_delay_ms,
713 ServerConfig::get_provider_retry_base_delay_ms()
714 );
715 assert_eq!(
716 config.provider_retry_max_delay_ms,
717 ServerConfig::get_provider_retry_max_delay_ms()
718 );
719 assert_eq!(
720 config.provider_max_failovers,
721 ServerConfig::get_provider_max_failovers()
722 );
723 assert_eq!(
724 config.repository_storage_type,
725 ServerConfig::get_repository_storage_type()
726 );
727 assert_eq!(
728 config.reset_storage_on_start,
729 ServerConfig::get_reset_storage_on_start()
730 );
731 assert_eq!(
732 config.storage_encryption_key,
733 ServerConfig::get_storage_encryption_key()
734 );
735 assert_eq!(
736 config.transaction_expiration_hours,
737 ServerConfig::get_transaction_expiration_hours()
738 );
739 }
740
741 mod get_worker_concurrency_tests {
742 use super::*;
743 use serial_test::serial;
744
745 #[test]
746 #[serial]
747 fn test_returns_default_when_env_not_set() {
748 let worker_name = "test_worker";
749 let env_var = format!(
750 "BACKGROUND_WORKER_{}_CONCURRENCY",
751 worker_name.to_uppercase()
752 );
753
754 env::remove_var(&env_var);
756
757 let default_value = 42;
758 let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
759
760 assert_eq!(
761 result, default_value,
762 "Should return default value when env var is not set"
763 );
764 }
765
766 #[test]
767 #[serial]
768 fn test_returns_env_value_when_set() {
769 let worker_name = "status_checker";
770 let env_var = format!(
771 "BACKGROUND_WORKER_{}_CONCURRENCY",
772 worker_name.to_uppercase()
773 );
774
775 env::set_var(&env_var, "100");
777
778 let default_value = 10;
779 let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
780
781 assert_eq!(result, 100, "Should return env var value when set");
782
783 env::remove_var(&env_var);
785 }
786
787 #[test]
788 #[serial]
789 fn test_returns_default_when_env_invalid() {
790 let worker_name = "invalid_worker";
791 let env_var = format!(
792 "BACKGROUND_WORKER_{}_CONCURRENCY",
793 worker_name.to_uppercase()
794 );
795
796 env::set_var(&env_var, "not_a_number");
798
799 let default_value = 25;
800 let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
801
802 assert_eq!(
803 result, default_value,
804 "Should return default value when env var is invalid"
805 );
806
807 env::remove_var(&env_var);
809 }
810
811 #[test]
812 #[serial]
813 fn test_returns_default_when_env_empty() {
814 let worker_name = "empty_worker";
815 let env_var = format!(
816 "BACKGROUND_WORKER_{}_CONCURRENCY",
817 worker_name.to_uppercase()
818 );
819
820 env::set_var(&env_var, "");
822
823 let default_value = 15;
824 let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
825
826 assert_eq!(
827 result, default_value,
828 "Should return default value when env var is empty"
829 );
830
831 env::remove_var(&env_var);
833 }
834
835 #[test]
836 #[serial]
837 fn test_returns_default_when_env_negative() {
838 let worker_name = "negative_worker";
839 let env_var = format!(
840 "BACKGROUND_WORKER_{}_CONCURRENCY",
841 worker_name.to_uppercase()
842 );
843
844 env::set_var(&env_var, "-5");
846
847 let default_value = 20;
848 let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
849
850 assert_eq!(
851 result, default_value,
852 "Should return default value when env var is negative"
853 );
854
855 env::remove_var(&env_var);
857 }
858
859 #[test]
860 #[serial]
861 fn test_env_var_name_formatting() {
862 let worker_names = vec![
864 (
865 "transaction_sender",
866 "BACKGROUND_WORKER_TRANSACTION_SENDER_CONCURRENCY",
867 ),
868 (
869 "status_checker_evm",
870 "BACKGROUND_WORKER_STATUS_CHECKER_EVM_CONCURRENCY",
871 ),
872 (
873 "notification_sender",
874 "BACKGROUND_WORKER_NOTIFICATION_SENDER_CONCURRENCY",
875 ),
876 ];
877
878 for (worker_name, expected_env_var) in worker_names {
879 let actual_env_var = format!(
880 "BACKGROUND_WORKER_{}_CONCURRENCY",
881 worker_name.to_uppercase()
882 );
883 assert_eq!(
884 actual_env_var, expected_env_var,
885 "Env var name should be correctly formatted for worker: {}",
886 worker_name
887 );
888 }
889 }
890
891 #[test]
892 #[serial]
893 fn test_zero_value() {
894 let worker_name = "zero_worker";
895 let env_var = format!(
896 "BACKGROUND_WORKER_{}_CONCURRENCY",
897 worker_name.to_uppercase()
898 );
899
900 env::set_var(&env_var, "0");
902
903 let default_value = 30;
904 let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
905
906 assert_eq!(result, 0, "Should accept zero as a valid value");
907
908 env::remove_var(&env_var);
910 }
911
912 #[test]
913 #[serial]
914 fn test_large_value() {
915 let worker_name = "large_worker";
916 let env_var = format!(
917 "BACKGROUND_WORKER_{}_CONCURRENCY",
918 worker_name.to_uppercase()
919 );
920
921 env::set_var(&env_var, "10000");
923
924 let default_value = 50;
925 let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
926
927 assert_eq!(result, 10000, "Should accept large values");
928
929 env::remove_var(&env_var);
931 }
932
933 #[test]
934 #[serial]
935 fn test_whitespace_in_value() {
936 let worker_name = "whitespace_worker";
937 let env_var = format!(
938 "BACKGROUND_WORKER_{}_CONCURRENCY",
939 worker_name.to_uppercase()
940 );
941
942 env::set_var(&env_var, " 75 ");
944
945 let default_value = 35;
946 let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
947
948 assert_eq!(
951 result, default_value,
952 "Should return default value when value has whitespace"
953 );
954
955 env::remove_var(&env_var);
957 }
958
959 #[test]
960 #[serial]
961 fn test_float_value_returns_default() {
962 let worker_name = "float_worker";
963 let env_var = format!(
964 "BACKGROUND_WORKER_{}_CONCURRENCY",
965 worker_name.to_uppercase()
966 );
967
968 env::set_var(&env_var, "12.5");
970
971 let default_value = 40;
972 let result = ServerConfig::get_worker_concurrency(worker_name, default_value);
973
974 assert_eq!(
975 result, default_value,
976 "Should return default value for float input"
977 );
978
979 env::remove_var(&env_var);
981 }
982 }
983}