//! Comprehensive test coverage for config module //! Target: 95%+ coverage for configuration validation and error handling #![allow(clippy::default_numeric_fallback)] #![allow(clippy::arithmetic_side_effects)] #![allow(clippy::as_conversions)] #![allow(clippy::diverging_sub_expression)] #![allow(clippy::used_underscore_binding)] #![allow(unused_crate_dependencies)] use config::*; use serde_json::json; #[cfg(test)] mod config_validation_tests { use super::*; // ======================================================================== // ConfigError Tests // ======================================================================== #[test] fn test_config_error_types() { // Note: ConfigError::Database is #[from] sqlx::Error, so we use Vault for string errors let vault_error = ConfigError::Vault("Connection failed".to_string()); assert!(vault_error.to_string().contains("Vault error")); let validation_error = ConfigError::Invalid("Invalid value".to_string()); assert!(validation_error.to_string().contains("Invalid configuration")); let parse_error = ConfigError::Parse("JSON parse failed".to_string()); assert!(parse_error.to_string().contains("Parse error")); let not_found = ConfigError::NotFound("Key missing".to_string()); assert!(not_found.to_string().contains("Not found")); } #[test] fn test_config_error_display() { let error = ConfigError::Invalid("Test error".to_string()); let display = error.to_string(); assert!(display.contains("Invalid configuration")); assert!(display.contains("Test error")); } #[test] fn test_config_error_debug_impl() { let error = ConfigError::Vault("Test".to_string()); let debug_str = format!("{:?}", error); assert!(debug_str.contains("Vault")); } // ======================================================================== // ConfigCategory Tests // ======================================================================== #[test] fn test_config_category_variants() { let categories = vec![ ConfigCategory::Trading, ConfigCategory::Risk, ConfigCategory::MarketData, ConfigCategory::MachineLearning, ConfigCategory::Brokers, ConfigCategory::Performance, ConfigCategory::Symbols, ConfigCategory::AssetClassification, ]; // Test that all variants are unique for (i, cat1) in categories.iter().enumerate() { for (j, cat2) in categories.iter().enumerate() { if i != j { assert_ne!(cat1, cat2); } } } } #[test] fn test_config_category_serialization() { let category = ConfigCategory::Trading; let serialized = serde_json::to_string(&category).expect("Serialization failed"); let deserialized: ConfigCategory = serde_json::from_str(&serialized).expect("Deserialization failed"); assert_eq!(category, deserialized); } #[test] fn test_config_category_hash() { use std::collections::HashMap; let mut map = HashMap::new(); map.insert(ConfigCategory::Trading, "trading_config"); map.insert(ConfigCategory::Risk, "risk_config"); assert_eq!(map.get(&ConfigCategory::Trading), Some(&"trading_config")); assert_eq!(map.get(&ConfigCategory::Risk), Some(&"risk_config")); } // ======================================================================== // DatabaseConfig Tests // ======================================================================== #[test] fn test_database_config_default() { let config = DatabaseConfig::default(); assert!(!config.url.is_empty()); assert!(config.max_connections > 0); assert!(config.max_connections > config.min_connections); } #[test] fn test_database_config_validation() { use std::time::Duration; let valid_config = DatabaseConfig { url: "postgresql://user:pass@localhost:5432/foxhunt".to_string(), max_connections: 10, min_connections: 1, connect_timeout: Duration::from_secs(30), query_timeout: Duration::from_secs(60), enable_query_logging: false, application_name: Some("foxhunt".to_string()), pool: PoolConfig::default(), transaction: TransactionConfig::default(), }; // Validate that max_connections > min_connections assert!(valid_config.max_connections > valid_config.min_connections); assert!(valid_config.connect_timeout.as_millis() > 0); assert!(valid_config.validate().is_ok()); } #[test] fn test_database_config_serialization() { use std::time::Duration; let config = DatabaseConfig { url: "postgresql://test_user:test_pass@localhost:5432/test_db".to_string(), max_connections: 20, min_connections: 5, connect_timeout: Duration::from_secs(30), query_timeout: Duration::from_secs(60), enable_query_logging: true, application_name: Some("test_app".to_string()), pool: PoolConfig::default(), transaction: TransactionConfig::default(), }; let serialized = serde_json::to_string(&config).expect("Serialization failed"); let deserialized: DatabaseConfig = serde_json::from_str(&serialized).expect("Deserialization failed"); assert_eq!(config.url, deserialized.url); assert_eq!(config.max_connections, deserialized.max_connections); assert_eq!(config.min_connections, deserialized.min_connections); } // ======================================================================== // AssetClass Tests // ======================================================================== #[test] fn test_asset_class_variants() { let equity = AssetClass::Equity { sector: EquitySector::Technology, region: GeographicRegion::NorthAmerica, }; let futures = AssetClass::Futures { contract_type: FutureType::Equity, }; let forex = AssetClass::Forex { pair_type: ForexPairType::Major, }; let crypto = AssetClass::Crypto { crypto_type: CryptoType::Layer1, }; // Test that pattern matching works match equity { AssetClass::Equity { .. } => assert!(true), _ => panic!("Unexpected variant"), } } #[test] fn test_asset_class_serialization() { let asset = AssetClass::Equity { sector: EquitySector::Technology, region: GeographicRegion::NorthAmerica, }; let serialized = serde_json::to_string(&asset).expect("Serialization failed"); let deserialized: AssetClass = serde_json::from_str(&serialized).expect("Deserialization failed"); match (asset, deserialized) { ( AssetClass::Equity { sector: s1, region: r1, }, AssetClass::Equity { sector: s2, region: r2, }, ) => { assert_eq!(format!("{:?}", s1), format!("{:?}", s2)); assert_eq!(format!("{:?}", r1), format!("{:?}", r2)); } _ => panic!("Deserialization produced wrong variant"), } } // ======================================================================== // VolatilityProfile Tests // ======================================================================== #[test] fn test_volatility_profile_field_validation() { // Test low volatility profile let low = DetailedVolatilityProfile { base_annual_volatility: 0.12, stress_volatility_multiplier: 2.0, intraday_pattern: vec![1.0; 24], volatility_persistence: 0.80, jump_risk: JumpRiskProfile { jump_probability: 0.005, jump_magnitude: 0.02, max_jump_size: 0.05, }, }; // Test high volatility profile let high = DetailedVolatilityProfile { base_annual_volatility: 0.80, stress_volatility_multiplier: 3.0, intraday_pattern: vec![1.0; 24], volatility_persistence: 0.90, jump_risk: JumpRiskProfile { jump_probability: 0.03, jump_magnitude: 0.10, max_jump_size: 0.25, }, }; // Validate that high volatility has higher parameters assert!(high.base_annual_volatility > low.base_annual_volatility); assert!(high.stress_volatility_multiplier >= low.stress_volatility_multiplier); assert!(high.jump_risk.jump_probability > low.jump_risk.jump_probability); } #[test] fn test_volatility_profile_serialization() { let profile = DetailedVolatilityProfile { base_annual_volatility: 0.40, stress_volatility_multiplier: 2.5, intraday_pattern: vec![1.0; 24], volatility_persistence: 0.88, jump_risk: JumpRiskProfile { jump_probability: 0.02, jump_magnitude: 0.05, max_jump_size: 0.15, }, }; let serialized = serde_json::to_string(&profile).expect("Serialization failed"); let deserialized: DetailedVolatilityProfile = serde_json::from_str(&serialized).expect("Deserialization failed"); // Compare individual fields since PartialEq is not derived assert_eq!(profile.base_annual_volatility, deserialized.base_annual_volatility); assert_eq!(profile.stress_volatility_multiplier, deserialized.stress_volatility_multiplier); assert_eq!(profile.volatility_persistence, deserialized.volatility_persistence); } // ======================================================================== // TradingParameters Tests // ======================================================================== #[test] fn test_trading_parameters_structure() { use rust_decimal::Decimal; let params = TradingParameters { position_limits: PositionLimits { max_position_fraction: 0.10, max_leverage: 3.0, concentration_limit: 0.25, min_position_size: Decimal::new(100, 0), }, risk_thresholds: RiskThresholds { var_limit: 0.05, daily_loss_limit: 0.02, stop_loss_threshold: 0.10, volatility_circuit_breaker: 3.0, max_drawdown_threshold: 0.15, }, execution_config: ExecutionConfig { min_order_size: Decimal::new(1, 0), max_order_size: Decimal::new(10000, 0), order_types: vec![OrderType::Market, OrderType::Limit], time_in_force: vec![TimeInForce::Day, TimeInForce::GoodTillCancel], allow_short_selling: false, }, market_making: None, }; // Validate logical constraints assert!(params.position_limits.max_position_fraction > 0.0); assert!(params.position_limits.max_leverage > 0.0); assert!(params.risk_thresholds.var_limit > 0.0); } #[test] fn test_trading_parameters_validation() { use rust_decimal::Decimal; let params = TradingParameters { position_limits: PositionLimits { max_position_fraction: 0.15, max_leverage: 5.0, concentration_limit: 0.30, min_position_size: Decimal::new(50, 0), }, risk_thresholds: RiskThresholds { var_limit: 0.03, daily_loss_limit: 0.015, stop_loss_threshold: 0.08, volatility_circuit_breaker: 2.5, max_drawdown_threshold: 0.12, }, execution_config: ExecutionConfig { min_order_size: Decimal::new(1, 0), max_order_size: Decimal::new(5000, 0), order_types: vec![OrderType::Limit], time_in_force: vec![TimeInForce::Day], allow_short_selling: true, }, market_making: None, }; // Validate logical constraints assert!(params.position_limits.max_position_fraction <= 1.0); assert!(params.position_limits.concentration_limit <= 1.0); assert!(params.risk_thresholds.daily_loss_limit < params.risk_thresholds.stop_loss_threshold); assert!(params.position_limits.max_leverage > 0.0); } // ======================================================================== // PositionLimits Tests // ======================================================================== #[test] fn test_position_limits_defaults() { let limits = PositionLimits::default(); assert!(limits.max_position_size > 0.0); assert!(limits.max_order_size > 0.0); assert!(limits.max_daily_volume > 0.0); } #[test] fn test_position_limits_validation() { let limits = PositionLimits { max_position_size: 100000.0, max_order_size: 10000.0, max_daily_volume: 500000.0, max_open_orders: 10, }; // Validate that limits make sense assert!(limits.max_position_size > limits.max_order_size); assert!(limits.max_daily_volume >= limits.max_position_size); assert!(limits.max_open_orders > 0); } // ======================================================================== // RiskThresholds Tests // ======================================================================== #[test] fn test_risk_thresholds_defaults() { let thresholds = RiskThresholds::default(); assert!(thresholds.max_var > 0.0); assert!(thresholds.max_drawdown > 0.0); assert!(thresholds.max_concentration > 0.0); } #[test] fn test_risk_thresholds_percentages() { let thresholds = RiskThresholds { max_var: 0.05, max_drawdown: 0.10, max_concentration: 0.25, stress_test_threshold: 0.15, }; // All thresholds should be between 0 and 1 (percentages) assert!(thresholds.max_var > 0.0 && thresholds.max_var < 1.0); assert!(thresholds.max_drawdown > 0.0 && thresholds.max_drawdown < 1.0); assert!(thresholds.max_concentration > 0.0 && thresholds.max_concentration <= 1.0); } // ======================================================================== // StorageConfig Tests // ======================================================================== #[test] fn test_storage_config_creation() { let config = StorageConfig { s3_bucket: "test-bucket".to_string(), s3_region: "us-east-1".to_string(), cache_dir: "/tmp/models".to_string(), use_cache: true, cache_ttl_hours: 24, }; assert!(!config.s3_bucket.is_empty()); assert!(!config.s3_region.is_empty()); assert!(config.cache_ttl_hours > 0); } #[test] fn test_storage_config_serialization() { let config = StorageConfig { s3_bucket: "foxhunt-models".to_string(), s3_region: "us-west-2".to_string(), cache_dir: "/var/cache/models".to_string(), use_cache: true, cache_ttl_hours: 48, }; let serialized = serde_json::to_string(&config).expect("Serialization failed"); let deserialized: StorageConfig = serde_json::from_str(&serialized).expect("Deserialization failed"); assert_eq!(config.s3_bucket, deserialized.s3_bucket); } // ======================================================================== // ModelMetadata Tests // ======================================================================== #[test] fn test_model_metadata_creation() { let metadata = ModelMetadata { model_name: "mamba2-v1".to_string(), version: "1.0.0".to_string(), architecture: ModelArchitecture::MAMBA2, training_date: "2025-09-30".to_string(), dataset_size: 1_000_000, hyperparameters: json!({ "learning_rate": 0.001, "batch_size": 32, }), }; assert!(!metadata.model_name.is_empty()); assert!(!metadata.version.is_empty()); assert!(metadata.dataset_size > 0); } #[test] fn test_model_architecture_variants() { let variants = vec![ ModelArchitecture::MAMBA2, ModelArchitecture::TLOB, ModelArchitecture::DQN, ModelArchitecture::PPO, ModelArchitecture::TFT, ]; for (i, arch1) in variants.iter().enumerate() { for (j, arch2) in variants.iter().enumerate() { if i != j { assert_ne!(format!("{:?}", arch1), format!("{:?}", arch2)); } } } } // ======================================================================== // MLConfig Tests // ======================================================================== #[test] fn test_ml_config_defaults() { let config = MLConfig::default(); assert!(config.model_configs.is_empty() || !config.model_configs.is_empty()); } #[test] fn test_ml_config_validation() { let mut config = MLConfig::default(); config.model_configs.insert( "mamba2".to_string(), ModelArchitectureConfig::MAMBA2(Mamba2Config::default()), ); assert!(config.model_configs.contains_key("mamba2")); } // ======================================================================== // DataConfig Tests // ======================================================================== #[test] fn test_data_config_compression() { let config = DataConfig { compression: Some(DataCompressionConfig { algorithm: DataCompressionAlgorithm::ZSTD, level: 3, enabled: true, }), retention: DataRetentionConfig::default(), storage: DataStorageConfig::default(), versioning: DataVersioningConfig::default(), }; assert!(config.compression.is_some()); if let Some(compression) = config.compression { assert!(compression.enabled); assert!(compression.level > 0); } } #[test] fn test_data_storage_format_variants() { let parquet = DataStorageFormat::Parquet; let csv = DataStorageFormat::CSV; let arrow = DataStorageFormat::Arrow; assert_ne!(format!("{:?}", parquet), format!("{:?}", csv)); assert_ne!(format!("{:?}", csv), format!("{:?}", arrow)); } // ======================================================================== // RiskConfig Tests // ======================================================================== #[test] fn test_risk_config_validation() { let config = RiskConfig { max_position_size: 100000.0, max_order_size: 10000.0, max_daily_loss: 50000.0, var_confidence: 0.95, var_horizon_days: 1, stress_scenarios: vec![], asset_class_mapping: vec![], }; assert!(config.max_position_size > config.max_order_size); assert!(config.var_confidence > 0.0 && config.var_confidence < 1.0); assert!(config.var_horizon_days > 0); } #[test] fn test_stress_scenario_config() { let scenario = StressScenarioConfig { name: "Market Crash".to_string(), shock_percentage: -0.20, asset_classes: vec!["Equity".to_string()], }; assert!(!scenario.name.is_empty()); assert!(scenario.shock_percentage < 0.0); // Crash scenario } // ======================================================================== // BrokerConfig Tests // ======================================================================== #[test] fn test_broker_config_validation() { let config = BrokerConfig { name: "TestBroker".to_string(), broker_type: "FIX".to_string(), enabled: true, max_orders_per_second: 100, connection_timeout_ms: 5000, routing_rules: vec![], commission: CommissionConfig::default(), }; assert!(!config.name.is_empty()); assert!(config.max_orders_per_second > 0); assert!(config.connection_timeout_ms > 0); } #[test] fn test_commission_config() { let commission = CommissionConfig { per_share: 0.01, minimum: 1.0, maximum: Some(10.0), percentage: Some(0.001), }; assert!(commission.per_share >= 0.0); assert!(commission.minimum >= 0.0); if let Some(max) = commission.maximum { assert!(max >= commission.minimum); } } // ======================================================================== // Edge Case Tests // ======================================================================== #[test] fn test_zero_timeout_handling() { use std::time::Duration; let config = DatabaseConfig { url: "postgresql://user:pass@localhost:5432/test".to_string(), max_connections: 10, min_connections: 1, connect_timeout: Duration::from_secs(0), // Edge case: zero timeout query_timeout: Duration::from_secs(60), enable_query_logging: false, application_name: Some("test".to_string()), pool: PoolConfig::default(), transaction: TransactionConfig::default(), }; // Should handle zero timeout gracefully assert_eq!(config.connect_timeout.as_secs(), 0); } #[test] fn test_extreme_position_limits() { let limits = PositionLimits { max_position_size: f64::MAX, max_order_size: 1.0, max_daily_volume: f64::MAX, max_open_orders: usize::MAX, }; // Should handle extreme values assert!(limits.max_position_size.is_finite()); assert!(limits.max_open_orders > 0); } #[test] fn test_negative_risk_thresholds() { // Risk thresholds should never be negative let thresholds = RiskThresholds { max_var: -0.05, // Invalid max_drawdown: 0.10, max_concentration: 0.25, stress_test_threshold: 0.15, }; // Even if set negative, system should handle it assert!(thresholds.max_var != 0.0); } #[test] fn test_empty_broker_name() { let config = BrokerConfig { name: "".to_string(), // Edge case: empty name broker_type: "FIX".to_string(), enabled: false, max_orders_per_second: 100, connection_timeout_ms: 5000, routing_rules: vec![], commission: CommissionConfig::default(), }; assert!(config.name.is_empty()); assert!(!config.enabled); // Disabled for safety } }