//! Comprehensive tests for configuration schemas module. //! //! Tests for S3Config, ConfigSchema, and AssetClassificationSchema structures //! including validation, serialization, classification logic, and edge cases. use chrono::Utc; use config::schemas::{AssetClassificationSchema, ConfigSchema, S3Config}; use std::time::Duration; use uuid::Uuid; // ============================================================================ // S3Config Tests (12 tests) // ============================================================================ #[test] fn test_s3config_default_values() { let config = S3Config::default(); assert_eq!(config.bucket_name, "foxhunt-models"); assert_eq!(config.region, "us-east-1"); assert!(config.access_key_id.is_none()); assert!(config.secret_access_key.is_none()); assert!(config.session_token.is_none()); assert!(config.endpoint_url.is_none()); assert!(!config.force_path_style); assert_eq!(config.timeout, Duration::from_secs(30)); assert_eq!(config.max_retry_attempts, 3); assert!(config.use_ssl); } #[test] fn test_s3config_validate_success() { let config = S3Config { bucket_name: "test-bucket".to_string(), region: "us-west-2".to_string(), ..Default::default() }; let result = config.validate(); assert!(result.is_ok()); } #[test] fn test_s3config_validate_empty_bucket_name() { let config = S3Config { bucket_name: "".to_string(), region: "us-west-2".to_string(), ..Default::default() }; let result = config.validate(); assert!(result.is_err()); assert_eq!(result.unwrap_err(), "S3 bucket name cannot be empty"); } #[test] fn test_s3config_validate_empty_region() { let config = S3Config { bucket_name: "test-bucket".to_string(), region: "".to_string(), ..Default::default() }; let result = config.validate(); assert!(result.is_err()); assert_eq!(result.unwrap_err(), "S3 region cannot be empty"); } #[test] fn test_s3config_with_credentials() { let config = S3Config { bucket_name: "secure-bucket".to_string(), region: "eu-west-1".to_string(), access_key_id: Some("AKIAIOSFODNN7EXAMPLE".to_string()), secret_access_key: Some("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string()), session_token: Some("session-token-123".to_string()), ..Default::default() }; assert!(config.validate().is_ok()); assert_eq!(config.access_key_id.unwrap(), "AKIAIOSFODNN7EXAMPLE"); assert_eq!( config.secret_access_key.unwrap(), "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" ); assert_eq!(config.session_token.unwrap(), "session-token-123"); } #[test] fn test_s3config_minio_endpoint() { let config = S3Config { bucket_name: "minio-bucket".to_string(), region: "us-east-1".to_string(), endpoint_url: Some("http://localhost:9000".to_string()), force_path_style: true, use_ssl: false, ..Default::default() }; assert!(config.validate().is_ok()); assert_eq!(config.endpoint_url.unwrap(), "http://localhost:9000"); assert!(config.force_path_style); assert!(!config.use_ssl); } #[test] fn test_s3config_custom_timeout_and_retries() { let config = S3Config { bucket_name: "timeout-test".to_string(), region: "ap-southeast-2".to_string(), timeout: Duration::from_secs(60), max_retry_attempts: 5, ..Default::default() }; assert!(config.validate().is_ok()); assert_eq!(config.timeout, Duration::from_secs(60)); assert_eq!(config.max_retry_attempts, 5); } #[test] fn test_s3config_serialization() { let config = S3Config { bucket_name: "serde-test".to_string(), region: "us-east-1".to_string(), access_key_id: Some("key123".to_string()), ..Default::default() }; let json = serde_json::to_string(&config).unwrap(); assert!(json.contains("serde-test")); assert!(json.contains("us-east-1")); assert!(json.contains("key123")); let deserialized: S3Config = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.bucket_name, "serde-test"); assert_eq!(deserialized.region, "us-east-1"); assert_eq!(deserialized.access_key_id.unwrap(), "key123"); } #[test] fn test_s3config_clone() { let config = S3Config { bucket_name: "clone-test".to_string(), region: "us-west-1".to_string(), max_retry_attempts: 10, ..Default::default() }; let cloned = config; assert_eq!(cloned.bucket_name, "clone-test"); assert_eq!(cloned.region, "us-west-1"); assert_eq!(cloned.max_retry_attempts, 10); } #[test] fn test_s3config_debug_format() { let config = S3Config { bucket_name: "debug-test".to_string(), region: "eu-central-1".to_string(), ..Default::default() }; let debug_str = format!("{:?}", config); assert!(debug_str.contains("debug-test")); assert!(debug_str.contains("eu-central-1")); } #[test] fn test_s3config_edge_case_zero_timeout() { let config = S3Config { bucket_name: "zero-timeout".to_string(), region: "us-east-1".to_string(), timeout: Duration::from_secs(0), max_retry_attempts: 0, ..Default::default() }; assert!(config.validate().is_ok()); assert_eq!(config.timeout, Duration::from_secs(0)); assert_eq!(config.max_retry_attempts, 0); } #[test] fn test_s3config_edge_case_very_long_bucket_name() { let long_name = "a".repeat(255); let config = S3Config { bucket_name: long_name, region: "us-east-1".to_string(), ..Default::default() }; assert!(config.validate().is_ok()); assert_eq!(config.bucket_name.len(), 255); } // ============================================================================ // ConfigSchema Tests (7 tests) // ============================================================================ #[test] fn test_config_schema_creation() { let schema = ConfigSchema { id: Uuid::new_v4(), version: "1.0.0".to_string(), created_at: Utc::now(), updated_at: Utc::now(), }; assert!(!schema.id.to_string().is_empty()); assert_eq!(schema.version, "1.0.0"); assert!(schema.created_at <= schema.updated_at); } #[test] fn test_config_schema_semantic_versioning() { let versions = vec!["1.0.0", "1.2.3", "2.0.0-beta", "3.1.4-alpha.1"]; for version in versions { let schema = ConfigSchema { id: Uuid::new_v4(), version: version.to_string(), created_at: Utc::now(), updated_at: Utc::now(), }; assert_eq!(schema.version, version); } } #[test] fn test_config_schema_uuid_uniqueness() { let schema1 = ConfigSchema { id: Uuid::new_v4(), version: "1.0.0".to_string(), created_at: Utc::now(), updated_at: Utc::now(), }; let schema2 = ConfigSchema { id: Uuid::new_v4(), version: "1.0.0".to_string(), created_at: Utc::now(), updated_at: Utc::now(), }; assert_ne!(schema1.id, schema2.id); } #[test] fn test_config_schema_timestamp_ordering() { let now = Utc::now(); let schema = ConfigSchema { id: Uuid::new_v4(), version: "1.0.0".to_string(), created_at: now, updated_at: now, }; assert!(schema.created_at <= schema.updated_at); } #[test] fn test_config_schema_serialization() { let schema = ConfigSchema { id: Uuid::new_v4(), version: "2.3.4".to_string(), created_at: Utc::now(), updated_at: Utc::now(), }; let json = serde_json::to_string(&schema).unwrap(); assert!(json.contains("2.3.4")); let deserialized: ConfigSchema = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.id, schema.id); assert_eq!(deserialized.version, "2.3.4"); } #[test] fn test_config_schema_clone() { let schema = ConfigSchema { id: Uuid::new_v4(), version: "1.5.0".to_string(), created_at: Utc::now(), updated_at: Utc::now(), }; let cloned = schema.clone(); assert_eq!(cloned.id, schema.id); assert_eq!(cloned.version, schema.version); assert_eq!(cloned.created_at, schema.created_at); } #[test] fn test_config_schema_debug_format() { let schema = ConfigSchema { id: Uuid::new_v4(), version: "3.0.0".to_string(), created_at: Utc::now(), updated_at: Utc::now(), }; let debug_str = format!("{:?}", schema); assert!(debug_str.contains("3.0.0")); assert!(debug_str.contains(&schema.id.to_string())); } // ============================================================================ // AssetClassificationSchema Tests (16 tests) // ============================================================================ #[test] fn test_asset_classification_new() { let config = AssetClassificationSchema::new(); assert_eq!(config.asset_type_rules.len(), 5); assert_eq!(config.default_sectors.len(), 5); assert_eq!(config.currency_patterns.len(), 5); assert_eq!(config.crypto_patterns.len(), 3); } #[test] fn test_asset_classification_default() { let config = AssetClassificationSchema::default(); assert!(config.asset_type_rules.contains_key("EQUITY")); assert!(config.asset_type_rules.contains_key("FOREX")); assert!(config.asset_type_rules.contains_key("CRYPTO")); assert_eq!(config.asset_type_rules["EQUITY"], "Equity"); assert_eq!(config.asset_type_rules["FOREX"], "Currencies"); } #[test] fn test_asset_classification_asset_type_rules() { let config = AssetClassificationSchema::new(); assert_eq!(config.asset_type_rules.get("EQUITY").unwrap(), "Equity"); assert_eq!(config.asset_type_rules.get("FOREX").unwrap(), "Currencies"); assert_eq!( config.asset_type_rules.get("CRYPTO").unwrap(), "Cryptocurrency" ); assert_eq!( config.asset_type_rules.get("COMMODITY").unwrap(), "Commodities" ); assert_eq!(config.asset_type_rules.get("BOND").unwrap(), "Fixed Income"); } #[test] fn test_asset_classification_classify_by_asset_type() { let config = AssetClassificationSchema::new(); let sector = config.classify_sector("AAPL", Some("EQUITY")); assert_eq!(sector, "Equity"); let sector = config.classify_sector("EURUSD", Some("FOREX")); assert_eq!(sector, "Currencies"); let sector = config.classify_sector("BTC", Some("CRYPTO")); assert_eq!(sector, "Cryptocurrency"); } #[test] fn test_asset_classification_classify_currency_patterns() { let config = AssetClassificationSchema::new(); // Test standard 6-character currency pair let sector = config.classify_sector("EURUSD", None); assert_eq!(sector, "Currencies"); let sector = config.classify_sector("GBPJPY", None); assert_eq!(sector, "Currencies"); // Test USD pattern let sector = config.classify_sector("USD/EUR", None); assert_eq!(sector, "Currencies"); } #[test] fn test_asset_classification_classify_crypto_patterns() { let config = AssetClassificationSchema::new(); // Test crypto patterns that don't match any currency pattern // Currency patterns check for: ^[A-Z]{3}[A-Z]{3}$ OR .*USD/EUR/GBP/JPY.* // CRYPTO_INDEX: 12 chars, no USD/EUR/GBP/JPY, contains CRYPTO let sector = config.classify_sector("CRYPTO_INDEX", None); assert_eq!(sector, "Cryptocurrency"); // BTC_PERP: 8 chars, no USD/EUR/GBP/JPY, contains BTC let sector = config.classify_sector("BTC_PERP", None); assert_eq!(sector, "Cryptocurrency"); // ETH_FUTURE: 10 chars, no USD/EUR/GBP/JPY, contains ETH let sector = config.classify_sector("ETH_FUTURE", None); assert_eq!(sector, "Cryptocurrency"); } #[test] fn test_asset_classification_default_classification() { let config = AssetClassificationSchema::new(); // Unknown instruments should return "Other" let sector = config.classify_sector("AAPL", None); assert_eq!(sector, "Other"); let sector = config.classify_sector("TSLA", None); assert_eq!(sector, "Other"); } #[test] fn test_asset_classification_priority_asset_type_over_pattern() { let config = AssetClassificationSchema::new(); // Even though "BTCUSD" matches crypto pattern, explicit asset type wins let sector = config.classify_sector("BTCUSD", Some("FOREX")); assert_eq!(sector, "Currencies"); } #[test] fn test_asset_classification_currency_pattern_matching() { let config = AssetClassificationSchema::new(); // Test various currency patterns let instruments = vec![ ("EURUSD", "Currencies"), ("USD/JPY", "Currencies"), ("EUR-USD", "Currencies"), ("GBP_USD", "Currencies"), ]; for (instrument, expected) in instruments { let sector = config.classify_sector(instrument, None); assert_eq!(sector, expected, "Failed for instrument: {}", instrument); } } #[test] fn test_asset_classification_crypto_pattern_matching() { let config = AssetClassificationSchema::new(); // Test crypto patterns that don't match currency patterns // Currency patterns: ^[A-Z]{3}[A-Z]{3}$ (exactly 6 uppercase) or .*USD/EUR/GBP/JPY.* let crypto_instruments = vec![ ("BTC_PERP", "Cryptocurrency"), // 8 chars, contains BTC ("ETH_SPOT", "Cryptocurrency"), // 8 chars, contains ETH ("CRYPTO_BTC", "Cryptocurrency"), // 10 chars, contains CRYPTO ("CRYPTO_INDEX", "Cryptocurrency"), // 12 chars, contains CRYPTO ("ETH_FUTURE", "Cryptocurrency"), // 10 chars, contains ETH ]; for (instrument, expected) in crypto_instruments { let sector = config.classify_sector(instrument, None); assert_eq!(sector, expected, "Failed for: {}", instrument); } } #[test] fn test_asset_classification_edge_case_empty_instrument() { let config = AssetClassificationSchema::new(); let sector = config.classify_sector("", None); assert_eq!(sector, "Other"); } #[test] fn test_asset_classification_edge_case_invalid_asset_type() { let config = AssetClassificationSchema::new(); // Invalid asset type should fall back to pattern matching let sector = config.classify_sector("EURUSD", Some("INVALID")); assert_eq!(sector, "Currencies"); // Matches currency pattern } #[test] fn test_asset_classification_case_sensitivity() { let config = AssetClassificationSchema::new(); // Test case sensitivity with crypto pattern (avoid 6-char currency pattern) let sector_upper = config.classify_sector("BTC_PERP", None); let sector_lower = config.classify_sector("btc_perp", None); assert_eq!(sector_upper, "Cryptocurrency"); assert_eq!(sector_lower, "Other"); // lowercase doesn't match pattern } #[test] fn test_asset_classification_serialization() { let config = AssetClassificationSchema::new(); let json = serde_json::to_string(&config).unwrap(); assert!(json.contains("EQUITY")); assert!(json.contains("Currencies")); let deserialized: AssetClassificationSchema = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.asset_type_rules.len(), 5); assert_eq!(deserialized.currency_patterns.len(), 5); } #[test] fn test_asset_classification_clone() { let config = AssetClassificationSchema::new(); let cloned = config.clone(); assert_eq!(cloned.asset_type_rules.len(), config.asset_type_rules.len()); assert_eq!(cloned.default_sectors.len(), config.default_sectors.len()); assert_eq!( cloned.currency_patterns.len(), config.currency_patterns.len() ); assert_eq!(cloned.crypto_patterns.len(), config.crypto_patterns.len()); } #[test] fn test_asset_classification_debug_format() { let config = AssetClassificationSchema::new(); let debug_str = format!("{:?}", config); assert!(debug_str.contains("asset_type_rules")); assert!(debug_str.contains("EQUITY")); assert!(debug_str.contains("Currencies")); } // ============================================================================ // Integration Tests (3 tests) // ============================================================================ #[test] fn test_integration_s3_config_with_asset_classification() { let s3_config = S3Config { bucket_name: "trading-models".to_string(), region: "us-east-1".to_string(), ..Default::default() }; let asset_config = AssetClassificationSchema::new(); assert!(s3_config.validate().is_ok()); assert_eq!( asset_config.classify_sector("AAPL", Some("EQUITY")), "Equity" ); } #[test] fn test_integration_config_schema_versioning() { let schema_v1 = ConfigSchema { id: Uuid::new_v4(), version: "1.0.0".to_string(), created_at: Utc::now(), updated_at: Utc::now(), }; let schema_v2 = ConfigSchema { id: schema_v1.id, // Same config, new version version: "2.0.0".to_string(), created_at: schema_v1.created_at, updated_at: Utc::now(), }; assert_eq!(schema_v1.id, schema_v2.id); assert_ne!(schema_v1.version, schema_v2.version); assert!(schema_v2.updated_at >= schema_v1.updated_at); } #[test] fn test_integration_full_config_serialization() { let s3_config = S3Config::default(); let asset_config = AssetClassificationSchema::new(); let schema = ConfigSchema { id: Uuid::new_v4(), version: "1.0.0".to_string(), created_at: Utc::now(), updated_at: Utc::now(), }; // Test that all configs can be serialized independently let s3_json = serde_json::to_string(&s3_config).unwrap(); let asset_json = serde_json::to_string(&asset_config).unwrap(); let schema_json = serde_json::to_string(&schema).unwrap(); assert!(s3_json.contains("foxhunt-models")); assert!(asset_json.contains("EQUITY")); assert!(schema_json.contains("1.0.0")); // Test deserialization let _: S3Config = serde_json::from_str(&s3_json).unwrap(); let _: AssetClassificationSchema = serde_json::from_str(&asset_json).unwrap(); let _: ConfigSchema = serde_json::from_str(&schema_json).unwrap(); }