ic-query 0.48.1

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

#[test]
fn sns_neurons_refresh_rejects_invalid_public_page_size() {
    let root = temp_dir("ic-query-sns-neurons-invalid-page-size");
    let mut request = sns_neurons_refresh_request(&root, None);
    request.page_size = 0;

    let err = refresh_sns_neurons_cache_with_source(&request, &PagedFixtureSnsNeuronsSource)
        .expect_err("zero page size is invalid");

    assert!(matches!(
        err,
        SnsHostError::InvalidRefreshPageSize { page_size: 0, .. }
    ));
    let _ = fs::remove_dir_all(root);
}

#[test]
fn sns_neurons_cached_sort_requires_existing_complete_cache() {
    let root = temp_dir("ic-query-sns-neurons-missing-cache");
    let mut request = neurons_request("1");
    request.cache_root = Some(root.clone());
    request.sort = SnsNeuronsSort::Stake;

    let err = build_sns_neurons_report_with_source(&request, &NoLiveSnsNeuronsSource)
        .expect_err("missing cache is not auto-refreshed");

    assert!(matches!(
        err,
        SnsHostError::MissingNeuronsCacheForId { id: 1, .. }
    ));

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

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

    let err = refresh_sns_neurons_cache_with_source(&request, &PagedFixtureSnsNeuronsSource)
        .expect_err("incomplete refresh");

    assert!(matches!(
        err,
        SnsHostError::IncompleteRefresh {
            pages_fetched: 1,
            rows_fetched: 2,
            ..
        }
    ));
    assert!(!sns_neurons_cache_path(&root, MAINNET_NETWORK, ROOT_A).exists());
    let attempt_path = sns_neurons_refresh_attempt_path(&root, MAINNET_NETWORK, ROOT_A);
    assert!(attempt_path.is_file());

    let attempt: serde_json::Value =
        serde_json::from_slice(&fs::read(attempt_path).expect("read attempt"))
            .expect("parse attempt");
    assert_eq!(attempt["status"], "failed");
    assert_eq!(attempt["pages_fetched"], 1);
    assert_eq!(attempt["rows_fetched"], 2);
    assert_eq!(attempt["last_cursor"], "02".repeat(32));
    assert!(
        attempt["last_error"]
            .as_str()
            .expect("last error")
            .contains("max pages reached before API exhaustion")
    );

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

#[test]
fn sns_neurons_cached_sort_rejects_unsupported_cache_schema() {
    let root = temp_dir("ic-query-sns-neurons-unsupported-schema");
    let request = sns_neurons_refresh_request(&root, None);
    refresh_sns_neurons_cache_with_source(&request, &PagedFixtureSnsNeuronsSource)
        .expect("refresh neurons");

    let cache_path = sns_neurons_cache_path(&root, MAINNET_NETWORK, ROOT_A);
    let mut cache: serde_json::Value =
        serde_json::from_slice(&fs::read(&cache_path).expect("read cache")).expect("parse cache");
    cache["schema_version"] = serde_json::json!(999);
    fs::write(
        &cache_path,
        serde_json::to_vec_pretty(&cache).expect("serialize cache"),
    )
    .expect("write cache");

    let mut cached_request = neurons_request("1");
    cached_request.cache_root = Some(root.clone());
    cached_request.sort = SnsNeuronsSort::Stake;
    let err = build_sns_neurons_report_with_source(&cached_request, &NoLiveSnsNeuronsSource)
        .expect_err("unsupported schema rejected");

    assert!(matches!(
        err,
        SnsHostError::Cache(crate::HostCacheError::UnsupportedCacheSchemaVersion {
            version: 999,
            expected: SNS_NEURONS_CACHE_SCHEMA_VERSION,
            ..
        })
    ));

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

#[test]
fn sns_neurons_cached_sort_rejects_snapshot_identity_mismatch() {
    let root = temp_dir("ic-query-sns-neurons-identity-mismatch");
    let request = sns_neurons_refresh_request(&root, None);
    refresh_sns_neurons_cache_with_source(&request, &PagedFixtureSnsNeuronsSource)
        .expect("refresh neurons");

    let cache_path = sns_neurons_cache_path(&root, MAINNET_NETWORK, ROOT_A);
    let mut cache: serde_json::Value =
        serde_json::from_slice(&fs::read(&cache_path).expect("read cache")).expect("parse cache");
    cache["entity"] = serde_json::json!("wrong-root");
    fs::write(
        &cache_path,
        serde_json::to_vec_pretty(&cache).expect("serialize cache"),
    )
    .expect("write cache");

    let mut cached_request = neurons_request("1");
    cached_request.cache_root = Some(root.clone());
    cached_request.sort = SnsNeuronsSort::Stake;
    let err = build_sns_neurons_report_with_source(&cached_request, &NoLiveSnsNeuronsSource)
        .expect_err("identity mismatch rejected");

    match err {
        SnsHostError::CacheIdentityMismatch {
            path,
            field,
            expected,
            actual,
        } => {
            assert_eq!(path, cache_path);
            assert_eq!(field, "entity");
            assert_eq!(expected, ROOT_A);
            assert_eq!(actual, "wrong-root");
        }
        other => panic!("unexpected error: {other:?}"),
    }

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

#[derive(Clone, Copy)]
enum InvalidNeuronPage {
    Cursor,
    DuplicateIds,
    DescendingIds,
}

struct InvalidNeuronPageSource(InvalidNeuronPage);

delegate_sns_discovery!(InvalidNeuronPageSource);

impl SnsNeuronsSource for InvalidNeuronPageSource {
    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> {
        let mut page = PagedFixtureSnsNeuronsSource.fetch_sns_neuron_page(
            request,
            sns,
            limit,
            start_page_at,
            owner_principal_id,
        )?;
        if start_page_at.is_some() {
            match self.0 {
                InvalidNeuronPage::Cursor => {
                    page.last_cursor = Some(SnsNeuronId { id: vec![4; 32] });
                }
                InvalidNeuronPage::DuplicateIds => {
                    page.neurons[0] = page.neurons[1].clone();
                }
                InvalidNeuronPage::DescendingIds => {
                    page.neurons[0].neuron_id = "04".repeat(32);
                }
            }
        }
        Ok(page)
    }
}

#[test]
fn invalid_neuron_page_preserves_complete_cache_and_records_failure() {
    let root = temp_dir("ic-query-sns-neurons-invalid-cursor");
    let request = sns_neurons_refresh_request(&root, None);
    refresh_sns_neurons_cache_with_source(&request, &PagedFixtureSnsNeuronsSource).unwrap();
    let path = sns_neurons_cache_path(&root, MAINNET_NETWORK, ROOT_A);
    let original = fs::read(&path).unwrap();
    for invalid_page in [
        InvalidNeuronPage::Cursor,
        InvalidNeuronPage::DuplicateIds,
        InvalidNeuronPage::DescendingIds,
    ] {
        let error =
            refresh_sns_neurons_cache_with_source(&request, &InvalidNeuronPageSource(invalid_page))
                .expect_err("invalid page cannot prove completeness");
        assert!(matches!(
            error,
            SnsHostError::InvalidSourceData {
                capability: "SNS neuron page",
                ..
            }
        ));
        assert_eq!(fs::read(&path).unwrap(), original);
        let attempt: serde_json::Value = serde_json::from_slice(
            &fs::read(sns_neurons_refresh_attempt_path(
                &root,
                MAINNET_NETWORK,
                ROOT_A,
            ))
            .unwrap(),
        )
        .unwrap();
        assert_eq!(attempt["status"], "failed");
        assert_eq!(attempt["pages_fetched"], 1);
        assert_eq!(attempt["rows_fetched"], 2);
        assert!(!sns_neurons_refresh_lock_path(&root, MAINNET_NETWORK, ROOT_A).exists());
    }
    fs::remove_dir_all(root).unwrap();
}