ic-host-tools 0.28.1

Host-side Candid interfaces, ICP responses and certificate decoding
Documentation
use std::{
    ffi::{OsStr, OsString},
    path::Path,
    process::Command,
};

use crate::icp::{
    CallArguments, CallMode, InstallationStatusError, StatusOutput, call_command,
    decode_installation_status, status_command,
};

#[test]
fn text_calls_keep_native_arguments_and_caller_context() {
    let mut base = Command::new("selected icp");
    base.arg("--identity-password-file")
        .arg("/private/password")
        .current_dir("/workspace")
        .env("CALLER_SETTING", "kept");
    let mut query = call_command(
        base,
        OsStr::new("actor"),
        OsStr::new("read"),
        CallArguments::Text(OsStr::new("(\"two words\")")),
        CallMode::Query,
    );
    query.args(["--output", "hex", "--environment", "chosen"]);
    assert_eq!(query.get_program(), OsStr::new("selected icp"));
    assert_eq!(query.get_current_dir(), Some(Path::new("/workspace")));
    assert!(
        query
            .get_envs()
            .any(|(key, value)| key == "CALLER_SETTING" && value == Some(OsStr::new("kept")))
    );
    let args: Vec<_> = query.get_args().collect();
    assert_eq!(
        args,
        [
            "--identity-password-file",
            "/private/password",
            "canister",
            "call",
            "actor",
            "read",
            "(\"two words\")",
            "--query",
            "--output",
            "hex",
            "--environment",
            "chosen"
        ]
        .map(OsStr::new)
    );
    let update = call_command(
        Command::new("icp"),
        OsStr::new("actor"),
        OsStr::new("write"),
        CallArguments::Text(OsStr::new("()")),
        CallMode::Update,
    );
    assert_eq!(
        update.get_args().collect::<Vec<_>>(),
        ["canister", "call", "actor", "write", "()"].map(OsStr::new)
    );
}

#[test]
fn binary_calls_and_status_select_only_the_requested_protocol() {
    for mode in [CallMode::Query, CallMode::Update] {
        let command = call_command(
            Command::new("icp"),
            OsStr::new("actor"),
            OsStr::new("method"),
            CallArguments::BinaryFile(Path::new("/args with spaces.bin")),
            mode,
        );
        let mut expected = vec![
            "canister",
            "call",
            "actor",
            "method",
            "--args-file",
            "/args with spaces.bin",
            "--args-format",
            "bin",
        ];
        if mode == CallMode::Query {
            expected.push("--query");
        }
        assert_eq!(
            command.get_args().collect::<Vec<_>>(),
            expected.iter().map(OsStr::new).collect::<Vec<_>>()
        );
    }
    for (output, suffix) in [
        (StatusOutput::Text, None),
        (StatusOutput::Json, Some("--json")),
        (StatusOutput::IdOnly, Some("--id-only")),
    ] {
        let command = status_command(Command::new("icp"), OsStr::new("actor"), output);
        let mut expected = vec![
            OsString::from("canister"),
            OsString::from("status"),
            OsString::from("actor"),
        ];
        expected.extend(suffix.map(OsString::from));
        assert_eq!(
            command
                .get_args()
                .map(OsStr::to_os_string)
                .collect::<Vec<_>>(),
            expected
        );
    }
}

#[cfg(unix)]
#[test]
fn argument_bytes_are_not_lossily_converted() {
    use std::os::unix::ffi::OsStrExt;
    let argument = OsStr::from_bytes(b"(\"\xff\")");
    let command = call_command(
        Command::new("icp"),
        OsStr::new("actor"),
        OsStr::new("method"),
        CallArguments::Text(argument),
        CallMode::Update,
    );
    assert_eq!(command.get_args().last(), Some(argument));
}

#[test]
fn successful_status_null_and_string_are_the_only_installation_facts() {
    assert!(!decode_installation_status(br#"{"module_hash":null,"other":"ignored"}"#).unwrap());
    assert!(decode_installation_status(br#"{"module_hash":"hash"}"#).unwrap());
    assert!(decode_installation_status(br#"{"module_hash":""}"#).unwrap());
    assert!(matches!(
        decode_installation_status(b"{}"),
        Err(InstallationStatusError::MissingModuleHash)
    ));
    for input in [
        br#"{"module_hash":false}"#.as_slice(),
        br#"{"module_hash":23}"#,
        br#"{"module_hash":[]}"#,
        br#"{"module_hash":{}}"#,
    ] {
        assert!(matches!(
            decode_installation_status(input),
            Err(InstallationStatusError::InvalidModuleHash)
        ));
    }
    for input in [b"{".as_slice(), b"{} trailing", b"\xff"] {
        assert!(matches!(
            decode_installation_status(input),
            Err(InstallationStatusError::Json(_))
        ));
    }
    assert!(matches!(
        decode_installation_status(b"null"),
        Err(InstallationStatusError::MissingModuleHash)
    ));
}