canic-host 0.93.7

Host-side build, install, deployment, and fleet-template library for Canic workspaces
Documentation
use super::{
    RegistryEntry, RegistryParseError, parse_registry_entries, registry_entries_from_response,
};
use candid::{CandidType, Encode, Principal};
use canic_core::{
    cdk::utils::hash::hex_bytes,
    dto::{
        canister::CanisterInfo,
        error::{Error as CanicError, ErrorCode},
        topology::{SubnetRegistryEntry, SubnetRegistryResponse},
    },
    ids::CanisterRole,
};

const ROOT_TEXT: &str = "aaaaa-aa";
const APP_TEXT: &str = "renrk-eyaaa-aaaaa-aaada-cai";

#[test]
fn parses_canonical_registry_response_bytes() {
    let root = Principal::from_text(ROOT_TEXT).expect("root principal");
    let app = Principal::from_text(APP_TEXT).expect("app principal");
    let response = Ok::<_, CanicError>(SubnetRegistryResponse(vec![
        registry_entry(root, "root", root, "root", None, None),
        registry_entry(app, "app", app, "app", Some(root), Some(vec![1, 171])),
    ]));

    let entries = parse_registry_entries(&response_json(&response)).expect("parse registry");

    assert_eq!(
        entries,
        vec![
            RegistryEntry {
                pid: ROOT_TEXT.to_string(),
                role: Some("root".to_string()),
                parent_pid: None,
                module_hash: None,
            },
            RegistryEntry {
                pid: APP_TEXT.to_string(),
                role: Some("app".to_string()),
                parent_pid: Some(ROOT_TEXT.to_string()),
                module_hash: Some("01ab".to_string()),
            },
        ]
    );
}

#[test]
fn rejects_redundant_principal_mismatch() {
    let root = Principal::from_text(ROOT_TEXT).expect("root principal");
    let app = Principal::from_text(APP_TEXT).expect("app principal");
    let response =
        SubnetRegistryResponse(vec![registry_entry(root, "root", app, "root", None, None)]);

    assert!(matches!(
        registry_entries_from_response(response),
        Err(RegistryParseError::PrincipalMismatch { entry_pid, record_pid })
            if entry_pid == ROOT_TEXT && record_pid == APP_TEXT
    ));
}

#[test]
fn rejects_redundant_role_mismatch() {
    let root = Principal::from_text(ROOT_TEXT).expect("root principal");
    let response = SubnetRegistryResponse(vec![registry_entry(
        root, "root", root, "worker", None, None,
    )]);

    assert!(matches!(
        registry_entries_from_response(response),
        Err(RegistryParseError::RoleMismatch {
            pid,
            entry_role,
            record_role,
        }) if pid == ROOT_TEXT && entry_role == "root" && record_role == "worker"
    ));
}

#[test]
fn preserves_typed_registry_rejection() {
    let response = Err::<SubnetRegistryResponse, _>(CanicError::unavailable("not ready"));
    let error = parse_registry_entries(&response_json(&response)).expect_err("reject response");

    let RegistryParseError::Response(crate::icp::IcpJsonResponseError::Rejected(error)) = error
    else {
        panic!("expected typed registry rejection");
    };
    assert_eq!(error.code, ErrorCode::Unavailable);
    assert_eq!(error.message, "not ready");
}

fn registry_entry(
    pid: Principal,
    role: &str,
    record_pid: Principal,
    record_role: &str,
    parent_pid: Option<Principal>,
    module_hash: Option<Vec<u8>>,
) -> SubnetRegistryEntry {
    SubnetRegistryEntry {
        pid,
        role: CanisterRole::owned(role.to_string()),
        record: CanisterInfo {
            pid: record_pid,
            role: CanisterRole::owned(record_role.to_string()),
            parent_pid,
            module_hash,
            created_at: 1,
        },
    }
}

fn response_json<T: CandidType>(response: &T) -> String {
    let bytes = Encode!(response).expect("encode response");
    serde_json::json!({ "response_bytes": hex_bytes(bytes) }).to_string()
}