tests/domstol_rattspraxis_live.rs
//! **The live proof** — `domstol-rattspraxis` against the real Domstolsverket PUH API.
//!
//! `#[ignore]` because CI has no network and this crate's law is that tests never hang
//! for want of one. Run it deliberately:
//!
//! ```text
//! cargo test --test domstol_rattspraxis_live --features daemon -- --ignored --nocapture
//! ```
//!
//! It is not a fixture dressed as a live test: it FAILS if the endpoint is unreachable
//! rather than falling back to a synthetic sample, because the whole point is to measure
//! what the real server does.
use korp_collectors::rest::{DOMSTOL_RATTSPRAXIS_URL, RestCollector};
use korp_collectors::sink::BatchSink;
use korp_collectors::{Bounds, Collector, CollectorCfg};
use arrow_array::{Array, Int64Array, StringArray};
/// **Three real pages of Swedish case law, through the real collector.**
///
/// Capped at 3 pages deliberately: the register is 174 pages and up to 26 MB a page (one
/// live page weighed 26.7 MB, 2026-09-05), so a full crawl is a job and not a test. What
/// this proves is the part a fixture cannot — that the pinned sort, the page parameter
/// and the field map all survive contact with the live server.
#[tokio::test]
#[ignore = "live network: the real Domstolsverket rättspraxis API"]
async fn three_live_pages_land_real_publications() {
let c = RestCollector::domstol_rattspraxis().with_max_pages(3);
let cfg = CollectorCfg::new(DOMSTOL_RATTSPRAXIS_URL, "domstol_rattspraxis");
let sink = BatchSink::memory();
let stats = c
.collect(&cfg, Bounds::default(), &sink)
.await
.expect("the live Domstolsverket endpoint must answer");
let batches = sink.take_batches();
let b = batches.first().expect("one batch");
println!("rows landed : {}", stats.rows);
println!("reason : {:?}", stats.reason);
println!("columns : {:?}", b.schema().fields().iter().map(|f| f.name().clone()).collect::<Vec<_>>());
// 3 pages of 100. Fewer would mean the walk stopped early against a register that
// has 17 340 publications; more would mean the ceiling is not a ceiling.
assert_eq!(stats.rows, 300, "3 pages × 100 publications");
assert_eq!(b.num_rows(), 300, "…and they are real Arrow rows, not a count");
let ids = b
.column_by_name("id")
.expect("id column")
.as_any()
.downcast_ref::<StringArray>()
.expect("id is Utf8");
let distinct: std::collections::BTreeSet<&str> = (0..ids.len()).map(|i| ids.value(i)).collect();
assert_eq!(
distinct.len(),
300,
"EVERY publication distinct across three live page boundaries — this is the \
assertion the default sort fails (610 rows, 607 distinct ids, measured \
2026-09-05)"
);
// The register really is a register: several courts, real case numbers, a summary
// on every row.
let court = b
.column_by_name("domstol_kod")
.expect("domstol_kod")
.as_any()
.downcast_ref::<StringArray>()
.expect("Utf8");
let courts: std::collections::BTreeSet<&str> =
(0..court.len()).filter(|i| !court.is_null(*i)).map(|i| court.value(i)).collect();
println!("courts : {courts:?}");
assert!(courts.len() >= 3, "more than one court publishes: {courts:?}");
let samm = b
.column_by_name("sammanfattning")
.expect("sammanfattning")
.as_any()
.downcast_ref::<StringArray>()
.expect("Utf8");
let with_summary = (0..samm.len()).filter(|i| !samm.is_null(*i)).count();
println!("summaries : {with_summary}/300");
assert!(with_summary > 250, "the editorial summary is the substance");
// The full text is measured, never landed.
assert!(
b.column_by_name("innehall").is_none(),
"the judgment text is NOT a column of this table"
);
let n = b
.column_by_name("innehall_tecken")
.expect("innehall_tecken")
.as_any()
.downcast_ref::<Int64Array>()
.expect("Int64");
let with_text: Vec<i64> = (0..n.len()).filter(|i| !n.is_null(*i)).map(|i| n.value(i)).collect();
println!(
"full texts : {} of 300 rows, widest {} characters",
with_text.len(),
with_text.iter().max().copied().unwrap_or(0)
);
assert!(!with_text.is_empty(), "some publications carry full text");
}
/// **The server's own lower bound really narrows the answer.**
///
/// `publicerad_fran_och_med` is the parameter `freshness()` declares, and this is the
/// proof that declaring it buys something: the same endpoint, one extra parameter, and
/// the register goes from 17 340 publications to a handful. Measured directly against
/// the wire rather than through the runner, so what is asserted is the SERVER's
/// behaviour and not korp's local row filter.
#[tokio::test]
#[ignore = "live network: the real Domstolsverket rättspraxis API"]
async fn the_declared_window_parameter_narrows_the_server_not_just_the_rows() {
let c = RestCollector::domstol_rattspraxis().with_max_pages(1);
let all = CollectorCfg::new(DOMSTOL_RATTSPRAXIS_URL, "domstol_rattspraxis");
let sink_all = BatchSink::memory();
let s_all = c.collect(&all, Bounds::default(), &sink_all).await.expect("full page");
// A window that starts today: whatever the courts published today, and nothing else.
let today = "2026-09-01";
let narrowed = CollectorCfg::new(
format!("{DOMSTOL_RATTSPRAXIS_URL}&publicerad_fran_och_med={today}"),
"domstol_rattspraxis",
);
let sink_win = BatchSink::memory();
let s_win = c
.collect(&narrowed, Bounds::default(), &sink_win)
.await
.expect("windowed page");
println!("unwindowed page : {} rows", s_all.rows);
println!("windowed page : {} rows (publicerad_fran_och_med={today})", s_win.rows);
assert_eq!(s_all.rows, 100, "an unwindowed page is full");
assert!(
s_win.rows < s_all.rows,
"the SERVER narrowed the answer — a window that returned a full page would mean \
the parameter was ignored, which is exactly what ten guessed parameter names \
did before the OpenAPI was found"
);
assert!(s_win.rows > 0, "and it is not narrowing everything away");
}