ic-query 0.25.4

Internet Computer query library for NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
use super::{fixtures::*, *};

#[test]
fn sns_metrics_preserves_native_cached_evidence_and_canonicalizes_rows() {
    let report =
        build_sns_metrics_report_with_source(&metrics_request("1"), &FixtureSnsMetricsSource)
            .expect("SNS metrics report");
    let text = sns_metrics_report_text(&report);

    assert_eq!(report.schema_version, SNS_METRICS_REPORT_SCHEMA_VERSION);
    assert_eq!(report.governance_canister_id, GOVERNANCE_A);
    assert_eq!(report.method, "get_metrics");
    assert_eq!(report.call_type, "composite_query");
    assert_eq!(report.time_window_seconds, 30 * 24 * 60 * 60);
    assert!(!report.point_in_time_guaranteed);
    assert!(report.treasury_metrics_cached);
    assert_eq!(report.treasury_metric_count, 2);
    assert_eq!(
        report
            .treasury_metrics
            .iter()
            .map(|row| row.treasury)
            .collect::<Vec<_>>(),
        vec![1, 2]
    );
    assert_eq!(report.num_recently_submitted_proposals, Some(12));
    assert_eq!(
        report
            .voting_power_metrics
            .as_ref()
            .and_then(|metrics| metrics.timestamp_seconds),
        Some(1_780_531_020)
    );
    assert!(text.contains("time_window: 30d"));
    assert!(text.contains("treasury_metrics_cached: yes"));
    assert!(text.contains("SNS token treasury"));
    assert!(text.contains("governance_total_potential_voting_power"));
}

#[test]
fn sns_metrics_preserves_unknown_native_treasury_codes() {
    let report = build_sns_metrics_report_with_source(
        &metrics_request("1"),
        &MutatingFixtureSnsMetricsSource(use_unknown_treasury_code),
    )
    .expect("unknown native code is preserved");

    assert_eq!(report.treasury_metrics[1].treasury, 99);
    assert_eq!(
        report.treasury_metrics[1].treasury_kind,
        SnsTreasuryKind::Unknown
    );
}

#[test]
fn sns_metrics_rejects_invalid_windows_before_source_access() {
    for seconds in [0, MAX_SNS_METRICS_TIME_WINDOW_SECONDS + 1] {
        let request = metrics_request("1").with_time_window_seconds(seconds);
        let error = build_sns_metrics_report_with_source(&request, &NoCallSnsMetricsSource)
            .expect_err("invalid window must fail before source access");

        assert!(matches!(
            error,
            SnsHostError::InvalidMetricsTimeWindow {
                seconds: actual,
                max_seconds: MAX_SNS_METRICS_TIME_WINDOW_SECONDS,
            } if actual == seconds
        ));
    }
}

#[test]
fn live_sns_metrics_source_rejects_non_mainnet_before_agent_construction() {
    let request = SnsSourceRequest::new(
        "local",
        "not a valid endpoint",
        "2026-08-01T00:00:00Z",
        "test",
    );
    let error = LiveSnsSource
        .fetch_sns_metrics(&request, &fixture_sns_a(), 86_400)
        .expect_err("non-mainnet must fail before endpoint parsing");

    assert!(matches!(
        error,
        SnsHostError::UnsupportedNetwork { network } if network == "local"
    ));
}

#[test]
fn live_sns_metrics_source_rejects_invalid_window_before_agent_construction() {
    let request =
        SnsSourceRequest::new("ic", "not a valid endpoint", "2026-08-01T00:00:00Z", "test");
    let error = LiveSnsSource
        .fetch_sns_metrics(&request, &fixture_sns_a(), 0)
        .expect_err("invalid window must fail before endpoint parsing");

    assert!(matches!(
        error,
        SnsHostError::InvalidMetricsTimeWindow { seconds: 0, .. }
    ));
}

