//! Comprehensive tests for BenzingaHistoricalProvider //! //! This module contains extensive tests for the Benzinga news provider, //! covering news processing, sentiment analysis, analyst ratings, earnings events, //! economic indicators, rate limiting, and error handling. use chrono::{DateTime, Duration as ChronoDuration, Utc}; use data::error::{DataError, Result}; use data::providers::benzinga::{ BenzingaChannel, BenzingaConfig, BenzingaEarnings, BenzingaEconomicEvent, BenzingaHistoricalProvider, BenzingaNewsArticle, BenzingaRating, BenzingaTag, NewsEvent, NewsEventType, OptionsContract, OptionsSentiment, OptionsType, RatingAction, SentimentPeriod, UnusualOptionsEvent, UnusualOptionsType, }; use rust_decimal_macros::dec; use serde_json::json; use std::collections::HashMap; use std::time::Duration; use tokio::time::{sleep, timeout}; use tokio_test; use rust_decimal::Decimal; use common::Symbol; use wiremock::matchers::{method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; /// Test BenzingaConfig creation and defaults #[test] fn test_config_creation_defaults() { let config = BenzingaConfig::default(); assert_eq!(config.base_url, "https://api.benzinga.com/api/v2"); assert_eq!(config.timeout_seconds, 30); assert_eq!(config.rate_limit, 5); assert_eq!(config.max_retries, 3); assert_eq!(config.retry_delay_ms, 1000); } /// Test BenzingaConfig creation with custom values #[test] fn test_config_creation_custom() { let config = BenzingaConfig { api_key: "test-api-key".to_string(), base_url: "https://custom-api.example.com".to_string(), timeout_seconds: 60, rate_limit: 10, max_retries: 5, retry_delay_ms: 2000, }; assert_eq!(config.api_key, "test-api-key"); assert_eq!(config.base_url, "https://custom-api.example.com"); assert_eq!(config.timeout_seconds, 60); assert_eq!(config.rate_limit, 10); assert_eq!(config.max_retries, 5); assert_eq!(config.retry_delay_ms, 2000); } /// Test BenzingaConfig serialization #[test] fn test_config_serialization() { let config = BenzingaConfig { api_key: "test-key".to_string(), ..Default::default() }; let json = serde_json::to_string(&config); assert!(json.is_ok()); let deserialized: Result = serde_json::from_str(&json.unwrap()); assert!(deserialized.is_ok()); let deserialized_config = deserialized.unwrap(); assert_eq!(deserialized_config.api_key, config.api_key); assert_eq!(deserialized_config.base_url, config.base_url); } /// Test provider creation success #[tokio::test] async fn test_provider_creation_success() { let config = BenzingaConfig { api_key: "test-api-key".to_string(), ..Default::default() }; let provider = BenzingaHistoricalProvider::new(config); assert!(provider.is_ok()); } /// Test provider creation with invalid timeout #[tokio::test] async fn test_provider_creation_zero_timeout() { let config = BenzingaConfig { api_key: "test-api-key".to_string(), timeout_seconds: 0, ..Default::default() }; let provider = BenzingaHistoricalProvider::new(config); // Should still create successfully, validation happens during requests assert!(provider.is_ok()); } /// Test news article conversion to news event #[test] fn test_news_article_conversion() { let config = BenzingaConfig::default(); let provider = BenzingaHistoricalProvider::new(config).unwrap(); let article = BenzingaNewsArticle { id: 12345, title: "Apple Reports Strong Q4 Earnings".to_string(), body: "Apple Inc. (AAPL) reported strong Q4 earnings with revenue beating estimates..." .to_string(), author: Some("Jane Doe".to_string()), created: Utc::now(), updated: Utc::now(), url: "https://example.com/article/12345".to_string(), image: Some("https://example.com/image.jpg".to_string()), symbols: vec!["AAPL".to_string()], channels: vec![BenzingaChannel { id: 1, name: "News".to_string(), }], tags: vec![ BenzingaTag { id: 1, name: "Earnings".to_string(), }, BenzingaTag { id: 2, name: "Breaking".to_string(), }, ], sentiment: Some(0.75), }; let news_event = provider.convert_news_article(article.clone()); assert_eq!(news_event.id, format!("benzinga_news_{}", article.id)); assert_eq!(news_event.event_type, NewsEventType::News); assert_eq!(news_event.symbols, article.symbols); assert_eq!(news_event.title, article.title); assert_eq!(news_event.content, article.body); assert_eq!(news_event.sentiment, Some(0.75)); assert_eq!(news_event.importance, 0.8); // Breaking news gets higher importance assert_eq!(news_event.source, "Benzinga News"); assert!(news_event.categories.contains(&"Earnings".to_string())); assert!(news_event.categories.contains(&"Breaking".to_string())); // Check metadata assert_eq!( news_event.metadata.get("article_id"), Some(&"12345".to_string()) ); assert_eq!(news_event.metadata.get("url"), Some(&article.url)); assert_eq!( news_event.metadata.get("author"), Some(&"Jane Doe".to_string()) ); assert_eq!( news_event.metadata.get("image_url"), Some(&"https://example.com/image.jpg".to_string()) ); } /// Test news article conversion without breaking tags #[test] fn test_news_article_conversion_non_breaking() { let config = BenzingaConfig::default(); let provider = BenzingaHistoricalProvider::new(config).unwrap(); let article = BenzingaNewsArticle { id: 67890, title: "Regular News Article".to_string(), body: "This is regular news content...".to_string(), author: None, created: Utc::now(), updated: Utc::now(), url: "https://example.com/article/67890".to_string(), image: None, symbols: vec!["SPY".to_string()], channels: vec![], tags: vec![BenzingaTag { id: 3, name: "Market Update".to_string(), }], sentiment: None, }; let news_event = provider.convert_news_article(article); assert_eq!(news_event.importance, 0.5); // Non-breaking news gets standard importance assert_eq!(news_event.sentiment, None); assert!(!news_event.metadata.contains_key("author")); assert!(!news_event.metadata.contains_key("image_url")); } /// Test earnings event conversion #[test] fn test_earnings_event_conversion() { let config = BenzingaConfig::default(); let provider = BenzingaHistoricalProvider::new(config).unwrap(); let earnings = BenzingaEarnings { id: 54321, ticker: "TSLA".to_string(), name: "Tesla Inc.".to_string(), date: Utc::now(), period: "Q4".to_string(), period_year: 2023, eps_est: Some(0.85), eps: Some(0.95), eps_surprise: Some(0.10), revenue_est: Some(25_000_000_000.0), revenue: Some(26_500_000_000.0), revenue_surprise: Some(1_500_000_000.0), time: Some("AMC".to_string()), importance: Some(4), }; let news_event = provider.convert_earnings_event(earnings.clone()); assert_eq!(news_event.id, format!("benzinga_earnings_{}", earnings.id)); assert_eq!(news_event.event_type, NewsEventType::Earnings); assert_eq!(news_event.symbols, vec![earnings.ticker]); assert_eq!(news_event.title, format!("Earnings: {}", earnings.name)); assert_eq!(news_event.importance, 0.8); // 4/5 importance assert_eq!(news_event.source, "Benzinga Earnings"); assert!(news_event.categories.contains(&"Earnings".to_string())); // Check content format assert!(news_event.content.contains("Tesla Inc.")); assert!(news_event.content.contains("TSLA")); assert!(news_event.content.contains("Q4 2023")); // Check metadata assert_eq!( news_event.metadata.get("earnings_id"), Some(&"54321".to_string()) ); assert_eq!(news_event.metadata.get("period"), Some(&"Q4".to_string())); assert_eq!( news_event.metadata.get("period_year"), Some(&"2023".to_string()) ); assert_eq!( news_event.metadata.get("earnings_time"), Some(&"AMC".to_string()) ); assert_eq!( news_event.metadata.get("eps_estimate"), Some(&"0.85".to_string()) ); assert_eq!( news_event.metadata.get("eps_actual"), Some(&"0.95".to_string()) ); } /// Test rating event conversion with upgrade #[test] fn test_rating_event_conversion_upgrade() { let config = BenzingaConfig::default(); let provider = BenzingaHistoricalProvider::new(config).unwrap(); let rating = BenzingaRating { id: 98765, ticker: "NVDA".to_string(), name: "NVIDIA Corporation".to_string(), analyst: "Goldman Sachs".to_string(), firm: "Goldman Sachs".to_string(), action: "Upgrades".to_string(), current_rating: "Buy".to_string(), previous_rating: Some("Hold".to_string()), price_target: Some(dec!(450.00)), previous_price_target: Some(dec!(400.00)), comment: Some("Strong AI growth prospects".to_string()), rating_date: Utc::now(), timestamp: Utc::now(), importance: Some(5), }; let news_event = provider.convert_rating_event(rating.clone()); assert_eq!(news_event.id, format!("benzinga_rating_{}", rating.id)); assert_eq!(news_event.event_type, NewsEventType::Rating); assert_eq!(news_event.symbols, vec![rating.ticker]); assert_eq!(news_event.title, "Rating: NVIDIA Corporation - Upgrades"); assert_eq!(news_event.importance, 1.0); // 5/5 importance assert_eq!(news_event.sentiment, Some(0.7)); // Upgrade is positive assert_eq!(news_event.source, "Benzinga Ratings"); assert!(news_event .categories .contains(&"Analyst Rating".to_string())); assert!(news_event.categories.contains(&"Upgrades".to_string())); // Check content format assert!(news_event.content.contains("Goldman Sachs")); assert!(news_event.content.contains("Upgrades")); assert!(news_event.content.contains("NVIDIA Corporation")); // Check metadata assert_eq!( news_event.metadata.get("rating_id"), Some(&"98765".to_string()) ); assert_eq!( news_event.metadata.get("analyst"), Some(&"Goldman Sachs".to_string()) ); assert_eq!( news_event.metadata.get("action"), Some(&"Upgrades".to_string()) ); assert_eq!(news_event.metadata.get("rating"), Some(&"Buy".to_string())); assert_eq!( news_event.metadata.get("price_target"), Some(&"450.00".to_string()) ); } /// Test rating event conversion with downgrade #[test] fn test_rating_event_conversion_downgrade() { let config = BenzingaConfig::default(); let provider = BenzingaHistoricalProvider::new(config).unwrap(); let rating = BenzingaRating { id: 13579, ticker: "META".to_string(), name: "Meta Platforms Inc.".to_string(), analyst: "Morgan Stanley".to_string(), firm: "Morgan Stanley".to_string(), action: "Downgrades".to_string(), current_rating: "Hold".to_string(), previous_rating: Some("Buy".to_string()), price_target: Some(dec!(300.00)), previous_price_target: Some(dec!(350.00)), comment: None, rating_date: Utc::now(), timestamp: Utc::now(), importance: Some(3), }; let news_event = provider.convert_rating_event(rating); assert_eq!(news_event.sentiment, Some(-0.7)); // Downgrade is negative assert_eq!(news_event.importance, 0.6); // 3/5 importance } /// Test economic event conversion #[test] fn test_economic_event_conversion() { let config = BenzingaConfig::default(); let provider = BenzingaHistoricalProvider::new(config).unwrap(); let economic = BenzingaEconomicEvent { id: 24680, name: "Non-Farm Payrolls".to_string(), description: Some("Monthly employment data".to_string()), date: Utc::now(), country: "US".to_string(), category: "Employment".to_string(), importance: "High".to_string(), actual: Some("250K".to_string()), consensus: Some("200K".to_string()), previous: Some("180K".to_string()), previous_revised: Some("185K".to_string()), }; let news_event = provider.convert_economic_event(economic.clone()); assert_eq!(news_event.id, format!("benzinga_economic_{}", economic.id)); assert_eq!(news_event.event_type, NewsEventType::Economic); assert!(news_event.symbols.is_empty()); // Economic events don't have specific symbols assert_eq!(news_event.title, "Economic: Non-Farm Payrolls (US)"); assert_eq!(news_event.importance, 0.8); // High importance assert_eq!(news_event.source, "Benzinga Economic"); assert!(news_event.categories.contains(&"Employment".to_string())); assert!(news_event.categories.contains(&"High".to_string())); // Check content format assert!(news_event.content.contains("Non-Farm Payrolls")); assert!(news_event.content.contains("US")); assert!(news_event.content.contains("250K")); assert!(news_event.content.contains("200K")); // Check metadata assert_eq!( news_event.metadata.get("economic_id"), Some(&"24680".to_string()) ); assert_eq!(news_event.metadata.get("country"), Some(&"US".to_string())); assert_eq!( news_event.metadata.get("category"), Some(&"Employment".to_string()) ); assert_eq!( news_event.metadata.get("importance"), Some(&"High".to_string()) ); assert_eq!( news_event.metadata.get("description"), Some(&"Monthly employment data".to_string()) ); assert_eq!(news_event.metadata.get("actual"), Some(&"250K".to_string())); } /// Test rate limiting functionality #[tokio::test] async fn test_rate_limiting() { let config = BenzingaConfig { api_key: "test-key".to_string(), rate_limit: 2, // 2 requests per second ..Default::default() }; let provider = BenzingaHistoricalProvider::new(config).unwrap(); let start = std::time::Instant::now(); // Make 4 requests, should take at least 1.5 seconds with 2 req/sec limit for _ in 0..4 { provider.enforce_rate_limit().await; } let elapsed = start.elapsed(); assert!(elapsed >= Duration::from_millis(1400)); // Allow some margin for timing variations } /// Test rate limiting with high rate limit #[tokio::test] async fn test_rate_limiting_high_rate() { let config = BenzingaConfig { api_key: "test-key".to_string(), rate_limit: 100, // 100 requests per second ..Default::default() }; let provider = BenzingaHistoricalProvider::new(config).unwrap(); let start = std::time::Instant::now(); // Make 5 requests, should be very fast with high rate limit for _ in 0..5 { provider.enforce_rate_limit().await; } let elapsed = start.elapsed(); assert!(elapsed < Duration::from_millis(100)); } /// Test news event type serialization #[test] fn test_news_event_type_serialization() { let types = vec![ NewsEventType::News, NewsEventType::Earnings, NewsEventType::Rating, NewsEventType::Economic, NewsEventType::CorporateAction, ]; for event_type in types { let json = serde_json::to_string(&event_type); assert!(json.is_ok()); let deserialized: Result = serde_json::from_str(&json.unwrap()); assert!(deserialized.is_ok()); assert_eq!(deserialized.unwrap(), event_type); } } /// Test rating action serialization #[test] fn test_rating_action_serialization() { let actions = vec![ RatingAction::Initiate, RatingAction::Upgrade, RatingAction::Downgrade, RatingAction::Maintain, RatingAction::Discontinue, ]; for action in actions { let json = serde_json::to_string(&action); assert!(json.is_ok()); let deserialized: Result = serde_json::from_str(&json.unwrap()); assert!(deserialized.is_ok()); assert_eq!(deserialized.unwrap(), action); } } /// Test sentiment period serialization #[test] fn test_sentiment_period_serialization() { let periods = vec![ SentimentPeriod::RealTime, SentimentPeriod::Hourly, SentimentPeriod::Daily, SentimentPeriod::Weekly, ]; for period in periods { let json = serde_json::to_string(&period); assert!(json.is_ok()); let deserialized: Result = serde_json::from_str(&json.unwrap()); assert!(deserialized.is_ok()); assert_eq!(deserialized.unwrap(), period); } } /// Test unusual options types serialization #[test] fn test_unusual_options_types_serialization() { let types = vec![ UnusualOptionsType::BlockTrade, UnusualOptionsType::Sweep, UnusualOptionsType::VolumeSpike, UnusualOptionsType::OpenInterestSpike, UnusualOptionsType::VolatilitySpike, ]; for opt_type in types { let json = serde_json::to_string(&opt_type); assert!(json.is_ok()); let deserialized: Result = serde_json::from_str(&json.unwrap()); assert!(deserialized.is_ok()); assert_eq!(deserialized.unwrap(), opt_type); } } /// Test options sentiment serialization #[test] fn test_options_sentiment_serialization() { let sentiments = vec![ OptionsSentiment::Bullish, OptionsSentiment::Bearish, OptionsSentiment::Neutral, ]; for sentiment in sentiments { let json = serde_json::to_string(&sentiment); assert!(json.is_ok()); let deserialized: Result = serde_json::from_str(&json.unwrap()); assert!(deserialized.is_ok()); assert_eq!(deserialized.unwrap(), sentiment); } } /// Test options type serialization #[test] fn test_options_type_serialization() { let types = vec![OptionsType::Call, OptionsType::Put]; for opt_type in types { let json = serde_json::to_string(&opt_type); assert!(json.is_ok()); let deserialized: Result = serde_json::from_str(&json.unwrap()); assert!(deserialized.is_ok()); assert_eq!(deserialized.unwrap(), opt_type); } } /// Test options contract serialization #[test] fn test_options_contract_serialization() { let contract = OptionsContract { strike: dec!(150.00), expiration: chrono::NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(), option_type: OptionsType::Call, multiplier: 100, }; let json = serde_json::to_string(&contract); assert!(json.is_ok()); let deserialized: Result = serde_json::from_str(&json.unwrap()); assert!(deserialized.is_ok()); let deserialized_contract = deserialized.unwrap(); assert_eq!(deserialized_contract.strike, contract.strike); assert_eq!(deserialized_contract.expiration, contract.expiration); assert_eq!(deserialized_contract.option_type, contract.option_type); assert_eq!(deserialized_contract.multiplier, contract.multiplier); } /// Test unusual options event serialization #[test] fn test_unusual_options_event_serialization() { let options_event = UnusualOptionsEvent { symbol: Symbol::from("AAPL"), contract: OptionsContract { strike: dec!(160.00), expiration: chrono::NaiveDate::from_ymd_opt(2024, 1, 19).unwrap(), option_type: OptionsType::Call, multiplier: 100, }, activity_type: UnusualOptionsType::Sweep, volume: 5000, open_interest: Some(10000), premium: Some(dec!(250000.00)), implied_volatility: Some(0.35), sentiment: OptionsSentiment::Bullish, confidence: 0.85, description: "Large call sweep near market".to_string(), timestamp: Utc::now(), }; let json = serde_json::to_string(&options_event); assert!(json.is_ok()); let deserialized: Result = serde_json::from_str(&json.unwrap()); assert!(deserialized.is_ok()); let deserialized_event = deserialized.unwrap(); assert_eq!(deserialized_event.symbol, options_event.symbol); assert_eq!( deserialized_event.activity_type, options_event.activity_type ); assert_eq!(deserialized_event.volume, options_event.volume); assert_eq!(deserialized_event.sentiment, options_event.sentiment); assert_eq!(deserialized_event.confidence, options_event.confidence); } /// Test news event complete serialization #[test] fn test_news_event_serialization() { let mut metadata = HashMap::new(); metadata.insert("article_id".to_string(), "12345".to_string()); metadata.insert("author".to_string(), "Test Author".to_string()); let news_event = NewsEvent { story_id: "test_news_123".to_string(), headline: "Tech Stocks Rise".to_string(), content: "Technology stocks showed strong performance today...".to_string(), summary: "Brief summary of tech stock performance".to_string(), symbol: Some(Symbol::from("AAPL")), symbols: vec![Symbol::from("AAPL"), Symbol::from("MSFT")], category: "Technology".to_string(), tags: vec!["Technology".to_string(), "Markets".to_string()], impact_score: Some(0.75), importance: 0.75, author: "Test Author".to_string(), timestamp: Utc::now(), published_at: Utc::now(), source: "Test Source".to_string(), url: "https://example.com/test-news".to_string(), sentiment_score: Some(0.6), sentiment: Some(0.6), event_type: NewsEventType::News, }; let json = serde_json::to_string(&news_event); assert!(json.is_ok()); let deserialized: Result = serde_json::from_str(&json.unwrap()); assert!(deserialized.is_ok()); let deserialized_event = deserialized.unwrap(); assert_eq!(deserialized_event.id, news_event.id); assert_eq!(deserialized_event.event_type, news_event.event_type); assert_eq!(deserialized_event.symbols, news_event.symbols); assert_eq!(deserialized_event.title, news_event.title); assert_eq!(deserialized_event.importance, news_event.importance); assert_eq!(deserialized_event.sentiment, news_event.sentiment); assert_eq!(deserialized_event.metadata.len(), news_event.metadata.len()); } /// Test benzinga article with minimal data #[test] fn test_benzinga_article_minimal() { let article = BenzingaNewsArticle { id: 1, title: "Minimal Article".to_string(), body: "Content".to_string(), author: None, created: Utc::now(), updated: Utc::now(), url: "https://example.com/1".to_string(), image: None, symbols: vec![], channels: vec![], tags: vec![], sentiment: None, }; let json = serde_json::to_string(&article); assert!(json.is_ok()); let deserialized: Result = serde_json::from_str(&json.unwrap()); assert!(deserialized.is_ok()); let config = BenzingaConfig::default(); let provider = BenzingaHistoricalProvider::new(config).unwrap(); let event = provider.convert_news_article(article); assert_eq!(event.importance, 0.5); // Default importance for non-breaking assert!(event.symbols.is_empty()); assert!(event.categories.is_empty()); assert_eq!(event.sentiment, None); } /// Test earnings with minimal data #[test] fn test_earnings_minimal() { let earnings = BenzingaEarnings { id: 1, ticker: "TEST".to_string(), name: "Test Company".to_string(), date: Utc::now(), period: "Q1".to_string(), period_year: 2024, eps_est: None, eps: None, eps_surprise: None, revenue_est: None, revenue: None, revenue_surprise: None, time: None, importance: None, }; let config = BenzingaConfig::default(); let provider = BenzingaHistoricalProvider::new(config).unwrap(); let event = provider.convert_earnings_event(earnings); assert_eq!(event.importance, 0.6); // Default importance (3/5) assert!(!event.metadata.contains_key("earnings_time")); assert!(!event.metadata.contains_key("eps_estimate")); assert!(!event.metadata.contains_key("eps_actual")); } /// Test rating with maintain action #[test] fn test_rating_maintain_action() { let config = BenzingaConfig::default(); let provider = BenzingaHistoricalProvider::new(config).unwrap(); let rating = BenzingaRating { id: 1, ticker: "SPY".to_string(), name: "SPDR S&P 500".to_string(), analyst: "Test Analyst".to_string(), firm: "Test Firm".to_string(), action: "Maintains".to_string(), current_rating: "Buy".to_string(), previous_rating: Some("Buy".to_string()), price_target: None, previous_price_target: None, comment: None, rating_date: Utc::now(), timestamp: Utc::now(), importance: None, }; let event = provider.convert_rating_event(rating); assert_eq!(event.sentiment, None); // Maintain action has no sentiment assert_eq!(event.importance, 0.6); // Default importance (3/5) } /// Test economic event with low importance #[test] fn test_economic_event_low_importance() { let config = BenzingaConfig::default(); let provider = BenzingaHistoricalProvider::new(config).unwrap(); let economic = BenzingaEconomicEvent { id: 1, name: "Minor Indicator".to_string(), description: None, date: Utc::now(), country: "CA".to_string(), category: "Other".to_string(), importance: "Low".to_string(), actual: None, consensus: None, previous: None, previous_revised: None, }; let event = provider.convert_economic_event(economic); assert_eq!(event.importance, 0.2); // Low importance assert!(!event.metadata.contains_key("description")); assert!(!event.metadata.contains_key("actual")); }