ic-query 0.35.0

Internet Computer query library for CloudEngine, NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
use super::{
    CLOUD_ENGINE_NODE_INCLUDED_STATUSES, CLOUD_ENGINE_NODE_REWARD_TYPE, CloudEngineNodeInfoReport,
    CloudEngineNodeListReport, CloudEngineNodeRow, cloud_engine_node_info_report_text,
    cloud_engine_node_list_report_text,
};
use crate::ic::{IcDashboardReportProvenance, IcNodeStatusCounts};

const NODE_A: &str = "53amq-7hjxu-6lxaj-o2sp6-kmngy-qa22h-b7bo6-oeyyn-fkqnv-7tauf-7qe";
const NODE_B: &str = "72xg3-cvfed-jrbv3-kal7e-o53wl-tug5y-t432v-5ovop-7et6y-abuzx-oae";
const PROVIDER: &str = "bvcsg-3od6r-jnydw-eysln-aql7w-td5zn-ay5m6-sibd2-jzojt-anwag-mqe";
const OPERATOR: &str = "e3aue-mkha2-6zddy-xbmd7-3oybi-3nfoh-3bwgn-izbjn-uuqx2-ykc2z-7qe";
const CLOUD_ENGINE_SUBNET: &str = "nx5oj-b2azr-x3alh-sgf7i-duhfw-bflus-hisa2-5n2oq-tv7sd-haspd-cae";

#[test]
fn text_separates_dashboard_provenance_from_type4_nodes() {
    let node = node(NODE_A, "UP");
    let list = CloudEngineNodeListReport {
        provenance: provenance(),
        node_reward_type: CLOUD_ENGINE_NODE_REWARD_TYPE.to_string(),
        included_statuses: included_statuses(),
        requested_node_provider_id: None,
        node_count: 1,
        status_counts: IcNodeStatusCounts {
            total: 1,
            up: 1,
            ..IcNodeStatusCounts::default()
        },
        node_provider_count: 1,
        cloud_engine_subnet_count: 1,
        unassigned_cloud_engine_node_count: 0,
        nodes: vec![node.clone()],
    };
    let info = CloudEngineNodeInfoReport {
        provenance: provenance(),
        node,
    };

    let list_text = cloud_engine_node_list_report_text(&list);
    let info_text = cloud_engine_node_info_report_text(&info);
    assert!(list_text.contains("node_count: 1\n"));
    assert!(list_text.contains("unassigned_cloud_engine_node_count: 0\n\nCloudEngine nodes\n"));
    assert!(info_text.contains("node_reward_type: Type4"));
    assert!(info_text.contains("cloud_engine_subnet_id: nx5oj"));
}

fn provenance() -> IcDashboardReportProvenance {
    IcDashboardReportProvenance {
        schema_version: 1,
        network: "ic".to_string(),
        authority: "official_ic_dashboard_api".to_string(),
        source_endpoint: "https://ic-api.internetcomputer.org/api/v3".to_string(),
        fetched_at: "2026-08-08T12:00:00Z".to_string(),
        fetched_by: "test".to_string(),
        certified: false,
        point_in_time_guaranteed: false,
    }
}

fn included_statuses() -> Vec<String> {
    CLOUD_ENGINE_NODE_INCLUDED_STATUSES
        .into_iter()
        .map(str::to_string)
        .collect()
}

fn node(node_id: &str, status: &str) -> CloudEngineNodeRow {
    CloudEngineNodeRow {
        node_id: node_id.to_string(),
        node_operator_id: OPERATOR.to_string(),
        node_provider_id: PROVIDER.to_string(),
        node_provider_name: "DFINITY Stiftung".to_string(),
        node_type: "UNASSIGNED".to_string(),
        node_reward_type: CLOUD_ENGINE_NODE_REWARD_TYPE.to_string(),
        status: status.to_string(),
        alert_name: None,
        subnet_id: None,
        cloud_engine_subnet_id: Some(CLOUD_ENGINE_SUBNET.to_string()),
        data_center_id: "tp1".to_string(),
        data_center_name: "Tampa".to_string(),
        owner: "Flexential".to_string(),
        region: "North America,US,Florida".to_string(),
        guestos_version: Some("release".to_string()),
        guestos_tee_active: Some(false),
        ip_address: Some("2001:db8::1".to_string()),
        ipv4_connectivity_status: Some(false),
        node_hardware_generation: Some("Gen1".to_string()),
    }
}

#[cfg(feature = "dashboard-host")]
mod host {
    use super::*;
    use crate::{
        cloud_engine::{
            CloudEngineNodeInfoRequest, CloudEngineNodeInfoSourceData, CloudEngineNodeListRequest,
            CloudEngineNodeListSourceData, CloudEngineNodeSource,
            DEFAULT_CLOUD_ENGINE_DASHBOARD_SOURCE_ENDPOINT,
            build_cloud_engine_node_info_report_with_source,
            build_cloud_engine_node_list_report_with_source,
        },
        ic::{IcHostError, IcSourceRequest},
    };
    use std::cell::Cell;

    const NOW: u64 = 1_800_000_000;

    #[test]
    fn list_validates_scope_sorts_rows_and_counts_status_and_assignment() {
        let source = Fixture::default();
        let report = build_cloud_engine_node_list_report_with_source(&list_request(None), &source)
            .expect("Type4 nodes");

        assert_eq!(source.list_calls.get(), 1);
        assert_eq!(report.node_count, 2);
        assert_eq!(report.nodes[0].node_id, NODE_A);
        assert_eq!(report.nodes[1].node_id, NODE_B);
        assert_eq!(report.status_counts.up, 1);
        assert_eq!(report.status_counts.down, 1);
        assert_eq!(report.node_provider_count, 1);
        assert_eq!(report.cloud_engine_subnet_count, 1);
        assert_eq!(report.unassigned_cloud_engine_node_count, 1);
    }

