ic-query 0.90.0

Internet Computer query library for CloudEngine, NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
use super::*;
use crate::sns::report::live::types::{DefiniteCanisterSettings, SnsRootExtensions};
use ic_platform::candid::contract::Nat;

#[test]
fn root_inventory_projection_preserves_native_health_and_canonical_roles() {
    let inventory = inventory_response();
    let health = health_response();

    let projected =
        mainnet_sns_canister_inventory(inventory, Ok(health)).expect("project inventory");

    assert_eq!(
        projected
            .canisters
            .iter()
            .map(|canister| canister.role)
            .collect::<Vec<_>>(),
        vec![
            SnsCanisterRole::Root,
            SnsCanisterRole::Governance,
            SnsCanisterRole::Ledger,
            SnsCanisterRole::Swap,
            SnsCanisterRole::Index,
            SnsCanisterRole::Archive,
            SnsCanisterRole::Dapp,
            SnsCanisterRole::Extension,
        ]
    );
    assert_eq!(
        projected.canisters[0].status,
        Some(SnsCanisterStatus::Running)
    );
    assert_eq!(
        projected.canisters[0].module_hash_hex.as_deref(),
        Some("0102")
    );
    assert_eq!(projected.canisters[0].cycles.as_deref(), Some("1000"));
    assert_eq!(
        projected.canisters[0].cycle_balance_status,
        SnsCanisterCycleBalanceStatus::ReportedNonzero
    );
    assert_eq!(projected.canisters[0].memory_size.as_deref(), Some("2000"));
    assert_eq!(
        projected.canisters[0].idle_cycles_burned_per_day.as_deref(),
        Some("30")
    );
    assert_eq!(projected.gaps.len(), 1);
    assert_eq!(projected.health_query_gap, None);
    assert_eq!(
        projected.gaps[0].kind,
        SnsCanisterGapKind::HealthUnsupported
    );
    assert!(!projected.point_in_time_guaranteed);
}

#[test]
fn failed_health_query_preserves_inventory_with_unavailable_cycle_evidence() {
    let projected = mainnet_sns_canister_inventory(
        inventory_response(),
        Err("SNS ingress method get_sns_canisters_summary failed: reject".to_string()),
    )
    .expect("project inventory without health");

    assert_eq!(projected.canisters.len(), 8);
    assert!(projected.canisters.iter().all(|canister| {
        canister.status.is_none()
            && canister.cycles.is_none()
            && canister.cycle_balance_status == SnsCanisterCycleBalanceStatus::Unavailable
    }));
    assert_eq!(
        projected.health_query_gap.as_ref().map(|gap| gap.method),
        Some(SnsCanisterMethod::GetSnsCanistersSummary)
    );
    assert!(
        projected
            .health_query_gap
            .as_ref()
            .is_some_and(|gap| gap.reason.contains("reject"))
    );
    assert_eq!(projected.gaps.len(), 1);
    assert_eq!(
        projected.gaps[0].kind,
        SnsCanisterGapKind::HealthUnsupported
    );
}

#[test]
fn cycle_balance_classification_preserves_raw_naturals() {
    const LARGE: &str = "18446744073709551616000";
    for (cycles, expected_status) in [
        ("0", SnsCanisterCycleBalanceStatus::ReportedZero),
        (LARGE, SnsCanisterCycleBalanceStatus::ReportedNonzero),
    ] {
        let mut canister_status = status(CanisterStatusType::Stopped);
        canister_status.cycles = cycles.parse().unwrap();
        canister_status.memory_size = LARGE.parse().unwrap();
        canister_status.idle_cycles_burned_per_day = LARGE.parse().unwrap();

        let row = canister_row(
            SnsCanisterRole::Root,
            principal(1).to_text(),
            Some(canister_status),
        );

        assert_eq!(row.cycles.as_deref(), Some(cycles));
        assert_eq!(row.cycle_balance_status, expected_status);
        assert_eq!(row.memory_size.as_deref(), Some(LARGE));
        assert_eq!(row.idle_cycles_burned_per_day.as_deref(), Some(LARGE));
    }
}

