//! Comprehensive Circuit Breaker Tests //! Target: 95%+ coverage for circuit breaker functionality //! Focus: State transitions, Redis coordination, dynamic limits, error handling #![allow( unused_crate_dependencies, clippy::field_reassign_with_default, clippy::redundant_clone, clippy::str_to_string )] // Import circuit breaker types use chrono::Utc; use common::types::Price; use risk::circuit_breaker::{CircuitBreakerConfig, CircuitBreakerState}; #[cfg(test)] mod circuit_breaker_state_tests { use super::*; #[test] fn test_circuit_breaker_state_default() { let state = CircuitBreakerState::default(); assert!(!state.is_active); assert_eq!(state.portfolio_value, Price::ZERO); assert_eq!(state.daily_loss_limit, Price::ZERO); assert_eq!(state.current_daily_loss, Price::ZERO); assert!(state.activation_reason.is_none()); assert!(state.activated_at.is_none()); assert_eq!(state.account_id, "default"); assert_eq!(state.consecutive_violations, 0); } #[test] fn test_circuit_breaker_state_activation() { let mut state = CircuitBreakerState::default(); state.is_active = true; state.activation_reason = Some("Daily loss limit exceeded".to_string()); state.activated_at = Some(Utc::now()); state.consecutive_violations = 1; assert!(state.is_active); assert!(state.activation_reason.is_some()); assert!(state.activated_at.is_some()); assert_eq!(state.consecutive_violations, 1); } #[test] fn test_circuit_breaker_state_serialization() { let state = CircuitBreakerState { is_active: true, portfolio_value: Price::new(1_000_000.0).unwrap(), daily_loss_limit: Price::new(20_000.0).unwrap(), current_daily_loss: Price::new(15_000.0).unwrap(), activation_reason: Some("Approaching limit".to_string()), activated_at: Some(Utc::now()), last_updated: Utc::now(), account_id: "test_account".to_string(), consecutive_violations: 2, }; let serialized = serde_json::to_string(&state).expect("Serialization failed"); let deserialized: CircuitBreakerState = serde_json::from_str(&serialized).expect("Deserialization failed"); assert_eq!(state.is_active, deserialized.is_active); assert_eq!(state.account_id, deserialized.account_id); assert_eq!( state.consecutive_violations, deserialized.consecutive_violations ); } #[test] fn test_circuit_breaker_state_clone() { let state = CircuitBreakerState::default(); let cloned = state.clone(); assert_eq!(state.is_active, cloned.is_active); assert_eq!(state.account_id, cloned.account_id); assert_eq!(state.consecutive_violations, cloned.consecutive_violations); } } #[cfg(test)] mod circuit_breaker_config_tests { use super::*; #[test] fn test_circuit_breaker_config_default() { let config = CircuitBreakerConfig::default(); assert!(config.enabled); assert!(!config.auto_recovery_enabled); // Default is false for safety (manual recovery) assert!(config.max_consecutive_violations > 0); assert!(!config.redis_url.is_empty()); } #[test] fn test_circuit_breaker_config_custom() { let config = CircuitBreakerConfig { enabled: true, daily_loss_percentage: Price::new(3.0).unwrap(), position_limit_percentage: Price::new(10.0).unwrap(), max_consecutive_violations: 5, redis_url: "redis://test:6379".to_string(), redis_key_prefix: "foxhunt:test".to_string(), auto_recovery_enabled: false, portfolio_refresh_interval_secs: 60, cooldown_period_secs: 300, }; assert!(config.enabled); assert!(!config.auto_recovery_enabled); assert_eq!(config.max_consecutive_violations, 5); assert_eq!(config.redis_url, "redis://test:6379"); } } #[cfg(test)] mod dynamic_limit_calculation_tests { #[test] fn test_daily_loss_limit_calculation() { let portfolio_value = 1_000_000.0; let loss_percentage = 2.0; // 2% let expected_limit = portfolio_value * (loss_percentage / 100.0); assert_eq!(expected_limit, 20_000.0); } #[test] fn test_position_limit_calculation() { let portfolio_value = 500_000.0; let position_percentage = 5.0; // 5% let expected_limit = portfolio_value * (position_percentage / 100.0); assert_eq!(expected_limit, 25_000.0); } #[test] fn test_zero_portfolio_value() { let portfolio_value = 0.0; let loss_percentage = 2.0; let limit = portfolio_value * (loss_percentage / 100.0); assert_eq!(limit, 0.0); } #[test] fn test_large_portfolio_value() { let portfolio_value = 100_000_000.0; // $100M let loss_percentage = 2.0; let expected_limit = 2_000_000.0; // $2M let limit = portfolio_value * (loss_percentage / 100.0); assert_eq!(limit, expected_limit); } } #[cfg(test)] mod consecutive_violation_tests { use super::*; #[test] fn test_violation_counter_increment() { let mut state = CircuitBreakerState::default(); assert_eq!(state.consecutive_violations, 0); state.consecutive_violations += 1; assert_eq!(state.consecutive_violations, 1); state.consecutive_violations += 1; assert_eq!(state.consecutive_violations, 2); } #[test] fn test_violation_counter_reset() { let mut state = CircuitBreakerState::default(); state.consecutive_violations = 5; state.consecutive_violations = 0; assert_eq!(state.consecutive_violations, 0); } #[test] fn test_max_violations_threshold() { let config = CircuitBreakerConfig::default(); let max = config.max_consecutive_violations; let mut state = CircuitBreakerState::default(); state.consecutive_violations = max; assert!(state.consecutive_violations >= max); } #[test] fn test_violation_escalation() { let mut state = CircuitBreakerState::default(); let threshold = 3; for _i in 0..5 { state.consecutive_violations += 1; if state.consecutive_violations >= threshold { state.is_active = true; break; } } assert!(state.is_active); assert!(state.consecutive_violations >= threshold); } } #[cfg(test)] mod loss_tracking_tests { use super::*; #[test] fn test_daily_loss_accumulation() { let mut state = CircuitBreakerState::default(); state.portfolio_value = Price::new(1_000_000.0).unwrap(); state.daily_loss_limit = Price::new(20_000.0).unwrap(); // Simulate losses state.current_daily_loss = Price::new(5_000.0).unwrap(); assert!(state.current_daily_loss < state.daily_loss_limit); state.current_daily_loss = Price::new(15_000.0).unwrap(); assert!(state.current_daily_loss < state.daily_loss_limit); state.current_daily_loss = Price::new(25_000.0).unwrap(); assert!(state.current_daily_loss > state.daily_loss_limit); } #[test] fn test_loss_limit_breach_detection() { let state = CircuitBreakerState { is_active: false, portfolio_value: Price::new(1_000_000.0).unwrap(), daily_loss_limit: Price::new(20_000.0).unwrap(), current_daily_loss: Price::new(25_000.0).unwrap(), activation_reason: None, activated_at: None, last_updated: Utc::now(), account_id: "test".to_string(), consecutive_violations: 0, }; let is_breached = state.current_daily_loss > state.daily_loss_limit; assert!(is_breached); } #[test] fn test_loss_percentage_calculation() { let portfolio_value = 1_000_000.0_f64; let current_loss = 15_000.0_f64; let loss_percentage = (current_loss / portfolio_value) * 100.0; assert!((loss_percentage - 1.5).abs() < 0.001); } #[test] fn test_zero_loss_scenario() { let mut state = CircuitBreakerState::default(); state.portfolio_value = Price::new(1_000_000.0).unwrap(); state.current_daily_loss = Price::ZERO; assert_eq!(state.current_daily_loss, Price::ZERO); } } #[cfg(test)] mod state_transition_tests { use super::*; #[test] fn test_inactive_to_active_transition() { let mut state = CircuitBreakerState::default(); assert!(!state.is_active); state.is_active = true; state.activation_reason = Some("Limit exceeded".to_string()); state.activated_at = Some(Utc::now()); assert!(state.is_active); assert!(state.activation_reason.is_some()); assert!(state.activated_at.is_some()); } #[test] fn test_active_to_inactive_transition() { let mut state = CircuitBreakerState { is_active: true, activation_reason: Some("Test".to_string()), activated_at: Some(Utc::now()), ..Default::default() }; state.is_active = false; state.activation_reason = None; state.activated_at = None; assert!(!state.is_active); assert!(state.activation_reason.is_none()); assert!(state.activated_at.is_none()); } #[test] fn test_state_persistence_across_updates() { let mut state = CircuitBreakerState::default(); state.account_id = "persistent_account".to_string(); let account_before = state.account_id.clone(); state.last_updated = Utc::now(); let account_after = state.account_id.clone(); assert_eq!(account_before, account_after); } } #[cfg(test)] mod cooldown_period_tests { use super::*; #[test] fn test_cooldown_period_configuration() { let config = CircuitBreakerConfig::default(); assert!(config.cooldown_period_secs > 0); } #[test] fn test_cooldown_expiration() { let cooldown_duration = 300; // 5 minutes in seconds let activation_time = Utc::now(); let current_time = activation_time + chrono::Duration::seconds(cooldown_duration as i64 + 10); let elapsed = (current_time - activation_time).num_seconds(); assert!(elapsed > cooldown_duration as i64); } #[test] fn test_cooldown_not_expired() { let cooldown_duration = 300; let activation_time = Utc::now(); let current_time = activation_time + chrono::Duration::seconds(100); let elapsed = (current_time - activation_time).num_seconds(); assert!(elapsed < cooldown_duration as i64); } } #[cfg(test)] mod portfolio_refresh_tests { use super::*; #[test] fn test_refresh_interval_configuration() { let config = CircuitBreakerConfig::default(); assert!(config.portfolio_refresh_interval_secs > 0); } #[test] fn test_portfolio_value_update() { let mut state = CircuitBreakerState::default(); let old_value = state.portfolio_value; state.portfolio_value = Price::new(1_500_000.0).unwrap(); state.last_updated = Utc::now(); assert_ne!(state.portfolio_value, old_value); } #[test] fn test_dynamic_limit_recalculation() { let mut state = CircuitBreakerState::default(); state.portfolio_value = Price::new(1_000_000.0).unwrap(); let loss_percentage = 2.0; let new_limit = state.portfolio_value.to_f64() * (loss_percentage / 100.0); state.daily_loss_limit = Price::new(new_limit).unwrap(); assert_eq!(state.daily_loss_limit, Price::new(20_000.0).unwrap()); } } #[cfg(test)] mod error_condition_tests { use super::*; #[test] fn test_negative_portfolio_value_handling() { // Portfolio values should never be negative let result = Price::new(-1000.0); assert!(result.is_err() || result.unwrap() == Price::ZERO); } #[test] fn test_negative_loss_percentage() { // Negative loss percentages should be handled let portfolio_value = 1_000_000.0; let loss_percentage = -2.0; let limit = portfolio_value * (loss_percentage / 100.0); assert!(limit < 0.0); } #[test] fn test_extreme_loss_percentage() { let portfolio_value = 1_000_000.0; let loss_percentage = 100.0; // 100% loss let limit = portfolio_value * (loss_percentage / 100.0); assert_eq!(limit, portfolio_value); } #[test] fn test_zero_consecutive_violations_threshold() { let config = CircuitBreakerConfig { max_consecutive_violations: 0, ..Default::default() }; // Should handle zero threshold gracefully assert_eq!(config.max_consecutive_violations, 0); } } #[cfg(test)] mod auto_recovery_tests { use super::*; #[test] fn test_auto_recovery_enabled_configuration() { let config = CircuitBreakerConfig { auto_recovery_enabled: true, ..Default::default() }; assert!(config.auto_recovery_enabled); } #[test] fn test_auto_recovery_disabled_configuration() { let config = CircuitBreakerConfig { auto_recovery_enabled: false, ..Default::default() }; assert!(!config.auto_recovery_enabled); } #[test] fn test_recovery_state_reset() { let mut state = CircuitBreakerState { is_active: true, consecutive_violations: 5, activation_reason: Some("Auto recovery test".to_string()), ..Default::default() }; // Simulate recovery state.is_active = false; state.consecutive_violations = 0; state.activation_reason = None; state.current_daily_loss = Price::ZERO; assert!(!state.is_active); assert_eq!(state.consecutive_violations, 0); assert!(state.activation_reason.is_none()); } }