//! Benzinga News Provider Streaming and Integration Tests //! //! Comprehensive tests for Benzinga news feed covering: //! - News article processing and parsing //! - Rate limiting and throttling //! - Real-time streaming //! - News sentiment analysis integration #![allow(unused_crate_dependencies)] use std::collections::HashMap; use chrono::{Duration, Utc}; use common::types::Symbol; use data::error::DataError; use data::providers::common::{NewsEvent, NewsEventType}; // ============================================================================ // News Article Processing Tests // ============================================================================ #[test] fn test_benzinga_news_article_structure() { let article = NewsEvent { symbol: Some(Symbol::from("AAPL")), symbols: vec![Symbol::from("AAPL")], story_id: "BZ123456".to_string(), headline: "Apple Announces New Product".to_string(), content: "Apple Inc. announced...".to_string(), summary: "Apple announces new product".to_string(), category: "News".to_string(), tags: vec!["tech".to_string(), "apple".to_string()], impact_score: Some(0.8), importance: 0.9, author: "Benzinga Staff".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "https://benzinga.com/news/123456".to_string(), sentiment_score: Some(0.6), #[allow(deprecated)] sentiment: Some(0.6), event_type: NewsEventType::News, }; assert_eq!(article.source, "Benzinga"); assert!(article.symbol.is_some()); assert!(!article.headline.is_empty()); } #[test] fn test_benzinga_news_empty_fields() { let article = NewsEvent { symbol: None, symbols: vec![], story_id: "".to_string(), headline: "".to_string(), content: "".to_string(), summary: "".to_string(), category: "".to_string(), tags: vec![], impact_score: None, importance: 0.0, author: "".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "".to_string(), sentiment_score: None, #[allow(deprecated)] sentiment: None, event_type: NewsEventType::News, }; assert!(article.symbol.is_none()); assert!(article.headline.is_empty()); assert!(article.content.is_empty()); } #[test] fn test_benzinga_news_with_symbol() { let article = NewsEvent { symbol: Some(Symbol::from("AAPL")), symbols: vec![Symbol::from("AAPL")], story_id: "BZ123457".to_string(), headline: "Tech Giants Partnership".to_string(), content: "Major tech companies announce partnership...".to_string(), summary: "Partnership announced".to_string(), category: "News".to_string(), tags: vec![], impact_score: None, importance: 0.5, author: "Benzinga Staff".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "https://benzinga.com/news/123457".to_string(), sentiment_score: None, #[allow(deprecated)] sentiment: None, event_type: NewsEventType::News, }; assert!(article.symbol.is_some()); assert_eq!(article.symbol.unwrap().as_str(), "AAPL"); } // ============================================================================ // Earnings Event Tests // ============================================================================ #[test] fn test_benzinga_earnings_event() { let earnings = NewsEvent { symbol: Some(Symbol::from("AAPL")), symbols: vec![Symbol::from("AAPL")], story_id: "BZ123458".to_string(), headline: "Apple Q4 Earnings Beat Estimates".to_string(), content: "Apple reports strong Q4... EPS: $2.75 (est: $2.50)".to_string(), summary: "Apple beats earnings estimates".to_string(), category: "Earnings".to_string(), tags: vec!["earnings".to_string()], impact_score: Some(0.9), importance: 0.95, author: "Benzinga Staff".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "https://benzinga.com/news/123458".to_string(), sentiment_score: Some(0.75), #[allow(deprecated)] sentiment: Some(0.75), event_type: NewsEventType::Earnings, }; assert!(earnings.content.contains("EPS")); assert!(earnings.content.contains("$2.75")); } #[test] fn test_benzinga_earnings_surprise() { // Test calculation logic (standalone, not tied to NewsEvent structure) let eps_estimate = 1.50; let eps_actual = 2.00; let surprise: f64 = ((eps_actual - eps_estimate) / eps_estimate) * 100.0; assert!(surprise > 0.0); // Positive surprise assert_eq!(surprise.round(), 33.0); // 33% surprise } // ============================================================================ // Analyst Rating Tests // ============================================================================ #[test] fn test_benzinga_analyst_rating_upgrade() { let rating = NewsEvent { symbol: Some(Symbol::from("TSLA")), symbols: vec![Symbol::from("TSLA")], story_id: "BZ123459".to_string(), headline: "Goldman Sachs Upgrades Tesla".to_string(), content: "Goldman Sachs upgraded Tesla from hold to buy".to_string(), summary: "Tesla upgraded to buy".to_string(), category: "Analyst Rating".to_string(), tags: vec!["upgrade".to_string()], impact_score: Some(0.85), importance: 0.9, author: "Benzinga Staff".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "https://benzinga.com/news/123459".to_string(), sentiment_score: Some(0.8), #[allow(deprecated)] sentiment: Some(0.8), event_type: NewsEventType::Rating, }; assert!(rating.content.contains("upgraded")); assert!(rating.content.contains("buy")); } #[test] fn test_benzinga_analyst_rating_downgrade() { let rating = NewsEvent { symbol: Some(Symbol::from("NFLX")), symbols: vec![Symbol::from("NFLX")], story_id: "BZ123460".to_string(), headline: "Netflix Downgraded on Subscriber Concerns".to_string(), content: "Downgraded from buy to sell".to_string(), summary: "Netflix downgraded to sell".to_string(), category: "Analyst Rating".to_string(), tags: vec!["downgrade".to_string()], impact_score: Some(0.85), importance: 0.9, author: "Benzinga Staff".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "https://benzinga.com/news/123460".to_string(), sentiment_score: Some(0.2), #[allow(deprecated)] sentiment: Some(0.2), event_type: NewsEventType::Rating, }; assert!(rating.content.contains("Downgraded")); assert!(rating.content.contains("sell")); } // ============================================================================ // Economic Event Tests // ============================================================================ #[test] fn test_benzinga_economic_calendar_event() { let economic = NewsEvent { symbol: None, symbols: vec![], story_id: "BZ123461".to_string(), headline: "Federal Reserve FOMC Meeting".to_string(), content: "Federal Reserve to announce interest rate decision... High importance event for USA".to_string(), summary: "FOMC meeting scheduled".to_string(), category: "Economic".to_string(), tags: vec!["fed".to_string(), "fomc".to_string()], impact_score: Some(0.95), importance: 1.0, author: "Benzinga Staff".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "https://benzinga.com/news/123461".to_string(), sentiment_score: None, #[allow(deprecated)] sentiment: None, event_type: NewsEventType::Economic, }; assert!(economic.content.contains("Federal Reserve")); assert!(economic.content.contains("High importance")); } #[test] fn test_benzinga_economic_data_release() { let data = NewsEvent { symbol: None, symbols: vec![], story_id: "BZ123462".to_string(), headline: "Consumer Price Index Exceeds Expectations".to_string(), content: "CPI: Expected 3.5%, Actual 3.8%".to_string(), summary: "CPI exceeds expectations".to_string(), category: "Economic".to_string(), tags: vec!["cpi".to_string(), "inflation".to_string()], impact_score: Some(0.9), importance: 0.95, author: "Benzinga Staff".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "https://benzinga.com/news/123462".to_string(), sentiment_score: Some(0.3), #[allow(deprecated)] sentiment: Some(0.3), event_type: NewsEventType::Economic, }; assert!(data.content.contains("CPI")); assert!(data.content.contains("3.8%")); } // ============================================================================ // Rate Limiting Tests // ============================================================================ #[test] fn test_benzinga_rate_limit_error() { let rate_limit_err = DataError::RateLimit; assert!(rate_limit_err.is_retryable()); assert_eq!(rate_limit_err.category(), "RATE_LIMIT"); } #[test] fn test_benzinga_rate_limit_throttling() { let requests_per_minute = 60; let current_requests = 55; let remaining = requests_per_minute - current_requests; let should_throttle = remaining <= 5; assert!(should_throttle); } #[test] fn test_benzinga_rate_limit_backoff() { let base_delay_ms = 1000; let attempts = vec![0, 1, 2, 3, 4]; for attempt in attempts { let delay = base_delay_ms * 2_u64.pow(attempt); let capped_delay = delay.min(60_000); assert!(capped_delay >= base_delay_ms); assert!(capped_delay <= 60_000); } } // ============================================================================ // API Error Handling Tests // ============================================================================ #[test] fn test_benzinga_api_error_responses() { let api_errors = vec![ ("400", "Bad Request"), ("401", "Unauthorized"), ("403", "Forbidden"), ("404", "Not Found"), ("429", "Too Many Requests"), ("500", "Internal Server Error"), ("503", "Service Unavailable"), ]; for (status, message) in api_errors { let err = DataError::api(message, Some(status)); assert!(matches!(err, DataError::Api { .. })); assert_eq!(err.category(), "API"); } } #[test] fn test_benzinga_authentication_error() { let auth_err = DataError::authentication("Invalid API token"); assert!(matches!(auth_err, DataError::Authentication { .. })); assert_eq!(auth_err.severity(), data::error::ErrorSeverity::Critical); } // ============================================================================ // Symbol Validation Tests // ============================================================================ #[test] fn test_benzinga_symbol_validation() { let too_long = "TOOLONG".repeat(100); let invalid_symbols = vec![ "", " ", "\n", &too_long, "!@#$%", "symbol with spaces", ]; for symbol in invalid_symbols { let is_valid = !symbol.is_empty() && symbol.len() <= 20 && symbol.trim() == symbol && symbol .chars() .all(|c| c.is_alphanumeric() || c == '.' || c == '-'); assert!(!is_valid); } } #[test] fn test_benzinga_symbol_normalization() { let symbols = vec![ ("aapl", "AAPL"), ("tsla", "TSLA"), ("brk.b", "BRK.B"), ]; for (input, expected) in symbols { let normalized = input.to_uppercase(); assert_eq!(normalized, expected); } } // ============================================================================ // News Filtering Tests // ============================================================================ #[test] fn test_benzinga_news_category_filtering() { let categories = vec!["earnings", "analyst_rating", "merger", "ipo"]; let article = NewsEvent { symbol: Some(Symbol::from("AAPL")), symbols: vec![Symbol::from("AAPL")], story_id: "BZ123463".to_string(), headline: "Test Earnings Release".to_string(), content: "earnings release content".to_string(), summary: "Earnings release".to_string(), category: "Earnings".to_string(), tags: vec!["earnings".to_string()], impact_score: Some(0.7), importance: 0.8, author: "Benzinga Staff".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "https://benzinga.com/news/123463".to_string(), sentiment_score: Some(0.5), #[allow(deprecated)] sentiment: Some(0.5), event_type: NewsEventType::Earnings, }; // Simulate category filtering based on content let should_include = categories.iter().any(|cat| article.content.contains(cat)); assert!(should_include); } #[test] fn test_benzinga_news_importance_filtering() { let min_importance = 0.7; // Simulate importance score (would come from content analysis in real implementation) let article_importance = 0.8; let should_include = article_importance >= min_importance; assert!(should_include); } // ============================================================================ // News Deduplication Tests // ============================================================================ #[test] fn test_benzinga_news_deduplication() { let mut seen_ids: std::collections::HashSet = std::collections::HashSet::new(); let event_ids = vec![ "news_1", "news_2", "news_3", "news_2", "news_4", "news_3", ]; let mut unique_count = 0; for id in event_ids { if seen_ids.insert(id.to_string()) { unique_count += 1; } } assert_eq!(unique_count, 4); // Only 4 unique IDs } // ============================================================================ // Timestamp Validation Tests // ============================================================================ #[test] fn test_benzinga_news_timestamp_validation() { let now = Utc::now(); let old_news = now - Duration::hours(24); let future_news = now + Duration::hours(1); let max_age = Duration::hours(6); let is_old = (now - old_news) > max_age; let is_future = future_news > now; assert!(is_old); assert!(is_future); } #[test] fn test_benzinga_news_timestamp_ordering() { let events = vec![ Utc::now() - Duration::hours(3), Utc::now() - Duration::hours(2), Utc::now() - Duration::hours(1), Utc::now(), ]; for i in 1..events.len() { assert!(events[i] > events[i - 1]); } } // ============================================================================ // Content Processing Tests // ============================================================================ #[test] fn test_benzinga_news_content_sanitization() { let raw_content = "Apple announces new product!\n\nStock up 5%"; let sanitized = raw_content.replace("", "").replace("", ""); assert!(!sanitized.contains("")); assert!(!sanitized.contains("")); } #[test] fn test_benzinga_news_content_truncation() { let long_content = "a".repeat(10000); let max_length = 5000; let truncated = if long_content.len() > max_length { &long_content[..max_length] } else { &long_content }; assert_eq!(truncated.len(), max_length); } // ============================================================================ // Streaming Integration Tests // ============================================================================ #[tokio::test] async fn test_benzinga_streaming_event_processing() { use tokio::task; let handles: Vec<_> = (0..10) .map(|i| { task::spawn(async move { let event = NewsEvent { symbol: Some(Symbol::from("AAPL")), symbols: vec![Symbol::from("AAPL")], story_id: format!("BZ12346{}", i), headline: format!("Test Article {}", i), content: format!("Content for article {}", i), summary: format!("Article {}", i), category: "News".to_string(), tags: vec![], impact_score: None, importance: 0.5, author: "Benzinga Staff".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: format!("https://benzinga.com/news/12346{}", i), sentiment_score: None, #[allow(deprecated)] sentiment: None, event_type: NewsEventType::News, }; event }) }) .collect(); for handle in handles { let event = handle.await.unwrap(); assert!(!event.headline.is_empty()); } } // ============================================================================ // Content Analysis Tests // ============================================================================ #[test] fn test_benzinga_sentiment_analysis() { // Simulate sentiment score extraction from content let content = "sentiment_score: 0.75, impact_score: 0.85"; let sentiment = 0.75; // Would be extracted from content in real implementation let impact = 0.85; assert!(sentiment > 0.0 && sentiment <= 1.0); assert!(impact > 0.0 && impact <= 1.0); } #[test] fn test_benzinga_content_missing_metadata() { let content = ""; // No metadata in content let sentiment = 0.0; // Default value when not found assert_eq!(sentiment, 0.0); } // ============================================================================ // Tag Processing Tests // ============================================================================ #[test] fn test_benzinga_tag_extraction() { let tags = vec![ "earnings", "technology", "ai", "quarterly_results", "revenue_growth", ]; let relevant_tags: Vec<&str> = tags .iter() .filter(|t| { t.contains("earnings") || t.contains("revenue") || t.contains("growth") }) .copied() .collect(); assert_eq!(relevant_tags.len(), 2); // earnings and revenue_growth } // ============================================================================ // Error Recovery Tests // ============================================================================ #[test] fn test_benzinga_error_recovery() { let mut attempt = 0; let max_attempts = 3; let result = loop { attempt += 1; let err = DataError::network("Connection timeout"); if err.is_retryable() && attempt < max_attempts { continue; } break if attempt < max_attempts { Ok(()) } else { Err(err) }; }; assert!(result.is_err()); } // ============================================================================ // Configuration Tests // ============================================================================ #[test] fn test_benzinga_config_validation() { let config = HashMap::from([ ("api_token", "valid_token_12345"), ("max_events_per_request", "100"), ("update_interval_seconds", "60"), ]); assert!(!config["api_token"].is_empty()); assert!(config["max_events_per_request"].parse::().is_ok()); } // ============================================================================ // Serialization Tests // ============================================================================ #[test] fn test_benzinga_news_event_serialization() { use serde_json; let event = NewsEvent { symbol: Some(Symbol::from("AAPL")), symbols: vec![Symbol::from("AAPL")], story_id: "BZ123499".to_string(), headline: "Test".to_string(), content: "Test content".to_string(), summary: "Test summary".to_string(), category: "News".to_string(), tags: vec![], impact_score: None, importance: 0.5, author: "Benzinga Staff".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Benzinga".to_string(), url: "https://benzinga.com/news/123499".to_string(), sentiment_score: None, #[allow(deprecated)] sentiment: None, event_type: NewsEventType::News, }; let json = serde_json::to_string(&event).unwrap(); let deserialized: NewsEvent = serde_json::from_str(&json).unwrap(); assert_eq!(event.headline, deserialized.headline); assert_eq!(event.content, deserialized.content); assert_eq!(event.source, deserialized.source); }