#![cfg(feature = "rss")]
use finance_query::feeds::{FeedEntry, FeedSource};
#[allow(dead_code)]
fn _verify_feed_entry_fields(e: FeedEntry) {
let _: String = e.title;
let _: String = e.url;
let _: Option<String> = e.published;
let _: Option<String> = e.summary;
let _: String = e.source;
}
#[test]
fn test_feed_source_urls_are_non_empty() {
let sources = [
FeedSource::FederalReserve,
FeedSource::SecPressReleases,
FeedSource::SecFilings("10-K".to_string()),
FeedSource::MarketWatch,
FeedSource::Cnbc,
FeedSource::Bloomberg,
FeedSource::FinancialTimes,
FeedSource::NytBusiness,
FeedSource::GuardianBusiness,
FeedSource::Investing,
FeedSource::Bea,
FeedSource::Ecb,
FeedSource::Cfpb,
FeedSource::WsjMarkets,
FeedSource::Fortune,
FeedSource::BusinessWire,
FeedSource::CoinDesk,
FeedSource::CoinTelegraph,
FeedSource::TechCrunch,
FeedSource::HackerNews,
FeedSource::OilPrice,
FeedSource::CalculatedRisk,
FeedSource::Scmp,
FeedSource::NikkeiAsia,
FeedSource::BankOfEngland,
FeedSource::VentureBeat,
FeedSource::YCombinator,
FeedSource::TheEconomist,
FeedSource::FinancialPost,
FeedSource::FtLex,
FeedSource::RitholtzBigPicture,
FeedSource::Custom("https://example.com/feed.xml".to_string()),
];
for source in &sources {
let url = source.url();
assert!(
!url.is_empty(),
"URL should not be empty for {:?}",
source.name()
);
assert!(
url.starts_with("http://") || url.starts_with("https://"),
"URL should be http(s): {} for {}",
url,
source.name()
);
}
}
#[test]
fn test_feed_source_names_are_non_empty() {
let sources = [
FeedSource::FederalReserve,
FeedSource::SecPressReleases,
FeedSource::SecFilings("8-K".to_string()),
FeedSource::MarketWatch,
FeedSource::Bloomberg,
FeedSource::WsjMarkets,
FeedSource::CoinDesk,
FeedSource::TechCrunch,
FeedSource::Custom("https://example.com/feed.xml".to_string()),
];
for source in &sources {
let name = source.name();
assert!(!name.is_empty(), "name should not be empty");
}
}
#[test]
fn test_sec_filings_url_contains_form_type() {
let source = FeedSource::SecFilings("10-K".to_string());
let url = source.url();
assert!(
url.contains("10-K"),
"SEC filings URL should contain the form type"
);
}
#[test]
fn test_custom_source_url_is_passthrough() {
let custom_url = "https://example.com/custom-feed.rss";
let source = FeedSource::Custom(custom_url.to_string());
assert_eq!(source.url(), custom_url);
}
#[tokio::test]
#[ignore = "requires network access"]
async fn test_fetch_federal_reserve() {
use finance_query::feeds;
let entries = feeds::fetch(FeedSource::FederalReserve).await.unwrap();
println!("Federal Reserve entries: {}", entries.len());
for entry in entries.iter().take(3) {
println!(
"[{}] {}: {}",
entry.source,
entry.published.as_deref().unwrap_or("?"),
entry.title
);
assert!(!entry.title.is_empty());
assert!(!entry.url.is_empty());
assert_eq!(entry.source, "Federal Reserve");
}
}
#[tokio::test]
#[ignore = "requires network access"]
async fn test_fetch_all_deduplicates() {
use finance_query::feeds;
use std::collections::HashSet;
let news = feeds::fetch_all(&[
FeedSource::FederalReserve,
FeedSource::SecPressReleases,
FeedSource::MarketWatch,
])
.await
.unwrap();
let urls: HashSet<&str> = news.iter().map(|e| e.url.as_str()).collect();
assert_eq!(
urls.len(),
news.len(),
"fetch_all should deduplicate by URL"
);
println!("Aggregated entries (deduplicated): {}", news.len());
}
#[tokio::test]
#[ignore = "requires network access"]
async fn test_fetch_single_feed_iteration() {
use finance_query::feeds;
let fed_news = feeds::fetch(FeedSource::FederalReserve).await.unwrap();
for entry in fed_news.iter().take(5) {
println!(
"{}: {}",
entry.published.as_deref().unwrap_or("?"),
entry.title
);
if let Some(_url) = entry.url.as_str().chars().next() {
println!(" {}", entry.url);
}
}
assert!(!fed_news.is_empty());
}
#[tokio::test]
#[ignore = "requires network access"]
async fn test_fetch_all_five_sources() {
use finance_query::feeds;
let news = feeds::fetch_all(&[
FeedSource::FederalReserve,
FeedSource::SecPressReleases,
FeedSource::MarketWatch,
FeedSource::Bloomberg,
FeedSource::WsjMarkets,
])
.await
.unwrap();
println!("Total entries (deduplicated): {}", news.len());
for entry in news.iter().take(10) {
println!(
"[{}] {}: {}",
entry.source,
entry.published.as_deref().unwrap_or("?"),
entry.title
);
}
assert!(!news.is_empty());
}
#[tokio::test]
#[ignore = "requires network access"]
async fn test_custom_feed_url() {
use finance_query::feeds;
let fed_url = FeedSource::FederalReserve.url().to_string();
let custom = feeds::fetch(FeedSource::Custom(fed_url)).await.unwrap();
assert!(!custom.is_empty());
}
#[tokio::test]
#[ignore = "requires network access"]
async fn test_fetch_sec_filings_10k() {
use finance_query::{FinanceError, feeds};
let result = feeds::fetch(FeedSource::SecFilings("10-K".to_string())).await;
let filings = match result {
Ok(f) => f,
Err(FinanceError::FeedParseError { .. }) => {
eprintln!(
"SEC filings feed skipped: likely missing EDGAR_EMAIL env var (SEC requires email in User-Agent)"
);
return;
}
Err(FinanceError::HttpError(ref e)) if e.is_connect() || e.is_timeout() => {
eprintln!("SEC filings feed skipped: connection error");
return;
}
Err(e) => panic!("unexpected error: {:?}", e),
};
println!("SEC 10-K filings: {}", filings.len());
for f in filings.iter().take(3) {
println!("{}: {}", f.published.as_deref().unwrap_or("?"), f.title);
println!(" {}", f.url);
}
assert!(!filings.is_empty());
}