#[test]
fn sns_metrics_rejects_invalid_custom_source_evidence() {
    for (mutate, expected_reason) in invalid_metrics_mutations() {
        let error = build_sns_metrics_report_with_source(
            &metrics_request("1"),
            &MutatingFixtureSnsMetricsSource(mutate),
        )
        .expect_err("invalid custom metrics evidence must fail");

        assert!(matches!(
            error,
            SnsHostError::InvalidSourceData {
                capability: "SNS metrics",
                reason,
            } if reason.contains(expected_reason)
        ));
    }
}

type MetricsMutation = (fn(&mut MainnetSnsMetrics), &'static str);

fn invalid_metrics_mutations() -> [MetricsMutation; 12] {
    [
        (wrong_target, "governance_canister_id"),
        (wrong_method, "method"),
        (wrong_call_type, "call_type"),
        (wrong_window, "time_window_seconds"),
        (claim_atomic_snapshot, "point-in-time guarantee"),
        (claim_uncached_treasury, "treasury_metrics_cached"),
        (duplicate_treasury_code, "duplicate treasury code"),
        (wrong_treasury_kind, "expected"),
        (invalid_ledger_principal, "ledger_canister_id"),
        (untrimmed_name, "surrounding whitespace"),
        (invalid_subaccount, "32-byte lowercase hexadecimal"),
        (too_many_treasury_rows, "exceeds"),
    ]
}

fn use_unknown_treasury_code(metrics: &mut MainnetSnsMetrics) {
    metrics.treasury_metrics[0].treasury = 99;
    metrics.treasury_metrics[0].treasury_kind = SnsTreasuryKind::Unknown;
}

fn wrong_target(metrics: &mut MainnetSnsMetrics) {
    metrics.governance_canister_id = ROOT_A.to_string();
}

fn wrong_method(metrics: &mut MainnetSnsMetrics) {
    metrics.method = "get_cached_metrics".to_string();
}

fn wrong_call_type(metrics: &mut MainnetSnsMetrics) {
    metrics.call_type = "query".to_string();
}

const fn wrong_window(metrics: &mut MainnetSnsMetrics) {
    metrics.time_window_seconds += 1;
}

const fn claim_atomic_snapshot(metrics: &mut MainnetSnsMetrics) {
    metrics.point_in_time_guaranteed = true;
}

const fn claim_uncached_treasury(metrics: &mut MainnetSnsMetrics) {
    metrics.treasury_metrics_cached = false;
}

fn duplicate_treasury_code(metrics: &mut MainnetSnsMetrics) {
    metrics.treasury_metrics[1].treasury = 2;
    metrics.treasury_metrics[1].treasury_kind = SnsTreasuryKind::SnsToken;
}

fn wrong_treasury_kind(metrics: &mut MainnetSnsMetrics) {
    metrics.treasury_metrics[0].treasury_kind = SnsTreasuryKind::Icp;
}

fn invalid_ledger_principal(metrics: &mut MainnetSnsMetrics) {
    metrics.treasury_metrics[0].ledger_canister_id = Some("not-a-principal".to_string());
}

fn untrimmed_name(metrics: &mut MainnetSnsMetrics) {
    metrics.treasury_metrics[0].name = Some(" SNS treasury".to_string());
}

fn invalid_subaccount(metrics: &mut MainnetSnsMetrics) {
    metrics.treasury_metrics[0].account_subaccount_hex = Some("AB".repeat(32));
}

fn too_many_treasury_rows(metrics: &mut MainnetSnsMetrics) {
    let template = metrics.treasury_metrics[0].clone();
    metrics.treasury_metrics = (0_i32..17)
        .map(|treasury| SnsTreasuryMetricRow {
            treasury,
            treasury_kind: match treasury {
                0 => SnsTreasuryKind::Unspecified,
                1 => SnsTreasuryKind::Icp,
                2 => SnsTreasuryKind::SnsToken,
                _ => SnsTreasuryKind::Unknown,
            },
            ..template.clone()
        })
        .collect();
}