use divkit::Divkit;
use tempfile::TempDir;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
const FIXTURE_SHA256: &str = "d0fe742c4c6de9147ed28e8bb85f82949361dbe64851603f1e2385fa1342ddd9";
fn fixture_bytes() -> Vec<u8> {
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
std::fs::read(manifest_dir.join("tests/fixtures/dividends-2024.parquet"))
.expect("fixture parquet must exist — run `cargo test --test parquet_io make_fixture -- --ignored` first")
}
fn manifest_body_with_digest(hex: &str) -> String {
format!(r#"{{"dividends-2024.parquet": "sha256:{hex}"}}"#)
}
fn manifest_body() -> String {
manifest_body_with_digest(FIXTURE_SHA256)
}
#[tokio::test]
async fn annual_dividend_known_ticker() {
let server = MockServer::start().await;
let parquet = fixture_bytes();
Mock::given(method("GET"))
.and(path("/manifest.json"))
.respond_with(ResponseTemplate::new(200).set_body_string(manifest_body()))
.expect(1..)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/dividends-2024.parquet"))
.respond_with(ResponseTemplate::new(200).set_body_bytes(parquet))
.expect(1..)
.mount(&server)
.await;
let cache_dir = TempDir::new().unwrap();
let client = Divkit::new()
.with_base_url(server.uri())
.with_cache_dir(cache_dir.path().to_path_buf())
.with_mirror_url(None);
let annual = client.annual_dividend("KO").await.unwrap();
assert!(
annual.is_some(),
"KO is in the fixture — annual_dividend must return Some(_)"
);
}
#[tokio::test]
async fn annual_dividend_unknown_ticker_returns_none() {
let server = MockServer::start().await;
let parquet = fixture_bytes();
Mock::given(method("GET"))
.and(path("/manifest.json"))
.respond_with(ResponseTemplate::new(200).set_body_string(manifest_body()))
.expect(1..)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/dividends-2024.parquet"))
.respond_with(ResponseTemplate::new(200).set_body_bytes(parquet))
.expect(1..)
.mount(&server)
.await;
let cache_dir = TempDir::new().unwrap();
let client = Divkit::new()
.with_base_url(server.uri())
.with_cache_dir(cache_dir.path().to_path_buf())
.with_mirror_url(None);
let annual = client.annual_dividend("NOPE").await.unwrap();
assert_eq!(annual, None, "unknown ticker must return Ok(None)");
}
#[tokio::test]
async fn dividends_for_known_ticker() {
let server = MockServer::start().await;
let parquet = fixture_bytes();
Mock::given(method("GET"))
.and(path("/manifest.json"))
.respond_with(ResponseTemplate::new(200).set_body_string(manifest_body()))
.expect(1..)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/dividends-2024.parquet"))
.respond_with(ResponseTemplate::new(200).set_body_bytes(parquet))
.expect(1..)
.mount(&server)
.await;
let cache_dir = TempDir::new().unwrap();
let client = Divkit::new()
.with_base_url(server.uri())
.with_cache_dir(cache_dir.path().to_path_buf())
.with_mirror_url(None);
let events = client.dividends("KO").await.unwrap();
assert_eq!(events.len(), 4, "fixture has 4 KO rows");
for ev in &events {
assert!((ev.amount - 0.485).abs() < 1e-9);
}
}
#[tokio::test]
async fn dividend_snapshot_for_known_ticker() {
let server = MockServer::start().await;
let parquet = fixture_bytes();
Mock::given(method("GET"))
.and(path("/manifest.json"))
.respond_with(ResponseTemplate::new(200).set_body_string(manifest_body()))
.expect(1..)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/dividends-2024.parquet"))
.respond_with(ResponseTemplate::new(200).set_body_bytes(parquet))
.expect(1..)
.mount(&server)
.await;
let cache_dir = TempDir::new().unwrap();
let client = Divkit::new()
.with_base_url(server.uri())
.with_cache_dir(cache_dir.path().to_path_buf())
.with_mirror_url(None);
let snap = client.dividend_snapshot("KO").await.unwrap();
assert_eq!(snap.ticker, "KO");
assert_eq!(snap.cik, 21344);
assert_eq!(snap.history.len(), 4);
let as_of = chrono::NaiveDate::from_ymd_opt(2024, 12, 13).unwrap();
assert!((snap.annual_amount_as_of(as_of) - 1.94).abs() < 1e-9);
}
#[test]
fn annual_dividend_blocking_known_ticker() {
let rt = tokio::runtime::Runtime::new().unwrap();
let server = rt.block_on(async { MockServer::start().await });
let parquet = fixture_bytes();
rt.block_on(async {
Mock::given(method("GET"))
.and(path("/manifest.json"))
.respond_with(ResponseTemplate::new(200).set_body_string(manifest_body()))
.expect(1..)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/dividends-2024.parquet"))
.respond_with(ResponseTemplate::new(200).set_body_bytes(parquet))
.expect(1..)
.mount(&server)
.await;
});
let cache_dir = TempDir::new().unwrap();
let client = Divkit::new()
.with_base_url(server.uri())
.with_cache_dir(cache_dir.path().to_path_buf())
.with_mirror_url(None);
let annual = client.annual_dividend_blocking("KO").unwrap();
assert!(annual.is_some());
}
#[tokio::test]
async fn checksum_mismatch_is_rejected() {
use divkit::Error;
let server = MockServer::start().await;
let parquet = fixture_bytes();
let bad_digest = "0".repeat(64);
Mock::given(method("GET"))
.and(path("/manifest.json"))
.respond_with(
ResponseTemplate::new(200).set_body_string(manifest_body_with_digest(&bad_digest)),
)
.expect(1..)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/dividends-2024.parquet"))
.respond_with(ResponseTemplate::new(200).set_body_bytes(parquet))
.expect(1..)
.mount(&server)
.await;
let cache_dir = TempDir::new().unwrap();
let client = Divkit::new()
.with_base_url(server.uri())
.with_cache_dir(cache_dir.path().to_path_buf())
.with_mirror_url(None);
let result = client.annual_dividend("KO").await;
assert!(
result.is_err(),
"a digest mismatch must surface as an error, not be silently ignored"
);
let err = result.unwrap_err();
let msg = err.to_string();
let is_checksum =
matches!(err, Error::ChecksumMismatch { .. }) || msg.contains("checksum mismatch");
assert!(
is_checksum,
"expected a checksum-mismatch error, got: {msg}"
);
}