ic-query 0.93.4

Internet Computer query library for CloudEngine, NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
use super::*;
use crate::report::{CollectionSourceRequest, SourceFuture};
use std::sync::Mutex;
const GOV: &str = "rrkah-fqaaa-aaaaa-aaaaq-cai";
const OWNER: &str = "aaaaa-aa";
fn row(id: u8, stake: u64) -> SnsNeuronRow {
    SnsNeuronRow {
        neuron_id: crate::hex::hex_bytes(&[id; 32]),
        permissions: vec![SnsNeuronPermissionRow {
            principal: OWNER.to_string(),
            permission_types: vec![5, 999],
        }],
        cached_neuron_stake_e8s: stake,
        neuron_fees_e8s: 10,
        maturity_e8s_equivalent: 2,
        staked_maturity_e8s_equivalent: Some(3),
        created_timestamp_seconds: 4,
        source_nns_neuron_id: None,
        auto_stake_maturity: Some(false),
        aging_since_timestamp_seconds: 5,
        dissolve_state: Some(SnsNeuronDissolveState::DissolveDelaySeconds(60)),
        voting_power_percentage_multiplier: 100,
        vesting_period_seconds: None,
    }
}
fn target() -> SnsNeuronTargetRequest {
    SnsNeuronTargetRequest::new("ic", "https://icp-api.io", 1_780_531_200, GOV).unwrap()
}
#[derive(Default)]
struct Source {
    calls: Mutex<Vec<String>>,
    fail: bool,
    wrong_owner: bool,
    cache_root: Option<std::path::PathBuf>,
}
impl SnsNeuronsSource for Source {
    fn fetch_sns_neurons<'a>(
        &'a self,
        request: &'a CollectionSourceRequest,
        governance: &'a str,
        limit: u32,
        owner: Option<&'a str>,
    ) -> SourceFuture<'a, Result<Vec<SnsNeuronRow>, SnsHostError>> {
        Box::pin(async move {
            assert_eq!(request.endpoint, "https://icp-api.io");
            assert_eq!(governance, GOV);
            assert_eq!(limit, 2);
            assert_eq!(owner, Some(OWNER));
            self.calls.lock().unwrap().push(governance.to_string());
            let mut neuron = row(1, 100);
            if self.wrong_owner {
                neuron.permissions.clear();
            }
            Ok(vec![neuron])
        })
    }
    fn fetch_sns_neuron_page<'a>(
        &'a self,
        request: &'a CollectionSourceRequest,
        governance: &'a str,
        limit: u32,
        start: Option<&'a SnsNeuronId>,
        owner: Option<&'a str>,
    ) -> SourceFuture<'a, Result<MainnetSnsNeuronPage, SnsHostError>> {
        Box::pin(async move {
            assert_eq!(governance, GOV);
            assert_eq!(limit, 2);
            assert!(owner.is_none());
            if let Some(root) = &self.cache_root {
                let path = sns_neurons_refresh_lock_path(root, &request.network, governance);
                assert!(crate::cache_file::inspect_refresh_lock(root, &path).unwrap());
                let attempt = cache_attempt::read_sns_refresh_attempt_status_strict(
                    root,
                    &sns_neurons_refresh_attempt_path(root, &request.network, governance),
                    &request.network,
                    None,
                )
                .unwrap()
                .unwrap();
                assert_eq!(
                    attempt.lifecycle.status,
                    crate::cache::CacheRefreshAttemptStatus::Running
                );
                assert_eq!(
                    attempt.lifecycle.pages_fetched as usize,
                    self.calls.lock().unwrap().len()
                );
            }
            self.calls.lock().unwrap().push(governance.to_string());
            if self.fail && start.is_some() {
                return Err(SnsHostError::AgentCall {
                    method: "list_neurons",
                    reason: "fixture unavailable".to_string(),
                });
            }
            let neurons = match start.map(|id| id.id[0]) {
                None => vec![row(1, 100), row(2, 300)],
                Some(2) => vec![row(2, 999), row(3, 200)],
                _ => Vec::new(),
            };
            let last_cursor = neurons.last().map(|n| SnsNeuronId {
                id: crate::hex::decode_lowercase_hex(&n.neuron_id).unwrap(),
            });
            Ok(MainnetSnsNeuronPage {
                neurons,
                last_cursor,
            })
        })
    }
}
impl SnsNeuronSource for Source {
    fn fetch_sns_neuron<'a>(
        &'a self,
        _: &'a CollectionSourceRequest,
        governance: &'a str,
        id: &'a str,
    ) -> SourceFuture<'a, Result<SnsNeuronRow, SnsHostError>> {
        Box::pin(async move {
            assert_eq!(governance, GOV);
            let neuron = row(1, 100);
            assert_eq!(id, neuron.neuron_id);
            Ok(neuron)
        })
    }
}
#[test]
fn explicit_targets_reject_numeric_ids_and_path_inputs() {
    for bad in ["1", "../cache", "", "https://example.com"] {
        assert!(SnsNeuronTargetRequest::new("ic", "https://icp-api.io", 0, bad).is_err());
    }
}

