ic-query 0.25.1

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

#[test]
fn sns_swap_preserves_native_bounded_components_and_renders_them() {
    let report = build_sns_swap_report_with_source(&swap_request("1"), &FixtureSnsSwapSource)
        .expect("sns swap report");
    let text = sns_swap_report_text(&report);

    assert_eq!(report.schema_version, SNS_SWAP_REPORT_SCHEMA_VERSION);
    assert_eq!(report.id, 1);
    assert_eq!(report.swap_canister_id, SWAP_A);
    assert_eq!(report.component_query_count, 3);
    assert_eq!(report.successful_component_query_count, 3);
    assert_eq!(report.component_gap_count, 0);
    assert!(!report.point_in_time_guaranteed);
    assert_eq!(
        report
            .lifecycle
            .as_ref()
            .and_then(|lifecycle| lifecycle.lifecycle),
        Some(2)
    );
    assert_eq!(
        report
            .lifecycle
            .as_ref()
            .and_then(|lifecycle| lifecycle.lifecycle_name.as_deref()),
        Some("open")
    );
    assert_eq!(
        report
            .sale_parameters
            .as_ref()
            .map(|params| params.min_participants),
        Some(25)
    );
    assert_eq!(
        report
            .derived_state
            .as_ref()
            .and_then(|state| state.direct_participant_count),
        Some(120)
    );
    assert!(text.contains("swap_canister_id: br5f7-7uaaa-aaaaa-qaaca-cai"));
    assert!(text.contains("point_in_time_guaranteed: no"));
    assert!(text.contains("get_lifecycle"));
    assert!(text.contains("lifecycle_name"));
    assert!(text.contains("open"));
    assert!(text.contains("min_direct_participation_icp_e8s"));
    assert!(text.contains("direct_participant_count"));
}

#[test]
fn sns_swap_retains_partial_query_failure_as_a_typed_gap() {
    let report =
        build_sns_swap_report_with_source(&swap_request("1"), &PartialFixtureSnsSwapSource)
            .expect("partial sns swap report");

    assert_eq!(report.component_query_count, 3);
    assert_eq!(report.successful_component_query_count, 2);
    assert_eq!(report.component_gap_count, 1);
    assert!(report.lifecycle.is_some());
    assert!(report.sale_parameters.is_some());
    assert!(report.derived_state.is_none());
    assert_eq!(report.gaps[0].component, SnsSwapComponent::DerivedState);
    assert_eq!(report.gaps[0].method, "get_derived_state");
    assert!(report.gaps[0].reason.contains("query rejected"));
}

#[test]
fn sns_swap_preserves_successful_empty_sale_parameters_without_a_gap() {
    let report = build_sns_swap_report_with_source(
        &swap_request("1"),
        &MutatingFixtureSnsSwapSource(clear_sale_parameters),
    )
    .expect("successful empty sale parameters");

    assert_eq!(report.component_query_count, 3);
    assert_eq!(report.successful_component_query_count, 3);
    assert_eq!(report.component_gap_count, 0);
    assert!(report.sale_parameters.is_none());
    assert!(report.gaps.is_empty());
}

#[test]
fn sns_swap_preserves_three_independent_component_failures() {
    let report = build_sns_swap_report_with_source(
        &swap_request("1"),
        &MutatingFixtureSnsSwapSource(fail_all_components),
    )
    .expect("all component failures remain a report");

    assert_eq!(report.component_query_count, 3);
    assert_eq!(report.successful_component_query_count, 0);
    assert_eq!(report.component_gap_count, 3);
    assert!(report.lifecycle.is_none());
    assert!(report.sale_parameters.is_none());
    assert!(report.derived_state.is_none());
    assert_eq!(
        report
            .gaps
            .iter()
            .map(|gap| gap.component)
            .collect::<Vec<_>>(),
        vec![
            SnsSwapComponent::Lifecycle,
            SnsSwapComponent::SaleParameters,
            SnsSwapComponent::DerivedState,
        ]
    );
}

#[test]
fn sns_swap_rejects_a_custom_source_target_mismatch() {
    let error =
        build_sns_swap_report_with_source(&swap_request("1"), &WrongTargetFixtureSnsSwapSource)
            .expect_err("mismatched swap target must fail");

    assert!(matches!(
        error,
        SnsHostError::InvalidSourceData {
            capability: "SNS swap",
            reason,
        } if reason.contains("swap_canister_id") && reason.contains("expected")
    ));
}

#[test]
fn live_sns_swap_source_rejects_non_mainnet_before_agent_construction() {
    let request = SnsSourceRequest::new(
        "local",
        "not a valid endpoint",
        "2026-08-01T00:00:00Z",
        "test",
    );
    let sns = fixture_sns_a();

    let error = LiveSnsSource
        .fetch_sns_swap(&request, &sns)
        .expect_err("non-mainnet must fail");

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

#[test]
fn sns_swap_rejects_invalid_custom_source_evidence() {
    for (mutate, expected_reason) in [
        (
            wrong_lifecycle_method as fn(&mut MainnetSnsSwap),
            "lifecycle_method",
        ),
        (claim_atomic_snapshot, "point-in-time guarantee"),
        (wrong_lifecycle_name, "lifecycle_name"),
        (negative_derived_rate, "finite non-negative"),
        (value_and_gap_for_component, "both a value and a query gap"),
    ] {
        let error = build_sns_swap_report_with_source(
            &swap_request("1"),
            &MutatingFixtureSnsSwapSource(mutate),
        )
        .expect_err("invalid custom swap evidence must fail");

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

fn wrong_lifecycle_method(swap: &mut MainnetSnsSwap) {
    swap.lifecycle_method = "get_state".to_string();
}

const fn claim_atomic_snapshot(swap: &mut MainnetSnsSwap) {
    swap.point_in_time_guaranteed = true;
}

fn wrong_lifecycle_name(swap: &mut MainnetSnsSwap) {
    swap.lifecycle
        .as_mut()
        .expect("fixture lifecycle")
        .lifecycle_name = Some("committed".to_string());
}

const fn negative_derived_rate(swap: &mut MainnetSnsSwap) {
    swap.derived_state
        .as_mut()
        .expect("fixture derived state")
        .sns_tokens_per_icp = Some(-1.0);
}

fn value_and_gap_for_component(swap: &mut MainnetSnsSwap) {
    swap.gaps.push(SnsSwapQueryGap {
        component: SnsSwapComponent::Lifecycle,
        method: "get_lifecycle".to_string(),
        reason: "fixture failure".to_string(),
    });
}

const fn clear_sale_parameters(swap: &mut MainnetSnsSwap) {
    swap.sale_parameters = None;
}

fn fail_all_components(swap: &mut MainnetSnsSwap) {
    swap.lifecycle = None;
    swap.sale_parameters = None;
    swap.derived_state = None;
    swap.gaps = vec![
        SnsSwapQueryGap {
            component: SnsSwapComponent::Lifecycle,
            method: "get_lifecycle".to_string(),
            reason: "fixture lifecycle failure".to_string(),
        },
        SnsSwapQueryGap {
            component: SnsSwapComponent::SaleParameters,
            method: "get_sale_parameters".to_string(),
            reason: "fixture sale parameters failure".to_string(),
        },
        SnsSwapQueryGap {
            component: SnsSwapComponent::DerivedState,
            method: "get_derived_state".to_string(),
            reason: "fixture derived state failure".to_string(),
        },
    ];
}