#[test]
fn root_inventory_projection_retains_relation_specific_gaps() {
    let mut inventory = inventory_response();
    inventory.governance = None;
    inventory.dapps = vec![principal(11)];
    let mut health = health_response();
    health.root = Some(summary(
        principal(9),
        Some(status(CanisterStatusType::Running)),
    ));
    health.governance = Some(summary(
        principal(2),
        Some(status(CanisterStatusType::Running)),
    ));
    health.dapps = vec![
        CanisterSummary {
            canister_id: None,
            status: None,
        },
        summary(principal(10), Some(status(CanisterStatusType::Stopped))),
    ];

    let projected =
        mainnet_sns_canister_inventory(inventory, Ok(health)).expect("project inventory");
    let kinds = projected
        .gaps
        .iter()
        .map(|gap| gap.kind)
        .collect::<Vec<_>>();

    assert!(kinds.contains(&SnsCanisterGapKind::SummaryCanisterIdMismatch));
    assert!(kinds.contains(&SnsCanisterGapKind::InventoryCanisterIdMissing));
    assert!(kinds.contains(&SnsCanisterGapKind::SummaryNotInInventory));
    assert!(kinds.contains(&SnsCanisterGapKind::SummaryCanisterIdMissing));
    assert!(kinds.contains(&SnsCanisterGapKind::SummaryMissing));
}

#[test]
fn root_inventory_projection_rejects_duplicate_inventory_canister_ids() {
    let mut inventory = inventory_response();
    inventory.dapps = vec![principal(1)];

    let error = mainnet_sns_canister_inventory(inventory, Ok(health_response()))
        .expect_err("duplicate inventory canister must fail");

    assert!(matches!(
        error,
        SnsHostError::DuplicateCanisterId {
            first_role,
            duplicate_role,
            ..
        } if first_role == "root" && duplicate_role == "dapp"
    ));
}

#[test]
fn duplicate_health_summaries_do_not_select_response_order_status() {
    let inventory = inventory_response();
    let mut health = health_response();
    health.archives = vec![
        summary(principal(6), Some(status(CanisterStatusType::Running))),
        summary(principal(6), Some(status(CanisterStatusType::Stopped))),
    ];

    let projected =
        mainnet_sns_canister_inventory(inventory, Ok(health)).expect("project inventory");
    let archive = projected
        .canisters
        .iter()
        .find(|canister| canister.role == SnsCanisterRole::Archive)
        .expect("archive inventory row");

    assert_eq!(archive.status, None);
    assert_eq!(
        projected
            .gaps
            .iter()
            .filter(|gap| {
                gap.role == SnsCanisterRole::Archive
                    && gap.kind == SnsCanisterGapKind::DuplicateSummary
            })
            .count(),
        1
    );
    assert!(!projected.gaps.iter().any(|gap| {
        gap.role == SnsCanisterRole::Archive && gap.kind == SnsCanisterGapKind::StatusMissing
    }));
}

fn inventory_response() -> ListSnsCanistersResponse {
    ListSnsCanistersResponse {
        root: Some(principal(1)),
        governance: Some(principal(2)),
        ledger: Some(principal(3)),
        swap: Some(principal(4)),
        index: Some(principal(5)),
        dapps: vec![principal(7)],
        archives: vec![principal(6)],
        extensions: Some(SnsRootExtensions {
            extension_canister_ids: vec![principal(8)],
        }),
    }
}

fn health_response() -> GetSnsCanistersSummaryResponse {
    GetSnsCanistersSummaryResponse {
        root: Some(summary(
            principal(1),
            Some(status(CanisterStatusType::Running)),
        )),
        governance: Some(summary(
            principal(2),
            Some(status(CanisterStatusType::Running)),
        )),
        ledger: Some(summary(
            principal(3),
            Some(status(CanisterStatusType::Running)),
        )),
        swap: Some(summary(
            principal(4),
            Some(status(CanisterStatusType::Stopping)),
        )),
        index: Some(summary(
            principal(5),
            Some(status(CanisterStatusType::Running)),
        )),
        dapps: vec![summary(
            principal(7),
            Some(status(CanisterStatusType::Stopped)),
        )],
        archives: vec![summary(
            principal(6),
            Some(status(CanisterStatusType::Running)),
        )],
    }
}

fn summary(canister_id: Principal, status: Option<CanisterStatusResult>) -> CanisterSummary {
    CanisterSummary {
        canister_id: Some(canister_id),
        status,
    }
}

fn status(status: CanisterStatusType) -> CanisterStatusResult {
    CanisterStatusResult {
        status,
        memory_size: Nat::from(2_000_u64),
        cycles: Nat::from(1_000_u64),
        settings: DefiniteCanisterSettings {
            controllers: vec![principal(20), principal(19)],
        },
        idle_cycles_burned_per_day: Nat::from(30_u64),
        module_hash: Some(vec![1, 2]),
    }
}

fn principal(value: u8) -> Principal {
    Principal::self_authenticating([value])
}