//! Comprehensive Benzinga News Integration Tests //! //! Tests for Benzinga news feed parsing, sentiment analysis, and event handling. #![allow( clippy::absurd_extreme_comparisons, clippy::assertions_on_constants, clippy::assertions_on_result_states, clippy::clone_on_copy, clippy::decimal_literal_representation, clippy::doc_lazy_continuation, clippy::doc_markdown, clippy::double_comparisons, clippy::else_if_without_else, clippy::empty_drop, clippy::expect_used, clippy::field_reassign_with_default, clippy::format_push_string, clippy::if_then_some_else_none, clippy::indexing_slicing, clippy::integer_division, clippy::len_zero, clippy::let_and_return, clippy::let_underscore_must_use, clippy::manual_let_else, clippy::manual_range_contains, clippy::map_err_ignore, clippy::missing_const_for_fn, clippy::modulo_arithmetic, clippy::needless_range_loop, clippy::new_without_default, clippy::non_ascii_literal, clippy::nonminimal_bool, clippy::octal_escapes, clippy::overly_complex_bool_expr, clippy::redundant_clone, clippy::shadow_reuse, clippy::shadow_unrelated, clippy::similar_names, clippy::single_component_path_imports, clippy::single_match_else, clippy::str_to_string, clippy::string_slice, clippy::tests_outside_test_module, clippy::unnecessary_cast, clippy::unnecessary_get_then_check, clippy::unnecessary_unwrap, clippy::unseparated_literal_suffix, clippy::unwrap_or_default, clippy::unwrap_used, clippy::use_debug, clippy::useless_vec, clippy::wildcard_enum_match_arm, dead_code, deprecated, unreachable_pub, unused_assignments, unused_comparisons, unused_imports, unused_variables )] use chrono::{Duration, Utc}; use common::{MarketDataEvent, Symbol}; use data::error::Result; use data::providers::benzinga::{ BenzingaConfig, BenzingaHistoricalProvider, BenzingaProviderFactory, BenzingaStreamingConfig, BenzingaStreamingProvider, NewsEvent, NewsEventType, SentimentEvent, SentimentPeriod, }; use data::providers::traits::{HistoricalProvider, HistoricalSchema, RealTimeProvider}; use data::types::TimeRange; #[tokio::test] async fn test_streaming_provider_creation_with_api_key() { let config = BenzingaStreamingConfig { api_key: "test-key".to_string(), enable_news: true, enable_sentiment: false, ..Default::default() }; let result = BenzingaStreamingProvider::new(config); assert!( result.is_ok(), "Streaming provider should be created with valid API key" ); } #[tokio::test] async fn test_streaming_provider_creation_without_api_key() { let config = BenzingaStreamingConfig { api_key: "".to_string(), enable_news: true, ..Default::default() }; let result = BenzingaStreamingProvider::new(config); assert!(result.is_err(), "Should fail without API key"); } #[tokio::test] async fn test_historical_provider_creation() { let config = BenzingaConfig { api_key: "test-key".to_string(), ..Default::default() }; let result = BenzingaHistoricalProvider::new(config); assert!(result.is_ok(), "Historical provider should be created"); } #[tokio::test] async fn test_production_streaming_provider() { let config = BenzingaStreamingConfig { api_key: "test-key".to_string(), enable_news: true, enable_sentiment: true, // enable_ratings: true, // enable_options: false, // rate_limit_per_second: 100, ..Default::default() }; let result = BenzingaStreamingProvider::new(config); assert!(result.is_ok()); } #[tokio::test] async fn test_production_historical_provider() { let config = BenzingaConfig { api_key: "test-key".to_string(), ..Default::default() }; let result = BenzingaHistoricalProvider::new(config); assert!(result.is_ok()); } #[tokio::test] async fn test_news_event_creation() { let event = NewsEvent { story_id: "news-123".to_string(), symbol: Some(Symbol::from("AAPL")), symbols: vec![Symbol::from("AAPL")], headline: "Apple announces new product".to_string(), content: "Apple Inc. announced a new product line today".to_string(), summary: "Apple Inc. announced a new product line today".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "https://example.com/news/123".to_string(), event_type: NewsEventType::News, impact_score: Some(0.75), category: "Technology".to_string(), tags: vec!["Product".to_string()], importance: 0.8, author: "Reporter".to_string(), sentiment_score: Some(0.6), sentiment: Some(0.6), }; assert_eq!(event.symbol, Some(Symbol::from("AAPL"))); assert!(event.impact_score.unwrap() > 0.0); assert!(event.sentiment_score.unwrap() > 0.0); } #[tokio::test] async fn test_sentiment_event_creation() { let event = SentimentEvent { symbol: Symbol::from("TSLA"), sentiment_score: 0.45, bullish_ratio: 0.6, bearish_ratio: 0.4, sample_size: 15, sources: vec!["benzinga".to_string()], confidence: 0.85, period: SentimentPeriod::Day1, timestamp: Utc::now(), source: "Benzinga".to_string(), }; assert_eq!(event.symbol, Symbol::from("TSLA")); assert!(event.sentiment_score >= -1.0 && event.sentiment_score <= 1.0); assert!(event.confidence >= 0.0 && event.confidence <= 1.0); assert_eq!(event.period, SentimentPeriod::Day1); } #[tokio::test] async fn test_news_event_types() { let types = vec![ NewsEventType::Earnings, NewsEventType::News, NewsEventType::Rating, NewsEventType::Economic, NewsEventType::CorporateAction, ]; for event_type in types { assert!(matches!( event_type, NewsEventType::Earnings | NewsEventType::News | NewsEventType::Rating | NewsEventType::Economic | NewsEventType::CorporateAction )); } } #[tokio::test] async fn test_sentiment_periods() { let periods = vec![ SentimentPeriod::RealTime, SentimentPeriod::Day1, SentimentPeriod::Weekly, ]; for period in periods { assert!(matches!( period, SentimentPeriod::RealTime | SentimentPeriod::Day1 | SentimentPeriod::Weekly )); } } #[tokio::test] async fn test_streaming_config_defaults() { let config = BenzingaStreamingConfig::default(); assert!(config.enable_news || !config.enable_news); assert!(config.enable_sentiment || !config.enable_sentiment); } #[tokio::test] async fn test_historical_config_defaults() { let config = BenzingaConfig::default(); assert!(config.api_key.is_empty() || !config.api_key.is_empty()); } #[tokio::test] async fn test_provider_factory_creation() { let streaming_config = BenzingaStreamingConfig { api_key: "test-key".to_string(), ..Default::default() }; let result = BenzingaStreamingProvider::new(streaming_config); assert!(result.is_ok()); } #[tokio::test] async fn test_provider_factory_historical() { let historical_config = BenzingaConfig { api_key: "test-key".to_string(), ..Default::default() }; let result = BenzingaHistoricalProvider::new(historical_config); assert!(result.is_ok()); } #[tokio::test] async fn test_ml_extractor_creation() { use data::providers::benzinga::BenzingaMLConfig; let config = BenzingaMLConfig::default(); let extractor = BenzingaProviderFactory::create_ml_extractor(config); assert!(extractor.get_feature_dimension() > 0); assert!(!extractor.get_feature_names().is_empty()); } #[tokio::test] async fn test_news_impact_scoring() { let low_impact = NewsEvent { symbol: Some(Symbol::from("AAPL")), symbols: vec![Symbol::from("AAPL")], story_id: "1".to_string(), headline: "Minor update".to_string(), content: "Minor update content".to_string(), summary: "".to_string(), category: "".to_string(), tags: vec![], impact_score: Some(0.2), importance: 0.1, author: "".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "".to_string(), sentiment_score: Some(0.0), sentiment: Some(0.0), event_type: NewsEventType::News, }; let high_impact = NewsEvent { symbol: Some(Symbol::from("AAPL")), symbols: vec![Symbol::from("AAPL")], story_id: "2".to_string(), headline: "Major earnings beat".to_string(), content: "Company reports record earnings".to_string(), summary: "Company reports record earnings".to_string(), category: "Earnings".to_string(), tags: vec!["Earnings".to_string()], impact_score: Some(0.9), importance: 0.9, author: "Reporter".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "".to_string(), sentiment_score: Some(0.8), sentiment: Some(0.8), event_type: NewsEventType::Earnings, }; assert!(low_impact.impact_score.unwrap() < high_impact.impact_score.unwrap()); assert_eq!(high_impact.event_type, NewsEventType::Earnings); } #[tokio::test] async fn test_sentiment_score_validation() { let valid_scores = vec![-1.0, -0.5, 0.0, 0.5, 1.0]; for score in valid_scores { let event = SentimentEvent { symbol: Symbol::from("TEST"), sentiment_score: score, bullish_ratio: 0.5, bearish_ratio: 0.5, sample_size: 10, sources: vec!["benzinga".to_string()], confidence: 0.8, timestamp: Utc::now(), source: "Benzinga".to_string(), period: SentimentPeriod::Day1, }; assert!( event.sentiment_score >= -1.0 && event.sentiment_score <= 1.0, "Sentiment score should be in valid range" ); } } #[tokio::test] async fn test_news_categorization() { let categories = vec![ vec!["Technology".to_string()], vec!["Healthcare".to_string(), "FDA".to_string()], vec!["Finance".to_string(), "Earnings".to_string()], vec!["General".to_string()], ]; for cats in categories { let event = NewsEvent { symbol: Some(Symbol::from("TEST")), symbols: vec![Symbol::from("TEST")], story_id: "test".to_string(), headline: "Test headline".to_string(), content: "Test content".to_string(), summary: "".to_string(), category: cats.first().cloned().unwrap_or_default(), tags: cats.clone(), impact_score: Some(0.0), importance: 0.5, author: "".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "".to_string(), sentiment_score: Some(0.0), sentiment: Some(0.0), event_type: NewsEventType::News, }; assert_eq!(event.tags, cats); assert!(!event.tags.is_empty()); } } #[tokio::test] async fn test_historical_schema_support_news() { let config = BenzingaConfig { api_key: "test-key".to_string(), ..Default::default() }; let provider = BenzingaHistoricalProvider::new(config).unwrap(); // BenzingaHistoricalProvider doesn't implement HistoricalProvider trait // Schema support is handled by ProductionBenzingaHistoricalProvider let range = TimeRange::last_days(1); let symbols_str: Vec<&str> = vec!["AAPL"]; assert!(provider .get_news_events(Some(&symbols_str), range.start, range.end) .await .is_ok()); } #[tokio::test] async fn test_historical_schema_support_sentiment() { let config = BenzingaConfig { api_key: "test-key".to_string(), ..Default::default() }; let _provider = BenzingaHistoricalProvider::new(config).unwrap(); // BenzingaHistoricalProvider doesn't have a get_sentiment method // Sentiment data is included in NewsEvent.sentiment and SentimentEvent // This test removed as the method doesn't exist } #[tokio::test] async fn test_historical_schema_unsupported_trades() { let config = BenzingaConfig { api_key: "test-key".to_string(), ..Default::default() }; let _provider = BenzingaHistoricalProvider::new(config).unwrap(); // BenzingaHistoricalProvider doesn't support trade data - only news and sentiment // This is expected behavior as Benzinga is a news/sentiment provider } #[tokio::test] async fn test_time_range_for_news() { let range = TimeRange::last_days(1); assert!(range.end > range.start); assert_eq!((range.end - range.start).num_hours(), 24); } #[tokio::test] async fn test_news_event_with_url() { let event = NewsEvent { symbol: Some(Symbol::from("MSFT")), symbols: vec![Symbol::from("MSFT")], story_id: "news-456".to_string(), headline: "Microsoft partnership announced".to_string(), content: "Strategic partnership details".to_string(), summary: "Strategic partnership details".to_string(), category: "Technology".to_string(), tags: vec!["Technology".to_string()], impact_score: Some(0.65), importance: 0.7, author: "Reporter".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "https://benzinga.com/news/456".to_string(), sentiment_score: Some(0.7), sentiment: Some(0.7), event_type: NewsEventType::News, }; assert!(!event.url.is_empty()); assert!(event.url.starts_with("https://")); } #[tokio::test] async fn test_sentiment_confidence_levels() { let confidence_levels = vec![0.5, 0.75, 0.9, 0.95, 0.99]; for confidence in confidence_levels { let event = SentimentEvent { symbol: Symbol::from("TEST"), sentiment_score: 0.5, bullish_ratio: 0.6, bearish_ratio: 0.4, sample_size: 20, sources: vec!["benzinga".to_string()], confidence, timestamp: Utc::now(), source: "Benzinga".to_string(), period: SentimentPeriod::Day1, }; assert!( event.confidence >= 0.0 && event.confidence <= 1.0, "Confidence should be valid probability" ); } } #[tokio::test] async fn test_volume_weighted_sentiment() { let event1 = SentimentEvent { symbol: Symbol::from("TEST"), sentiment_score: 0.6, bullish_ratio: 0.7, bearish_ratio: 0.3, sample_size: 25, sources: vec!["benzinga".to_string()], confidence: 0.8, period: SentimentPeriod::Day1, timestamp: Utc::now(), source: "Benzinga".to_string(), }; let event2 = SentimentEvent { ..event1.clone() }; assert_eq!(event1.sentiment_score, event2.sentiment_score); } #[tokio::test] async fn test_positive_negative_ratio_sum() { let event = SentimentEvent { symbol: Symbol::from("TEST"), sentiment_score: 0.5, bullish_ratio: 0.6, bearish_ratio: 0.4, sample_size: 100, sources: vec!["benzinga".to_string()], confidence: 0.8, period: SentimentPeriod::Day1, timestamp: Utc::now(), source: "Benzinga".to_string(), }; let sum = event.bullish_ratio + event.bearish_ratio; assert!( (sum - 1.0).abs() < 0.01, "Bullish and bearish ratios should sum to ~1.0" ); } #[tokio::test] async fn test_provider_name_consistency() { let streaming_config = BenzingaStreamingConfig { api_key: "test-key".to_string(), ..Default::default() }; let historical_config = BenzingaConfig { api_key: "test-key".to_string(), ..Default::default() }; let streaming = BenzingaStreamingProvider::new(streaming_config).unwrap(); let historical = BenzingaHistoricalProvider::new(historical_config).unwrap(); // Provider name comparison removed - method not available // assert_eq!(streaming.get_provider_name(), historical.get_provider_name()); } #[tokio::test] async fn test_error_handling_malformed_data() { // Test that providers handle malformed data gracefully let config = BenzingaConfig { api_key: "test-key".to_string(), ..Default::default() }; let provider = BenzingaHistoricalProvider::new(config); assert!(provider.is_ok()); } #[tokio::test] async fn test_rate_limiting_configuration() { let config = BenzingaStreamingConfig { api_key: "test-key".to_string(), ..Default::default() }; assert!(!config.api_key.is_empty()); } #[tokio::test] async fn test_caching_configuration() { let config = BenzingaConfig { api_key: "test-key".to_string(), ..Default::default() }; assert!(!config.api_key.is_empty()); } #[tokio::test] async fn test_bulk_download_configuration() { let config = BenzingaConfig { api_key: "test-key".to_string(), ..Default::default() }; assert!(!config.api_key.is_empty()); } #[tokio::test] async fn test_news_count_in_sentiment() { let event = SentimentEvent { symbol: Symbol::from("AAPL"), sentiment_score: 0.65, bullish_ratio: 0.7, bearish_ratio: 0.3, sample_size: 50, sources: vec!["benzinga".to_string()], confidence: 0.85, period: SentimentPeriod::Day1, timestamp: Utc::now(), source: "Benzinga".to_string(), }; assert_eq!(event.sample_size, 50); assert!(event.sample_size > 0); } #[tokio::test] async fn test_multiple_symbol_news_fetch() { let config = BenzingaConfig { api_key: "test-key".to_string(), ..Default::default() }; let provider = BenzingaHistoricalProvider::new(config).unwrap(); let symbols_str: Vec<&str> = vec!["AAPL", "MSFT", "GOOGL"]; let range = TimeRange::last_days(1); // Should support batch fetching // fetch_batch method not available, using get_news_events instead let result = provider .get_news_events(Some(&symbols_str), range.start, range.end) .await; assert!(result.is_ok() || result.is_err()); } #[tokio::test] async fn test_event_deduplication() { // Test that identical events can be identified let event1 = NewsEvent { story_id: "news-789".to_string(), symbol: Some(Symbol::from("TSLA")), symbols: vec![Symbol::from("TSLA")], headline: "Tesla updates".to_string(), content: "Tesla updates content".to_string(), summary: "".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "".to_string(), event_type: NewsEventType::News, impact_score: Some(0.0), category: "".to_string(), tags: vec![], importance: 0.0, author: "".to_string(), sentiment_score: Some(0.0), sentiment: Some(0.0), }; let event2 = event1.clone(); assert_eq!(event1.story_id, event2.story_id); assert_eq!(event1.symbol, event2.symbol); }