#[tokio::test(flavor = "current_thread")]
async fn invalid_principals_preserve_validation_order_without_source_calls() {
    let source = Source::default();
    for (governance, owner, expected_field) in [
        (
            GOV.to_uppercase(),
            OWNER.to_uppercase(),
            "governance_canister_id",
        ),
        (GOV.to_string(), "invalid".to_string(), "owner_principal_id"),
        (GOV.to_string(), OWNER.to_uppercase(), "owner_principal_id"),
    ] {
        let mut target = target();
        target.governance_canister_id = governance;
        let request = SnsNeuronsRequest::live(target, 2, Some(owner));
        let result = build_sns_neurons_report_with_source(&request, &source).await;
        assert!(
            matches!(result, Err(SnsHostError::InvalidPrincipal { field, .. }) if field == expected_field)
        );
    }
    assert!(source.calls.lock().unwrap().is_empty());
}
#[tokio::test(flavor = "current_thread")]
async fn direct_reads_preserve_stake_permission_codes_and_target() {
    let source = Source::default();
    let request = SnsNeuronsRequest::live(target(), 2, Some(OWNER.to_string()));
    let report = build_sns_neurons_report_with_source(&request, &source)
        .await
        .unwrap();
    assert_eq!(report.context.governance_canister_id, GOV);
    assert_eq!(report.neurons[0], row(1, 100));
    assert_eq!(report.neurons[0].permissions[0].permission_types, [5, 999]);
    let exact = build_sns_neuron_detail_report_with_source(
        &SnsNeuronRequest::new(target(), row(1, 0).neuron_id),
        &source,
    )
    .await
    .unwrap();
    assert_eq!(exact.neuron, report.neurons[0]);
    assert!(matches!(
        build_sns_neurons_report_with_source(
            &request,
            &Source {
                wrong_owner: true,
                ..Default::default()
            }
        )
        .await,
        Err(SnsHostError::InvalidSourceData { .. })
    ));
}
#[tokio::test(flavor = "current_thread")]
async fn complete_snapshots_keep_permissions_and_repeated_boundaries_once() {
    let root = crate::tests::temp_dir("explicit-governance-neuron-snapshot");
    let source = Source {
        cache_root: Some(root.clone()),
        ..Default::default()
    };
    let request = SnsSnapshotRefreshRequest::new(&root, target(), 2);
    let refresh =
        refresh_sns_neurons_cache_with_source_and_progress(&request, &source, &mut |_| {})
            .await
            .unwrap();
    assert_eq!(refresh.row_count, 3);
    assert_eq!(refresh.page_count, 3);
    assert!(refresh.cache_path.contains("sns-neuron/ic/"));
    assert!(
        !crate::cache_file::inspect_refresh_lock(
            &root,
            std::path::Path::new(&refresh.refresh_lock_path)
        )
        .unwrap()
    );
    let before = std::fs::read(&refresh.cache_path).unwrap();
    let read = SnsNeuronsRequest::cached(target(), 10, &root, SnsNeuronsCacheSort::Stake);
    let report = build_sns_neurons_report_with_source(
        &read,
        &Source {
            fail: true,
            ..Default::default()
        },
    )
    .await
    .unwrap();
    assert_eq!(
        report
            .neurons
            .iter()
            .map(|n| n.cached_neuron_stake_e8s)
            .collect::<Vec<_>>(),
        [300, 200, 100]
    );
    assert_eq!(report.neurons[0].permissions, row(2, 0).permissions);
    assert_eq!(
        serde_json::from_slice::<serde_json::Value>(&before).unwrap()["completeness"]["point_in_time_guaranteed"],
        false
    );
    assert!(
        refresh_sns_neurons_cache_with_source_and_progress(
            &request,
            &Source {
                fail: true,
                cache_root: Some(root.clone()),
                ..Default::default()
            },
            &mut |_| {}
        )
        .await
        .is_err()
    );
    assert_eq!(std::fs::read(&refresh.cache_path).unwrap(), before);
    let status =
        build_sns_neurons_cache_status_report(&SnsCacheStatusRequest::new(&root, "ic", GOV))
            .unwrap();
    assert!(status.found);
    assert_eq!(
        status
            .cache
            .unwrap()
            .valid()
            .unwrap()
            .governance_canister_id,
        GOV
    );
    assert_eq!(
        status.latest_attempt.unwrap().lifecycle.status,
        crate::cache::CacheRefreshAttemptStatus::Failed
    );
    let mut damaged: serde_json::Value = serde_json::from_slice(&before).unwrap();
    damaged["metadata"]["governance_canister_id"] = "aaaaa-aa".into();
    std::fs::write(&refresh.cache_path, serde_json::to_vec(&damaged).unwrap()).unwrap();
    let invalid =
        build_sns_neurons_cache_status_report(&SnsCacheStatusRequest::new(&root, "ic", GOV))
            .unwrap();
    assert!(invalid.cache.unwrap().valid().is_none());
}
#[tokio::test(flavor = "current_thread")]
async fn incomplete_refresh_preserves_previous_snapshot() {
    let root = crate::tests::temp_dir("explicit-neuron-page-ceiling");
    let source = Source::default();
    let mut request = SnsSnapshotRefreshRequest::new(&root, target(), 2);
    let initial =
        refresh_sns_neurons_cache_with_source_and_progress(&request, &source, &mut |_| {})
            .await
            .unwrap();
    let bytes = std::fs::read(&initial.cache_path).unwrap();
    request.max_pages = Some(1);
    assert!(matches!(
        refresh_sns_neurons_cache_with_source_and_progress(&request, &source, &mut |_| {}).await,
        Err(SnsHostError::IncompleteRefresh { .. })
    ));
    assert_eq!(std::fs::read(&initial.cache_path).unwrap(), bytes);
}