ic-query 0.54.7

Internet Computer query library for CloudEngine, NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
use crate::test_support::temp_dir;
use crate::{
    HostCacheError,
    cache_file::CacheFileError,
    sns::report::tests::{fixtures::*, *},
};
use std::fs;

#[test]
fn sns_neurons_refresh_writes_complete_cache_and_cached_sort_uses_it() {
    let root = temp_dir("ic-query-sns-neurons-refresh");
    let request = sns_neurons_refresh_request(&root, None);

    let refresh = refresh_sns_neurons_cache_with_source(&request, &PagedFixtureSnsNeuronsSource)
        .expect("refresh neurons");
    let cache_path = sns_neurons_cache_path(&root, MAINNET_NETWORK, ROOT_A);
    let attempt_path = sns_neurons_refresh_attempt_path(&root, MAINNET_NETWORK, ROOT_A);
    let lock_path = sns_neurons_refresh_lock_path(&root, MAINNET_NETWORK, ROOT_A);

    assert!(cache_path.is_file());
    assert!(attempt_path.is_file());
    assert!(!lock_path.exists());
    assert!(refresh.complete);
    assert_eq!(refresh.id, 1);
    assert_eq!(refresh.page_count, 3);
    assert_eq!(refresh.neuron_count, 3);

    let cache: serde_json::Value =
        serde_json::from_slice(&fs::read(&cache_path).expect("read cache")).expect("parse cache");
    assert_eq!(cache["schema_version"], 1);
    assert_eq!(cache["domain"], "sns");
    assert_eq!(cache["entity"], ROOT_A);
    assert_eq!(cache["collection"], "neurons");
    assert_eq!(cache["scope"], "full");
    assert_eq!(cache["id"], 1);
    assert_eq!(cache["completeness"]["status"], "api_exhausted");
    assert_eq!(cache["neurons"].as_array().expect("cache neurons").len(), 3);
    assert!(cache.get("metadata").is_none());
    assert!(cache.get("data").is_none());

    let mut cached_request = neurons_request("1");
    cached_request.cache_root = Some(root.clone());
    cached_request.sort = SnsNeuronsSort::Stake;
    cached_request.limit = 2;
    cached_request.verbose = true;
    let report = build_sns_neurons_report_with_source(&cached_request, &NoLiveSnsNeuronsSource)
        .expect("cached neurons report");

    assert_eq!(report.data_source.as_str(), "cache");
    assert_eq!(report.sort, "stake");
    assert_eq!(report.total_neuron_count, 3);
    assert_eq!(report.neuron_count, 2);
    assert_eq!(report.requested_limit, 2);
    assert!(report.verbose);
    assert_eq!(report.owner_principal_id, None);
    assert_eq!(report.cache_complete, Some(true));
    let report_json = serde_json::to_value(&report).expect("serialize cached report");
    for field in [
        "network",
        "sns_wasm_canister_id",
        "fetched_at",
        "source_endpoint",
        "fetched_by",
        "id",
        "name",
        "root_canister_id",
        "governance_canister_id",
    ] {
        assert_eq!(report_json[field], cache[field], "cached {field}");
    }
    assert_eq!(report.neurons[0].neuron_id, "03".repeat(32));
    assert_eq!(report.neurons[0].cached_neuron_stake_e8s, 50);
    assert_eq!(report.neurons[1].neuron_id, "02".repeat(32));
    assert_eq!(report.neurons[1].cached_neuron_stake_e8s, 30);

    let attempt: serde_json::Value =
        serde_json::from_slice(&fs::read(attempt_path).expect("read attempt"))
            .expect("parse attempt");
    assert_eq!(attempt["status"], "complete");
    assert_eq!(attempt["pages_fetched"], 3);
    assert_eq!(attempt["rows_fetched"], 3);
    assert_eq!(attempt["last_cursor"], "03".repeat(32));
    assert_eq!(attempt["id"], refresh.id);
    assert_eq!(attempt["root_canister_id"], ROOT_A);
    assert!(attempt.get("metadata").is_none());

    let _ = fs::remove_dir_all(root);
}

struct ExclusiveNeuronPagesSource;

delegate_sns_discovery!(ExclusiveNeuronPagesSource);

impl SnsNeuronsSource for ExclusiveNeuronPagesSource {
    fn fetch_sns_neurons(
        &self,
        _request: &SnsSourceRequest,
        _sns: &MainnetSns,
        _limit: u32,
        _owner_principal_id: Option<&str>,
    ) -> Result<MainnetSnsNeurons, SnsHostError> {
        unreachable!("refresh uses pages")
    }

