Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/korp-collectors

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");
}