    #[test]
    fn provider_filter_is_canonicalized_before_source_and_enforced_on_rows() {
        let source = Fixture::default();
        let report =
            build_cloud_engine_node_list_report_with_source(&list_request(Some(PROVIDER)), &source)
                .expect("provider Type4 nodes");
        assert_eq!(report.requested_node_provider_id.as_deref(), Some(PROVIDER));
        assert_eq!(source.seen_provider.get(), Some(PROVIDER));

        source.mutation.set(Some(Mutation::WrongProvider));
        assert!(matches!(
            build_cloud_engine_node_list_report_with_source(&list_request(Some(PROVIDER)), &source),
            Err(IcHostError::InvalidSourceData { .. })
        ));
    }

    #[test]
    fn exact_info_requires_the_requested_type4_node() {
        let source = Fixture::default();
        let request = CloudEngineNodeInfoRequest::new(
            "ic",
            DEFAULT_CLOUD_ENGINE_DASHBOARD_SOURCE_ENDPOINT,
            NOW,
            NODE_A,
        );
        let report = build_cloud_engine_node_info_report_with_source(&request, &source)
            .expect("exact Type4 node");
        assert_eq!(report.node.node_id, NODE_A);

        source.mutation.set(Some(Mutation::WrongRewardType));
        assert!(matches!(
            build_cloud_engine_node_info_report_with_source(&request, &source),
            Err(IcHostError::InvalidSourceData { .. })
        ));
    }

    #[test]
    fn list_source_contract_rejects_inconsistent_scope_and_duplicate_rows() {
        let source = Fixture::default();
        for mutation in [
            Mutation::WrongSource,
            Mutation::WrongRewardType,
            Mutation::WrongStatuses,
            Mutation::DuplicateNode,
            Mutation::UnknownListStatus,
        ] {
            source.mutation.set(Some(mutation));
            assert!(matches!(
                build_cloud_engine_node_list_report_with_source(&list_request(None), &source),
                Err(IcHostError::InvalidSourceData { .. })
            ));
        }
    }

    #[derive(Clone, Copy)]
    enum Mutation {
        WrongSource,
        WrongRewardType,
        WrongStatuses,
        DuplicateNode,
        UnknownListStatus,
        WrongProvider,
    }

    #[derive(Default)]
    struct Fixture {
        list_calls: Cell<usize>,
        mutation: Cell<Option<Mutation>>,
        seen_provider: Cell<Option<&'static str>>,
    }

    impl CloudEngineNodeSource for Fixture {
        fn fetch_cloud_engine_node_list(
            &self,
            request: &IcSourceRequest,
            node_provider_id: Option<&str>,
        ) -> Result<CloudEngineNodeListSourceData, IcHostError> {
            self.list_calls.set(self.list_calls.get() + 1);
            self.seen_provider.set(node_provider_id.map(|_| PROVIDER));
            let mut data = CloudEngineNodeListSourceData {
                source: request.clone(),
                requested_node_provider_id: node_provider_id.map(str::to_string),
                node_reward_type: CLOUD_ENGINE_NODE_REWARD_TYPE.to_string(),
                included_statuses: included_statuses(),
                nodes: vec![
                    {
                        let mut row = node(NODE_B, "DOWN");
                        row.cloud_engine_subnet_id = None;
                        row
                    },
                    node(NODE_A, "UP"),
                ],
            };
            match self.mutation.take() {
                Some(Mutation::WrongSource) => data.source.endpoint.push_str("/other"),
                Some(Mutation::WrongRewardType) => {
                    data.node_reward_type = "Type3dot1".to_string();
                    data.nodes[0].node_reward_type = "Type3dot1".to_string();
                }
                Some(Mutation::WrongStatuses) => {
                    data.included_statuses.pop();
                }
                Some(Mutation::DuplicateNode) => {
                    data.nodes[1].node_id = data.nodes[0].node_id.clone();
                }
                Some(Mutation::UnknownListStatus) => {
                    data.nodes[0].status = "FUTURE".to_string();
                }
                Some(Mutation::WrongProvider) => {
                    data.nodes[0].node_provider_id = "aaaaa-aa".to_string();
                }
                None => {}
            }
            Ok(data)
        }

        fn fetch_cloud_engine_node_info(
            &self,
            request: &IcSourceRequest,
            node_id: &str,
        ) -> Result<CloudEngineNodeInfoSourceData, IcHostError> {
            let mut row = node(node_id, "UP");
            if matches!(self.mutation.take(), Some(Mutation::WrongRewardType)) {
                row.node_reward_type = "Type3dot1".to_string();
            }
            Ok(CloudEngineNodeInfoSourceData {
                source: request.clone(),
                node_id: node_id.to_string(),
                node: row,
            })
        }
    }

    fn list_request(node_provider_id: Option<&str>) -> CloudEngineNodeListRequest {
        let request = CloudEngineNodeListRequest::new(
            "ic",
            DEFAULT_CLOUD_ENGINE_DASHBOARD_SOURCE_ENDPOINT,
            NOW,
        );
        match node_provider_id {
            Some(provider) => request.with_node_provider_id(provider),
            None => request,
        }
    }
}