bee-check 0.6.0

Retrievability checker for Ethereum Swarm references. Multi-vantage stewardship probes, per-chunk drill-down, and one-shot re-seed.
Documentation
//! Unit test for the status-aggregation logic. The network path is
//! exercised by the binary smoke test (see README).

use bee_check::{Report, Status, VantageResult};

fn v(url: &str, r: Option<bool>, err: Option<&str>) -> VantageResult {
    VantageResult {
        bee_url: url.into(),
        retrievable: r,
        elapsed_ms: 1,
        error: err.map(str::to_string),
        overlay: None,
        bee_version: None,
        proximity_to_root: None,
        target_proximity: None,
    }
}

fn report_with(vantages: Vec<VantageResult>) -> Report {
    let json = serde_json::json!({
        "reference": "00".repeat(32),
        "status": "retrievable",
        "vantages": vantages,
        "spec_version": 1,
    });
    serde_json::from_value(json).unwrap()
}

#[test]
fn json_roundtrip_preserves_shape() {
    let r = report_with(vec![v("http://a", Some(true), None)]);
    let json = serde_json::to_string(&r).unwrap();
    assert!(json.contains("\"retrievable\":true"));
    assert!(json.contains("\"spec_version\":1"));
    let back: Report = serde_json::from_str(&json).unwrap();
    assert_eq!(back.vantages.len(), 1);
}

#[test]
fn status_serializes_snake_case() {
    let json = serde_json::to_string(&Status::Unretrievable).unwrap();
    assert_eq!(json, "\"unretrievable\"");
    let json = serde_json::to_string(&Status::Partial).unwrap();
    assert_eq!(json, "\"partial\"");
}

#[test]
fn error_field_omitted_when_none() {
    let r = report_with(vec![v("http://a", Some(false), None)]);
    let json = serde_json::to_string(&r).unwrap();
    assert!(!json.contains("\"error\""));
}

#[test]
fn neighborhood_fields_round_trip() {
    // v0.2 additive fields: overlay, bee_version, proximity_to_root.
    let json = serde_json::json!({
        "reference": "00".repeat(32),
        "status": "retrievable",
        "vantages": [{
            "bee_url": "http://a",
            "retrievable": true,
            "elapsed_ms": 5,
            "overlay": "7179856eb3c28642983d0e1b7f264693e407a2738593dec9fd59f2ee29046c9a",
            "bee_version": "2.7.2-rc1",
            "proximity_to_root": 3
        }],
        "spec_version": 1,
    });
    let r: Report = serde_json::from_value(json).unwrap();
    let v = &r.vantages[0];
    assert_eq!(v.overlay.as_deref(), Some("7179856eb3c28642983d0e1b7f264693e407a2738593dec9fd59f2ee29046c9a"));
    assert_eq!(v.bee_version.as_deref(), Some("2.7.2-rc1"));
    assert_eq!(v.proximity_to_root, Some(3));

    // Re-emit and make sure the optional fields survive.
    let s = serde_json::to_string(&r).unwrap();
    assert!(s.contains("\"overlay\":\"7179"));
    assert!(s.contains("\"proximity_to_root\":3"));
}

#[test]
fn old_v01_reports_still_parse() {
    // A v0.1 report (no overlay/version/PO) should still parse cleanly.
    let json = serde_json::json!({
        "reference": "00".repeat(32),
        "status": "unretrievable",
        "vantages": [{
            "bee_url": "http://a",
            "retrievable": false,
            "elapsed_ms": 1,
        }],
        "spec_version": 1,
    });
    let r: Report = serde_json::from_value(json).unwrap();
    assert!(r.vantages[0].overlay.is_none());
    assert!(r.vantages[0].bee_version.is_none());
    assert!(r.gateways.is_empty());
    assert!(r.resolution.is_none());
}

#[test]
fn gateway_result_round_trips() {
    // v0.3 gateway HEAD probes: separate `gateways` array.
    let json = serde_json::json!({
        "reference": "00".repeat(32),
        "status": "partial",
        "vantages": [],
        "gateways": [{
            "url": "https://api.gateway.ethswarm.org",
            "retrievable": true,
            "elapsed_ms": 412,
            "status_code": 200,
        }],
        "spec_version": 1,
    });
    let r: Report = serde_json::from_value(json).unwrap();
    assert_eq!(r.gateways.len(), 1);
    assert_eq!(r.gateways[0].retrievable, Some(true));
    assert_eq!(r.gateways[0].status_code, Some(200));
}

#[test]
fn chunk_stats_compute() {
    use bee_check::{ChunkProbe, ChunkVantage, compute_chunk_stats};
    use std::collections::BTreeMap;

    let mk = |addr: &str, latencies_a: u64, found_a: bool, latencies_b: u64, found_b: bool| {
        let mut per_vantage = BTreeMap::new();
        per_vantage.insert(
            "http://a".to_string(),
            ChunkVantage { found: found_a, elapsed_ms: latencies_a, error: None, proximity: None },
        );
        per_vantage.insert(
            "http://b".to_string(),
            ChunkVantage { found: found_b, elapsed_ms: latencies_b, error: None, proximity: None },
        );
        ChunkProbe {
            address: addr.to_string(),
            neighborhood: addr.chars().take(2).collect(),
            per_vantage,
        }
    };
    let probes = vec![
        mk("1a00000000000000000000000000000000000000000000000000000000000000", 100, true,  500, true),
        mk("1a11111111111111111111111111111111111111111111111111111111111111", 200, true,  600, true),
        mk("4a22222222222222222222222222222222222222222222222222222222222222", 300, true,  100, false),
    ];
    let stats = compute_chunk_stats(&probes);
    let a = &stats.per_vantage["http://a"];
    assert_eq!(a.found, 3);
    assert_eq!(a.missing, 0);
    assert_eq!(a.elapsed_max_ms, Some(300));
    let b = &stats.per_vantage["http://b"];
    assert_eq!(b.found, 2);
    assert_eq!(b.missing, 1);
    let nb_1a = &stats.per_neighborhood["1a"];
    assert_eq!(nb_1a.total, 4); // 2 chunks × 2 vantages
    assert_eq!(nb_1a.found, 4);
}

#[test]
fn parse_input_handles_feed_and_reference() {
    use bee_check::{ParsedInput, parse_input};
    match parse_input(&"00".repeat(32)) {
        ParsedInput::Reference(r) => assert_eq!(r.len(), 64),
        _ => panic!("expected Reference"),
    }
    match parse_input("feed:abcd:1234") {
        ParsedInput::Feed { owner, topic } => {
            assert_eq!(owner, "abcd");
            assert_eq!(topic, "1234");
        }
        _ => panic!("expected Feed"),
    }
    // Slash separator (URL-style) also accepted.
    match parse_input("feed:abcd/1234") {
        ParsedInput::Feed { owner, topic } => {
            assert_eq!(owner, "abcd");
            assert_eq!(topic, "1234");
        }
        _ => panic!("expected Feed via /"),
    }
}