    fn fetch_sns_neuron_page(
        &self,
        _request: &SnsSourceRequest,
        _sns: &MainnetSns,
        limit: u32,
        start_page_at: Option<&SnsNeuronId>,
        owner_principal_id: Option<&str>,
    ) -> Result<MainnetSnsNeuronPage, SnsHostError> {
        assert_eq!(limit, 2);
        assert!(owner_principal_id.is_none());
        let ids = match start_page_at.map(|cursor| cursor.id.as_slice()) {
            None => vec![1, 2],
            Some(cursor) if cursor == [2; 32] => vec![3, 4],
            Some(cursor) if cursor == [4; 32] => vec![],
            other => panic!("unexpected cursor {other:?}"),
        };
        Ok(MainnetSnsNeuronPage {
            last_cursor: ids.last().map(|id| SnsNeuronId { id: vec![*id; 32] }),
            neurons: ids
                .into_iter()
                .map(|id| {
                    let mut row = fixture_sns_neuron().detail.neuron;
                    row.neuron_id = format!("{id:02x}").repeat(32);
                    row
                })
                .collect(),
        })
    }
}

#[test]
fn sns_neurons_refresh_accepts_exclusive_pages_and_empty_terminal_page() {
    let root = temp_dir("ic-query-sns-neurons-exclusive-pages");
    let request = sns_neurons_refresh_request(&root, None);
    let refresh = refresh_sns_neurons_cache_with_source(&request, &ExclusiveNeuronPagesSource)
        .expect("exclusive pages exhaust successfully");
    assert_eq!(refresh.page_count, 3);
    assert_eq!(refresh.neuron_count, 4);
    let cache: serde_json::Value = serde_json::from_slice(
        &fs::read(sns_neurons_cache_path(&root, MAINNET_NETWORK, ROOT_A)).unwrap(),
    )
    .unwrap();
    let ids: Vec<_> = cache["neurons"]
        .as_array()
        .unwrap()
        .iter()
        .map(|row| row["neuron_id"].as_str().unwrap())
        .collect();
    assert_eq!(
        ids,
        (1..=4)
            .map(|id| format!("{id:02x}").repeat(32))
            .collect::<Vec<_>>()
    );
    fs::remove_dir_all(root).unwrap();
}

#[test]
fn sns_neurons_refresh_failure_preserves_existing_complete_cache() {
    let root = temp_dir("ic-query-sns-neurons-refresh-preserves-cache");
    let complete_request = sns_neurons_refresh_request(&root, None);
    refresh_sns_neurons_cache_with_source(&complete_request, &PagedFixtureSnsNeuronsSource)
        .expect("complete refresh");

    let failed_request = sns_neurons_refresh_request(&root, Some(1));
    refresh_sns_neurons_cache_with_source(&failed_request, &PagedFixtureSnsNeuronsSource)
        .expect_err("incomplete refresh");

    let mut cached_request = neurons_request("1");
    cached_request.cache_root = Some(root.clone());
    cached_request.sort = SnsNeuronsSort::Stake;
    let report = build_sns_neurons_report_with_source(&cached_request, &NoLiveSnsNeuronsSource)
        .expect("previous complete cache remains usable");

    assert_eq!(report.data_source.as_str(), "cache");
    assert_eq!(report.total_neuron_count, 3);
    assert_eq!(report.neurons[0].neuron_id, "03".repeat(32));

    let attempt_path = sns_neurons_refresh_attempt_path(&root, MAINNET_NETWORK, ROOT_A);
    let attempt: serde_json::Value =
        serde_json::from_slice(&fs::read(attempt_path).expect("read attempt"))
            .expect("parse attempt");
    assert_eq!(attempt["status"], "failed");

    let _ = fs::remove_dir_all(root);
}

#[test]
fn sns_neurons_refresh_rejects_stale_lock_without_publishing() {
    let root = temp_dir("ic-query-sns-neurons-stale-lock");
    let request = sns_neurons_refresh_request(&root, None);
    let cache_path = sns_neurons_cache_path(&root, MAINNET_NETWORK, ROOT_A);
    let lock_path = sns_neurons_refresh_lock_path(&root, MAINNET_NETWORK, ROOT_A);
    crate::cache_file::write_managed_text_atomically(
        &root,
        &lock_path,
        &serde_json::to_string_pretty(&serde_json::json!({
            "schema_version": 1,
            "network": MAINNET_NETWORK,
            "pid": 999_999,
            "started_at_unix_ms": 1,
            "stale_after_seconds": DEFAULT_SNS_NEURONS_REFRESH_LOCK_STALE_SECONDS,
            "target_path": cache_path.display().to_string(),
        }))
        .expect("serialize stale lock"),
    )
    .expect("write stale lock");

    let err = refresh_sns_neurons_cache_with_source(&request, &PagedFixtureSnsNeuronsSource)
        .expect_err("stale lock requires manual cleanup");

    assert!(matches!(
        err,
        SnsHostError::Cache(HostCacheError::Operation {
            source: CacheFileError::StaleRefreshLock { .. },
            ..
        })
    ));
    assert!(!cache_path.is_file());
    assert!(lock_path.exists());

    let _ = fs::remove_dir_all(root);
}