canic 0.19.0

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
// Category C - Artifact / deployment test (embedded config).
// This test relies on embedded production config by design.

use canic::{
    cdk::types::Principal,
    dto::{
        canister::CanisterInfo,
        env::EnvSnapshotResponse,
        page::{Page, PageRequest},
        state::{AppStateResponse, SubnetStateResponse},
        topology::DirectoryEntryResponse,
    },
    ids::{CanisterRole, SubnetRole},
    protocol,
};
use canic_testkit::pic::Pic;
use std::collections::HashMap;

/// Assert that the registry contains the expected roles with the expected parents.
pub fn assert_registry_parents(
    pic: &Pic,
    root_id: Principal,
    expected: &[(CanisterRole, Option<Principal>)],
) {
    let registry: Result<canic::dto::topology::SubnetRegistryResponse, canic::Error> = pic
        .query_call(root_id, protocol::CANIC_SUBNET_REGISTRY, ())
        .expect("query registry transport");
    let registry = registry.expect("query registry application").0;

    for (role, expected_parent) in expected {
        let entry = registry
            .iter()
            .find(|entry| &entry.role == role)
            .unwrap_or_else(|| panic!("missing {role} entry in registry"));

        assert_eq!(
            entry.record.parent_pid, *expected_parent,
            "unexpected parent for {role}"
        );
    }
}

/// Look up one canister principal by role in the root subnet registry.
pub fn registry_pid_for_role(pic: &Pic, root_id: Principal, role: &CanisterRole) -> Principal {
    let registry: Result<canic::dto::topology::SubnetRegistryResponse, canic::Error> = pic
        .query_call(root_id, protocol::CANIC_SUBNET_REGISTRY, ())
        .expect("query registry transport");
    let registry = registry.expect("query registry application").0;

    registry
        .iter()
        .find(|entry| &entry.role == role)
        .map_or_else(
            || panic!("missing {role} entry in registry"),
            |entry| entry.pid,
        )
}

/// Assert that a child canister exposes a correct EnvSnapshotResponse.
pub fn assert_child_env(pic: &Pic, child_pid: Principal, role: CanisterRole, root_id: Principal) {
    let env: Result<EnvSnapshotResponse, canic::Error> = pic
        .query_call(child_pid, protocol::CANIC_ENV, ())
        .expect("query env transport");
    let env = env.expect("query env application");

    assert_eq!(
        env.canister_role,
        Some(role.clone()),
        "env canister role for {role}"
    );
    assert_eq!(env.parent_pid, Some(root_id), "env parent for {role}");
    assert_eq!(env.root_pid, Some(root_id), "env root for {role}");
    assert_eq!(
        env.prime_root_pid,
        Some(root_id),
        "env prime root for {role}"
    );
    assert_eq!(
        env.subnet_role,
        Some(SubnetRole::PRIME),
        "env subnet role for {role}"
    );
    assert!(
        env.subnet_pid.is_some(),
        "env subnet pid should be set for {role}"
    );
}

/// Assert that app and subnet directories are identical across all canisters.
pub fn assert_directories_consistent(
    pic: &Pic,
    root_id: Principal,
    subnet_directory: &HashMap<CanisterRole, Principal>,
) {
    let root_app_dir: Result<Page<DirectoryEntryResponse>, canic::Error> = pic
        .query_call(
            root_id,
            protocol::CANIC_APP_DIRECTORY,
            (PageRequest {
                limit: 100,
                offset: 0,
            },),
        )
        .expect("root app directory transport");
    let root_app_dir = root_app_dir.expect("root app directory application");

    let root_subnet_dir: Result<Page<DirectoryEntryResponse>, canic::Error> = pic
        .query_call(
            root_id,
            protocol::CANIC_SUBNET_DIRECTORY,
            (PageRequest {
                limit: 100,
                offset: 0,
            },),
        )
        .expect("root subnet directory transport");
    let root_subnet_dir = root_subnet_dir.expect("root subnet directory application");

    for (role, pid) in subnet_directory.iter().filter(|(r, _)| !r.is_root()) {
        let app_dir: Result<Page<DirectoryEntryResponse>, canic::Error> = pic
            .query_call(
                *pid,
                protocol::CANIC_APP_DIRECTORY,
                (PageRequest {
                    limit: 100,
                    offset: 0,
                },),
            )
            .expect("child app directory transport");
        let app_dir = app_dir.expect("child app directory application");

        let subnet_dir: Result<Page<DirectoryEntryResponse>, canic::Error> = pic
            .query_call(
                *pid,
                protocol::CANIC_SUBNET_DIRECTORY,
                (PageRequest {
                    limit: 100,
                    offset: 0,
                },),
            )
            .expect("child subnet directory transport");
        let subnet_dir = subnet_dir.expect("child subnet directory application");

        assert_eq!(
            app_dir.entries,
            root_app_dir.entries,
            "app directory mismatch for {role} (child={}, root={})",
            app_dir.entries.len(),
            root_app_dir.entries.len(),
        );

        assert_eq!(
            subnet_dir.entries,
            root_subnet_dir.entries,
            "subnet directory mismatch for {role} (child={}, root={})",
            subnet_dir.entries.len(),
            root_subnet_dir.entries.len(),
        );
    }
}

