//! Comprehensive tests for runtime configuration (config/src/runtime.rs) //! //! Tests cover: //! - Environment detection and override //! - Configuration validation //! - Environment variable parsing //! - Environment-specific defaults //! - Cross-field validation //! - Rollback on invalid config use config::error::ConfigError; use config::runtime::{ CacheRuntimeConfig, DatabaseRuntimeConfig, Environment, LimitsConfig, RuntimeConfig, TimeoutConfig, }; use serial_test::serial; use std::env; use std::time::Duration; // ============================================================================ // Helper Functions // ============================================================================ /// Sets an environment variable for the duration of a test fn with_env_var(key: &str, value: &str, test: F) where F: FnOnce() -> (), { env::set_var(key, value); test(); env::remove_var(key); } /// Sets multiple environment variables for a test fn with_env_vars(vars: Vec<(&str, &str)>, test: F) where F: FnOnce() -> (), { for (key, value) in &vars { env::set_var(key, value); } test(); for (key, _) in &vars { env::remove_var(key); } } // ============================================================================ // 1. Environment Detection and Configuration Tests (8 tests) // ============================================================================ #[test] #[serial] fn test_environment_detect_production() { with_env_var("ENVIRONMENT", "production", || { let env = Environment::detect(); assert_eq!(env, Environment::Production); assert!(env.is_production()); assert!(!env.is_development()); }); } #[test] #[serial] fn test_environment_detect_prod_alias() { with_env_var("ENVIRONMENT", "prod", || { let env = Environment::detect(); assert_eq!(env, Environment::Production); assert!(env.is_production()); }); } #[test] #[serial] fn test_environment_detect_staging() { with_env_var("ENVIRONMENT", "staging", || { let env = Environment::detect(); assert_eq!(env, Environment::Staging); assert!(!env.is_production()); assert!(!env.is_development()); }); } #[test] #[serial] fn test_environment_detect_stage_alias() { with_env_var("ENVIRONMENT", "stage", || { let env = Environment::detect(); assert_eq!(env, Environment::Staging); }); } #[test] #[serial] fn test_environment_detect_development_default() { env::remove_var("ENVIRONMENT"); let env = Environment::detect(); assert_eq!(env, Environment::Development); assert!(env.is_development()); assert!(!env.is_production()); } #[test] #[serial] fn test_environment_detect_unknown_fallback() { with_env_var("ENVIRONMENT", "invalid", || { let env = Environment::detect(); assert_eq!(env, Environment::Development); assert!(env.is_development()); }); } #[test] #[serial] fn test_environment_case_insensitive() { with_env_var("ENVIRONMENT", "PRODUCTION", || { let env = Environment::detect(); assert_eq!(env, Environment::Production); }); with_env_var("ENVIRONMENT", "StAgInG", || { let env = Environment::detect(); assert_eq!(env, Environment::Staging); }); } #[test] #[serial] fn test_runtime_config_from_env_respects_environment() { with_env_var("ENVIRONMENT", "production", || { let config = RuntimeConfig::from_env().unwrap(); assert_eq!(config.environment, Environment::Production); // Production should have tight timeouts assert!(config.database.query_timeout.as_millis() <= 1000); assert!(config.cache.position_ttl.as_secs() <= 60); }); } // ============================================================================ // 2. Configuration Validation Tests (7 tests) // ============================================================================ #[test] fn test_database_config_validation_zero_query_timeout() { let mut config = DatabaseRuntimeConfig::with_defaults(Environment::Production); config.query_timeout = Duration::from_millis(0); let result = config.validate(); assert!(result.is_err()); match result { Err(ConfigError::Invalid(msg)) => { assert!(msg.contains("Query timeout")); assert!(msg.contains("positive")); } _ => panic!("Expected Invalid error for zero query timeout"), } } #[test] fn test_database_config_validation_zero_pool_size() { let mut config = DatabaseRuntimeConfig::with_defaults(Environment::Production); config.pool_size = 0; let result = config.validate(); assert!(result.is_err()); match result { Err(ConfigError::Invalid(msg)) => { assert!(msg.contains("Pool size")); assert!(msg.contains("positive")); } _ => panic!("Expected Invalid error for zero pool size"), } } #[test] fn test_database_config_validation_pool_size_exceeds_max() { let mut config = DatabaseRuntimeConfig::with_defaults(Environment::Production); config.pool_size = 200; config.max_pool_size = 100; let result = config.validate(); assert!(result.is_err()); match result { Err(ConfigError::Invalid(msg)) => { assert!(msg.contains("Pool size")); assert!(msg.contains("max pool size")); } _ => panic!("Expected Invalid error for pool size > max"), } } #[test] fn test_cache_config_validation_zero_ttl() { let mut config = CacheRuntimeConfig::with_defaults(Environment::Production); config.position_ttl = Duration::from_secs(0); let result = config.validate(); assert!(result.is_err()); match result { Err(ConfigError::Invalid(msg)) => { assert!(msg.contains("TTL")); assert!(msg.contains("positive")); } _ => panic!("Expected Invalid error for zero TTL"), } } #[test] fn test_limits_config_validation_zero_retry_attempts() { let mut config = LimitsConfig::with_defaults(Environment::Production); config.retry_max_attempts = 0; let result = config.validate(); assert!(result.is_err()); match result { Err(ConfigError::Invalid(msg)) => { assert!(msg.contains("Retry")); assert!(msg.contains("positive")); } _ => panic!("Expected Invalid error for zero retry attempts"), } } #[test] fn test_limits_config_validation_invalid_backoff_multiplier() { let mut config = LimitsConfig::with_defaults(Environment::Production); config.retry_backoff_multiplier = 0.5; let result = config.validate(); assert!(result.is_err()); match result { Err(ConfigError::Invalid(msg)) => { assert!(msg.contains("Backoff multiplier")); assert!(msg.contains("> 1.0")); } _ => panic!("Expected Invalid error for backoff multiplier <= 1.0"), } } #[test] fn test_limits_config_validation_var_confidence_out_of_range() { let mut config = LimitsConfig::with_defaults(Environment::Production); // Test confidence > 1.0 config.risk_var_confidence = 1.5; let result = config.validate(); assert!(result.is_err()); match result { Err(ConfigError::Invalid(msg)) => { assert!(msg.contains("VaR confidence")); assert!(msg.contains("0.0") && msg.contains("1.0")); } _ => panic!("Expected Invalid error for VaR confidence > 1.0"), } // Test confidence < 0.0 config.risk_var_confidence = -0.5; let result = config.validate(); assert!(result.is_err()); } // ============================================================================ // 3. Environment Variable Parsing Tests (5 tests) // ============================================================================ #[test] #[serial] fn test_parse_database_config_from_env_vars() { let vars = vec![ ("DATABASE_QUERY_TIMEOUT_MS", "2500"), ("DATABASE_CONNECTION_TIMEOUT_MS", "300"), ("DATABASE_POOL_SIZE", "25"), ("DATABASE_MAX_POOL_SIZE", "150"), ]; with_env_vars(vars, || { let config = DatabaseRuntimeConfig::from_env(Environment::Development).unwrap(); assert_eq!(config.query_timeout.as_millis(), 2500); assert_eq!(config.connection_timeout.as_millis(), 300); assert_eq!(config.pool_size, 25); assert_eq!(config.max_pool_size, 150); }); } #[test] #[serial] fn test_parse_cache_config_from_env_vars() { let vars = vec![ ("CACHE_POSITION_TTL_SECS", "45"), ("CACHE_VAR_TTL_SECS", "1800"), ("CACHE_MARKET_DATA_TTL_SECS", "180"), ]; with_env_vars(vars, || { let config = CacheRuntimeConfig::from_env(Environment::Development).unwrap(); assert_eq!(config.position_ttl.as_secs(), 45); assert_eq!(config.var_ttl.as_secs(), 1800); assert_eq!(config.market_data_ttl.as_secs(), 180); }); } #[test] #[serial] fn test_parse_timeout_config_from_env_vars() { let vars = vec![ ("NETWORK_GRPC_CONNECT_TIMEOUT_SECS", "8"), ("NETWORK_GRPC_REQUEST_TIMEOUT_SECS", "15"), ("NETWORK_MAX_CONCURRENT_CONNECTIONS", "200"), ]; with_env_vars(vars, || { let config = TimeoutConfig::from_env(Environment::Development).unwrap(); assert_eq!(config.grpc_connect_timeout.as_secs(), 8); assert_eq!(config.grpc_request_timeout.as_secs(), 15); assert_eq!(config.max_concurrent_connections, 200); }); } #[test] #[serial] fn test_parse_limits_config_from_env_vars() { let vars = vec![ ("RETRY_INITIAL_DELAY_MS", "150"), ("RETRY_MAX_ATTEMPTS", "5"), ("RETRY_BACKOFF_MULTIPLIER", "2.5"), ("ML_MAX_BATCH_SIZE", "2048"), ("RISK_VAR_CONFIDENCE", "0.99"), ]; with_env_vars(vars, || { let config = LimitsConfig::from_env(Environment::Development).unwrap(); assert_eq!(config.retry_initial_delay.as_millis(), 150); assert_eq!(config.retry_max_attempts, 5); assert_eq!(config.retry_backoff_multiplier, 2.5); assert_eq!(config.ml_max_batch_size, 2048); assert_eq!(config.risk_var_confidence, 0.99); }); } #[test] #[serial] fn test_parse_invalid_env_var_returns_error() { with_env_var("DATABASE_POOL_SIZE", "invalid", || { let result = DatabaseRuntimeConfig::from_env(Environment::Development); assert!(result.is_err()); match result { Err(ConfigError::Invalid(msg)) => { assert!(msg.contains("Invalid u32")); assert!(msg.contains("DATABASE_POOL_SIZE")); } _ => panic!("Expected Invalid error for invalid env var"), } }); } // ============================================================================ // 4. Environment-Specific Defaults Tests (5 tests) // ============================================================================ #[test] fn test_development_has_relaxed_timeouts() { let dev_config = RuntimeConfig::with_defaults(Environment::Development); let prod_config = RuntimeConfig::with_defaults(Environment::Production); // Development should have more relaxed timeouts for debugging assert!(dev_config.database.query_timeout > prod_config.database.query_timeout); assert!(dev_config.cache.position_ttl > prod_config.cache.position_ttl); assert!(dev_config.timeouts.grpc_request_timeout > prod_config.timeouts.grpc_request_timeout); assert!(dev_config.limits.safety_check_timeout > prod_config.limits.safety_check_timeout); } #[test] fn test_production_has_tight_timeouts_for_hft() { let prod_config = RuntimeConfig::with_defaults(Environment::Production); // Production should have tight timeouts for HFT assert!(prod_config.database.query_timeout.as_millis() <= 1000); assert!(prod_config.cache.position_ttl.as_secs() <= 60); assert!(prod_config.timeouts.grpc_request_timeout.as_secs() <= 10); assert!(prod_config.limits.safety_check_timeout.as_millis() <= 5); assert!(prod_config.limits.ml_inference_timeout.as_millis() <= 100); } #[test] fn test_staging_is_between_dev_and_prod() { let dev_config = RuntimeConfig::with_defaults(Environment::Development); let staging_config = RuntimeConfig::with_defaults(Environment::Staging); let prod_config = RuntimeConfig::with_defaults(Environment::Production); // Staging should have settings between dev and prod assert!(staging_config.database.query_timeout > prod_config.database.query_timeout); assert!(staging_config.database.query_timeout < dev_config.database.query_timeout); assert!(staging_config.cache.position_ttl > prod_config.cache.position_ttl); assert!(staging_config.cache.position_ttl < dev_config.cache.position_ttl); assert!(staging_config.limits.retry_max_attempts >= prod_config.limits.retry_max_attempts); assert!(staging_config.limits.retry_max_attempts <= dev_config.limits.retry_max_attempts); } #[test] fn test_production_pool_sizes_are_largest() { let dev_config = DatabaseRuntimeConfig::with_defaults(Environment::Development); let staging_config = DatabaseRuntimeConfig::with_defaults(Environment::Staging); let prod_config = DatabaseRuntimeConfig::with_defaults(Environment::Production); // Production should have the largest pool sizes for throughput assert!(prod_config.pool_size >= staging_config.pool_size); assert!(staging_config.pool_size >= dev_config.pool_size); assert!(prod_config.max_pool_size >= staging_config.max_pool_size); assert!(staging_config.max_pool_size >= dev_config.max_pool_size); } #[test] fn test_production_ml_batch_sizes_are_largest() { let dev_config = LimitsConfig::with_defaults(Environment::Development); let staging_config = LimitsConfig::with_defaults(Environment::Staging); let prod_config = LimitsConfig::with_defaults(Environment::Production); // Production should have largest batch sizes for throughput assert!(prod_config.ml_max_batch_size >= staging_config.ml_max_batch_size); assert!(staging_config.ml_max_batch_size >= dev_config.ml_max_batch_size); } // ============================================================================ // 5. Cross-Field Validation Tests (5 tests) // ============================================================================ #[test] fn test_database_pool_size_cross_validation() { let mut config = DatabaseRuntimeConfig::with_defaults(Environment::Production); // Valid: pool_size < max_pool_size config.pool_size = 50; config.max_pool_size = 100; assert!(config.validate().is_ok()); // Valid: pool_size == max_pool_size config.pool_size = 100; config.max_pool_size = 100; assert!(config.validate().is_ok()); // Invalid: pool_size > max_pool_size config.pool_size = 150; config.max_pool_size = 100; assert!(config.validate().is_err()); } #[test] fn test_limits_config_ml_batch_size_validation() { let mut config = LimitsConfig::with_defaults(Environment::Production); // Valid: positive batch size config.ml_max_batch_size = 1024; assert!(config.validate().is_ok()); // Invalid: zero batch size config.ml_max_batch_size = 0; assert!(config.validate().is_err()); } #[test] fn test_limits_config_var_lookback_days_validation() { let mut config = LimitsConfig::with_defaults(Environment::Production); // Valid: positive lookback config.risk_var_lookback_days = 252; assert!(config.validate().is_ok()); // Invalid: zero lookback config.risk_var_lookback_days = 0; let result = config.validate(); assert!(result.is_err()); match result { Err(ConfigError::Invalid(msg)) => { assert!(msg.contains("VaR lookback")); assert!(msg.contains("positive")); } _ => panic!("Expected Invalid error for zero lookback days"), } } #[test] fn test_runtime_config_validates_all_subconfigs() { let mut config = RuntimeConfig::with_defaults(Environment::Production); // Valid configuration assert!(config.validate().is_ok()); // Invalid database config should fail validation config.database.pool_size = 200; config.database.max_pool_size = 100; assert!(config.validate().is_err()); // Reset and test cache validation config = RuntimeConfig::with_defaults(Environment::Production); config.cache.position_ttl = Duration::from_secs(0); assert!(config.validate().is_err()); // Reset and test limits validation config = RuntimeConfig::with_defaults(Environment::Production); config.limits.retry_max_attempts = 0; assert!(config.validate().is_err()); } #[test] fn test_timeout_config_concurrent_connections_validation() { let mut config = TimeoutConfig::with_defaults(Environment::Production); // Valid: positive connections config.max_concurrent_connections = 100; assert!(config.validate().is_ok()); // Invalid: zero connections config.max_concurrent_connections = 0; let result = config.validate(); assert!(result.is_err()); match result { Err(ConfigError::Invalid(msg)) => { assert!(msg.contains("concurrent connections")); assert!(msg.contains("positive")); } _ => panic!("Expected Invalid error for zero connections"), } } // ============================================================================ // 6. Rollback on Invalid Config Tests (3 tests) // ============================================================================ #[test] #[serial] fn test_from_env_with_invalid_config_returns_error() { // Set invalid environment variables that will fail validation let vars = vec![ ("DATABASE_POOL_SIZE", "200"), ("DATABASE_MAX_POOL_SIZE", "100"), // pool_size > max_pool_size ]; with_env_vars(vars, || { let result = RuntimeConfig::from_env_with_environment(Environment::Production); assert!(result.is_err()); // Ensure no partial state is created - error should be returned match result { Err(ConfigError::Invalid(_)) => { /* Expected */ } _ => panic!("Expected Invalid error for pool_size > max_pool_size"), } }); } #[test] #[serial] fn test_from_env_falls_back_to_defaults_on_missing_vars() { // Remove all config env vars env::remove_var("DATABASE_QUERY_TIMEOUT_MS"); env::remove_var("CACHE_POSITION_TTL_SECS"); env::remove_var("RETRY_MAX_ATTEMPTS"); let config = RuntimeConfig::from_env_with_environment(Environment::Production).unwrap(); // Should use defaults let defaults = RuntimeConfig::with_defaults(Environment::Production); assert_eq!( config.database.query_timeout, defaults.database.query_timeout ); assert_eq!(config.cache.position_ttl, defaults.cache.position_ttl); assert_eq!( config.limits.retry_max_attempts, defaults.limits.retry_max_attempts ); } #[test] fn test_with_defaults_never_fails_validation() { // All default configurations should be valid let dev_config = RuntimeConfig::with_defaults(Environment::Development); assert!(dev_config.validate().is_ok()); let staging_config = RuntimeConfig::with_defaults(Environment::Staging); assert!(staging_config.validate().is_ok()); let prod_config = RuntimeConfig::with_defaults(Environment::Production); assert!(prod_config.validate().is_ok()); } // ============================================================================ // 7. Dynamic Parameter Updates Tests (3 tests) // ============================================================================ #[test] fn test_runtime_config_can_be_cloned_for_updates() { let original = RuntimeConfig::with_defaults(Environment::Production); let mut updated = original.clone(); // Modify cloned config updated.database.pool_size = 30; updated.cache.position_ttl = Duration::from_secs(45); // Original should be unchanged assert_ne!(original.database.pool_size, updated.database.pool_size); assert_ne!(original.cache.position_ttl, updated.cache.position_ttl); // Both should still be valid assert!(original.validate().is_ok()); assert!(updated.validate().is_ok()); } #[test] fn test_runtime_config_serialization_for_hot_reload() { let config = RuntimeConfig::with_defaults(Environment::Production); // Should be serializable for persistence let json = serde_json::to_string(&config).unwrap(); assert!(json.contains("Production")); assert!(json.len() > 100); // Non-trivial serialization // Should be deserializable for hot reload let deserialized: RuntimeConfig = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.environment, Environment::Production); assert!(deserialized.validate().is_ok()); } #[test] fn test_partial_config_update_preserves_other_fields() { let mut config = RuntimeConfig::with_defaults(Environment::Production); let original_cache_ttl = config.cache.position_ttl; let original_timeout = config.timeouts.grpc_request_timeout; // Update only database config config.database.pool_size = 35; config.database.max_pool_size = 120; // Other configs should be unchanged assert_eq!(config.cache.position_ttl, original_cache_ttl); assert_eq!(config.timeouts.grpc_request_timeout, original_timeout); // Updated config should still be valid assert!(config.validate().is_ok()); } // ============================================================================ // 8. Edge Cases and Error Handling Tests (3 tests) // ============================================================================ #[test] fn test_extreme_timeout_values() { let mut config = DatabaseRuntimeConfig::with_defaults(Environment::Production); // Very small timeout (still > 0) config.query_timeout = Duration::from_millis(1); assert!(config.validate().is_ok()); // Very large timeout config.query_timeout = Duration::from_secs(3600); // 1 hour assert!(config.validate().is_ok()); } #[test] fn test_var_confidence_edge_values() { let mut config = LimitsConfig::with_defaults(Environment::Production); // Minimum valid confidence config.risk_var_confidence = 0.0; assert!(config.validate().is_ok()); // Maximum valid confidence config.risk_var_confidence = 1.0; assert!(config.validate().is_ok()); // Just below minimum config.risk_var_confidence = -0.001; assert!(config.validate().is_err()); // Just above maximum config.risk_var_confidence = 1.001; assert!(config.validate().is_err()); } #[test] fn test_cache_ttl_extreme_values() { let mut config = CacheRuntimeConfig::with_defaults(Environment::Production); // Very short TTL (still > 0) config.position_ttl = Duration::from_secs(1); config.var_ttl = Duration::from_secs(1); assert!(config.validate().is_ok()); // Very long TTL config.position_ttl = Duration::from_secs(86400 * 7); // 1 week config.var_ttl = Duration::from_secs(86400 * 30); // 30 days assert!(config.validate().is_ok()); }