//! Asset Parser Unit Tests //! //! Comprehensive test suite for asset parsing and validation. //! Tests cover futures, equities, mixed assets, whitespace handling, //! case normalization, and error cases. use ml_training_service::asset_parser::{Asset, AssetParser}; // ============================================================================ // FUTURES PARSING TESTS // ============================================================================ #[test] fn test_parse_single_future_es() { let result = AssetParser::parse("ES.FUT").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"), _ => panic!("Expected Future variant"), } } #[test] fn test_parse_single_future_nq() { let result = AssetParser::parse("NQ.FUT").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Future { symbol } => assert_eq!(symbol, "NQ.FUT"), _ => panic!("Expected Future variant"), } } #[test] fn test_parse_single_future_6e() { let result = AssetParser::parse("6E.FUT").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Future { symbol } => assert_eq!(symbol, "6E.FUT"), _ => panic!("Expected Future variant"), } } #[test] fn test_parse_single_future_zn() { let result = AssetParser::parse("ZN.FUT").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Future { symbol } => assert_eq!(symbol, "ZN.FUT"), _ => panic!("Expected Future variant"), } } #[test] fn test_parse_multiple_futures() { let result = AssetParser::parse("ES.FUT,NQ.FUT").unwrap(); assert_eq!(result.len(), 2); let symbols: Vec = result.iter().map(|a| match a { Asset::Future { symbol } => symbol.clone(), _ => panic!("Expected Future variant"), }).collect(); assert!(symbols.contains(&"ES.FUT".to_string())); assert!(symbols.contains(&"NQ.FUT".to_string())); } #[test] fn test_parse_four_futures() { let result = AssetParser::parse("ES.FUT,NQ.FUT,6E.FUT,ZN.FUT").unwrap(); assert_eq!(result.len(), 4); let symbols: Vec = result.iter().map(|a| match a { Asset::Future { symbol } => symbol.clone(), _ => panic!("Expected Future variant"), }).collect(); assert!(symbols.contains(&"ES.FUT".to_string())); assert!(symbols.contains(&"NQ.FUT".to_string())); assert!(symbols.contains(&"6E.FUT".to_string())); assert!(symbols.contains(&"ZN.FUT".to_string())); } // ============================================================================ // EQUITIES PARSING TESTS // ============================================================================ #[test] fn test_parse_single_equity_aapl() { let result = AssetParser::parse("AAPL").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Equity { symbol } => assert_eq!(symbol, "AAPL"), _ => panic!("Expected Equity variant"), } } #[test] fn test_parse_single_equity_msft() { let result = AssetParser::parse("MSFT").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Equity { symbol } => assert_eq!(symbol, "MSFT"), _ => panic!("Expected Equity variant"), } } #[test] fn test_parse_single_equity_googl() { let result = AssetParser::parse("GOOGL").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Equity { symbol } => assert_eq!(symbol, "GOOGL"), _ => panic!("Expected Equity variant"), } } #[test] fn test_parse_single_equity_tsla() { let result = AssetParser::parse("TSLA").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Equity { symbol } => assert_eq!(symbol, "TSLA"), _ => panic!("Expected Equity variant"), } } #[test] fn test_parse_multiple_equities() { let result = AssetParser::parse("AAPL,MSFT,GOOGL").unwrap(); assert_eq!(result.len(), 3); let symbols: Vec = result.iter().map(|a| match a { Asset::Equity { symbol } => symbol.clone(), _ => panic!("Expected Equity variant"), }).collect(); assert!(symbols.contains(&"AAPL".to_string())); assert!(symbols.contains(&"MSFT".to_string())); assert!(symbols.contains(&"GOOGL".to_string())); } // ============================================================================ // MIXED ASSETS PARSING TESTS // ============================================================================ #[test] fn test_parse_mixed_futures_and_equities() { let result = AssetParser::parse("ES.FUT,AAPL,NQ.FUT,MSFT").unwrap(); assert_eq!(result.len(), 4); // Count futures vs equities let futures_count = result.iter().filter(|a| matches!(a, Asset::Future { .. })).count(); let equities_count = result.iter().filter(|a| matches!(a, Asset::Equity { .. })).count(); assert_eq!(futures_count, 2); assert_eq!(equities_count, 2); } #[test] fn test_parse_complex_mixed_list() { let result = AssetParser::parse("ES.FUT,AAPL,NQ.FUT,MSFT,6E.FUT,GOOGL,ZN.FUT,TSLA").unwrap(); assert_eq!(result.len(), 8); let futures_count = result.iter().filter(|a| matches!(a, Asset::Future { .. })).count(); let equities_count = result.iter().filter(|a| matches!(a, Asset::Equity { .. })).count(); assert_eq!(futures_count, 4); assert_eq!(equities_count, 4); } // ============================================================================ // WHITESPACE HANDLING TESTS // ============================================================================ #[test] fn test_parse_with_leading_whitespace() { let result = AssetParser::parse(" ES.FUT").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"), _ => panic!("Expected Future variant"), } } #[test] fn test_parse_with_trailing_whitespace() { let result = AssetParser::parse("ES.FUT ").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"), _ => panic!("Expected Future variant"), } } #[test] fn test_parse_with_surrounding_whitespace() { let result = AssetParser::parse(" ES.FUT ").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"), _ => panic!("Expected Future variant"), } } #[test] fn test_parse_with_whitespace_in_list() { let result = AssetParser::parse(" ES.FUT , AAPL , NQ.FUT ").unwrap(); assert_eq!(result.len(), 3); } #[test] fn test_parse_with_tabs_and_spaces() { let result = AssetParser::parse("ES.FUT\t,\t AAPL , NQ.FUT").unwrap(); assert_eq!(result.len(), 3); } // ============================================================================ // CASE NORMALIZATION TESTS // ============================================================================ #[test] fn test_parse_lowercase_future_normalizes() { let result = AssetParser::parse("es.fut").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"), _ => panic!("Expected Future variant"), } } #[test] fn test_parse_lowercase_equity_normalizes() { let result = AssetParser::parse("aapl").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Equity { symbol } => assert_eq!(symbol, "AAPL"), _ => panic!("Expected Equity variant"), } } #[test] fn test_parse_mixed_case_future_normalizes() { let result = AssetParser::parse("Es.FuT").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"), _ => panic!("Expected Future variant"), } } #[test] fn test_parse_mixed_case_equity_normalizes() { let result = AssetParser::parse("AaPl").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Equity { symbol } => assert_eq!(symbol, "AAPL"), _ => panic!("Expected Equity variant"), } } // ============================================================================ // DEDUPLICATION TESTS // ============================================================================ #[test] fn test_parse_deduplicates_exact_matches() { let result = AssetParser::parse("ES.FUT,ES.FUT,ES.FUT").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"), _ => panic!("Expected Future variant"), } } #[test] fn test_parse_deduplicates_case_insensitive() { let result = AssetParser::parse("ES.FUT,es.fut,Es.FuT").unwrap(); assert_eq!(result.len(), 1); match &result[0] { Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"), _ => panic!("Expected Future variant"), } } #[test] fn test_parse_deduplicates_mixed_list() { let result = AssetParser::parse("ES.FUT,AAPL,ES.FUT,AAPL,NQ.FUT").unwrap(); assert_eq!(result.len(), 3); } // ============================================================================ // ERROR HANDLING TESTS - FUTURES // ============================================================================ #[test] fn test_reject_future_without_suffix() { let result = AssetParser::parse("ES"); assert!(result.is_err()); let err_msg = result.unwrap_err().to_string(); assert!(err_msg.contains("Invalid asset format")); } #[test] fn test_reject_future_wrong_suffix() { let result = AssetParser::parse("ES.FUTURE"); assert!(result.is_err()); let err_msg = result.unwrap_err().to_string(); assert!(err_msg.contains("Invalid asset format")); } #[test] fn test_reject_future_too_long_symbol() { let result = AssetParser::parse("TOOLONG.FUT"); assert!(result.is_err()); let err_msg = result.unwrap_err().to_string(); assert!(err_msg.contains("Invalid asset format")); } #[test] fn test_reject_future_empty_symbol() { let result = AssetParser::parse(".FUT"); assert!(result.is_err()); } // ============================================================================ // ERROR HANDLING TESTS - EQUITIES // ============================================================================ #[test] fn test_reject_equity_too_long() { let result = AssetParser::parse("TOOLONG"); assert!(result.is_err()); let err_msg = result.unwrap_err().to_string(); assert!(err_msg.contains("Invalid asset format")); } #[test] fn test_reject_equity_with_numbers() { let result = AssetParser::parse("AAPL1"); assert!(result.is_err()); } #[test] fn test_reject_equity_with_special_chars() { let result = AssetParser::parse("AAPL@"); assert!(result.is_err()); } // ============================================================================ // ERROR HANDLING TESTS - GENERAL // ============================================================================ #[test] fn test_reject_empty_string() { let result = AssetParser::parse(""); assert!(result.is_err()); let err_msg = result.unwrap_err().to_string(); assert!(err_msg.contains("empty") || err_msg.contains("asset list")); } #[test] fn test_reject_only_whitespace() { let result = AssetParser::parse(" "); assert!(result.is_err()); } #[test] fn test_reject_only_commas() { let result = AssetParser::parse(",,,"); assert!(result.is_err()); } #[test] fn test_reject_invalid_characters_in_future() { let result = AssetParser::parse("ES@.FUT"); assert!(result.is_err()); } #[test] fn test_reject_invalid_characters_in_equity() { let result = AssetParser::parse("A#PPL"); assert!(result.is_err()); } #[test] fn test_partial_invalid_list_fails_all() { // If any asset is invalid, the entire parse should fail let result = AssetParser::parse("ES.FUT,INVALID_FORMAT,AAPL"); assert!(result.is_err()); } // ============================================================================ // VALIDATE METHODS TESTS // ============================================================================ #[test] fn test_validate_future_valid_es() { let result = AssetParser::validate_future("ES.FUT"); assert!(result.is_ok()); match result.unwrap() { Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"), _ => panic!("Expected Future variant"), } } #[test] fn test_validate_future_valid_6e() { let result = AssetParser::validate_future("6E.FUT"); assert!(result.is_ok()); } #[test] fn test_validate_future_invalid_missing_suffix() { let result = AssetParser::validate_future("ES"); assert!(result.is_err()); } #[test] fn test_validate_equity_valid_aapl() { let result = AssetParser::validate_equity("AAPL"); assert!(result.is_ok()); match result.unwrap() { Asset::Equity { symbol } => assert_eq!(symbol, "AAPL"), _ => panic!("Expected Equity variant"), } } #[test] fn test_validate_equity_valid_googl() { let result = AssetParser::validate_equity("GOOGL"); assert!(result.is_ok()); } #[test] fn test_validate_equity_invalid_too_long() { let result = AssetParser::validate_equity("TOOLONG"); assert!(result.is_err()); } // ============================================================================ // DISPLAY METHODS TESTS // ============================================================================ #[test] fn test_asset_display_future() { let asset = Asset::Future { symbol: "ES.FUT".to_string() }; assert_eq!(format!("{}", asset), "ES.FUT"); } #[test] fn test_asset_display_equity() { let asset = Asset::Equity { symbol: "AAPL".to_string() }; assert_eq!(format!("{}", asset), "AAPL"); } // ============================================================================ // EDGE CASES // ============================================================================ #[test] fn test_reject_single_character_equity_ambiguous() { // Single character symbols rejected to avoid futures ambiguity // Use .FUT suffix for futures: X.FUT let result = AssetParser::parse("X"); assert!(result.is_err()); } #[test] fn test_parse_one_character_future() { let result = AssetParser::parse("E.FUT"); assert!(result.is_ok()); match &result.unwrap()[0] { Asset::Future { symbol } => assert_eq!(symbol, "E.FUT"), _ => panic!("Expected Future variant"), } } #[test] fn test_parse_max_length_future() { let result = AssetParser::parse("ABCD.FUT"); assert!(result.is_ok()); } #[test] fn test_parse_max_length_equity() { let result = AssetParser::parse("ABCDE"); assert!(result.is_ok()); }