/// Assert that the CANIC_CANISTER_CHILDREN endpoint matches the registry.
pub fn assert_children_match_registry(pic: &Pic, root_id: Principal) {
    // 1. Query authoritative registry
    let registry: Result<canic::dto::topology::SubnetRegistryResponse, canic::Error> = pic
        .query_call(root_id, protocol::CANIC_SUBNET_REGISTRY, ())
        .expect("query registry transport");
    let registry = registry.expect("query registry application").0;

    // 2. Build expected children from registry (topology-only)
    let mut expected: Vec<CanisterInfo> = registry
        .iter()
        .filter(|entry| entry.record.parent_pid == Some(root_id))
        .map(|entry| CanisterInfo {
            pid: entry.pid,
            role: entry.role.clone(),
            parent_pid: entry.record.parent_pid,
            module_hash: None, // ignored for topology comparison
            created_at: 0,     // ignored for topology comparison
        })
        .collect();

    assert!(
        !expected.is_empty(),
        "registry should contain root children"
    );

    // 3. Query children endpoint
    let page: Result<Page<CanisterInfo>, canic::Error> = pic
        .query_call(
            root_id,
            protocol::CANIC_CANISTER_CHILDREN,
            (PageRequest {
                limit: 100,
                offset: 0,
            },),
        )
        .expect("query canister children transport");
    let mut page = page.expect("query canister children application");

    // 4. Normalize actual entries (ignore lifecycle metadata)
    for entry in &mut page.entries {
        entry.module_hash = None;
        entry.created_at = 0;
    }

    // 5. Normalize ordering (endpoint order is not significant)
    expected.sort_by(|a, b| a.role.cmp(&b.role));
    page.entries.sort_by(|a, b| a.role.cmp(&b.role));

    // 6. Assert invariants
    assert_eq!(page.total, expected.len() as u64, "reported total mismatch");

    assert_eq!(
        page.entries, expected,
        "child list from endpoint must match registry"
    );
}

/// Assert that root serves state snapshots and ordinary children do not export them.
pub fn assert_state_endpoints_are_root_only(pic: &Pic, root_id: Principal, child_pid: Principal) {
    let app_state: Result<AppStateResponse, canic::Error> = pic
        .query_call(root_id, protocol::CANIC_APP_STATE, ())
        .expect("root app state transport");
    app_state.expect("root app state application");

    let subnet_state: Result<SubnetStateResponse, canic::Error> = pic
        .query_call(root_id, protocol::CANIC_SUBNET_STATE, ())
        .expect("root subnet state transport");
    subnet_state.expect("root subnet state application");

    let child_app_state: Result<Result<AppStateResponse, canic::Error>, canic::Error> =
        pic.query_call(child_pid, protocol::CANIC_APP_STATE, ());
    let Err(err) = child_app_state else {
        panic!("child app state endpoint should be absent")
    };
    assert_missing_method(&err, protocol::CANIC_APP_STATE);

    let child_subnet_state: Result<Result<SubnetStateResponse, canic::Error>, canic::Error> =
        pic.query_call(child_pid, protocol::CANIC_SUBNET_STATE, ());
    let Err(err) = child_subnet_state else {
        panic!("child subnet state endpoint should be absent")
    };
    assert_missing_method(&err, protocol::CANIC_SUBNET_STATE);
}

// Match PocketIC missing-method failures without depending on one exact transport string.
fn assert_missing_method(err: &canic::Error, method: &str) {
    let message = err.message.as_str();

    assert!(
        message.contains(method),
        "missing-method error should mention {method}: {message}"
    );
    assert!(
        message.contains("not found")
            || message.contains("has no method")
            || message.contains("unknown method")
            || message.contains("did not find method"),
        "expected missing-method transport failure for {method}, got: {message}"
    );
}