canic-cli 0.109.15

Operator CLI for Canic fleet setup, builds, evidence, catalog, backup, and restore workflows
Documentation
use super::*;
use crate::cli::{
    globals::{INTERNAL_ENVIRONMENT_OPTION, INTERNAL_ICP_OPTION},
    help::usage,
};

#[cfg(unix)]
use crate::test_support::TempDir;

fn strip_ansi(text: &str) -> String {
    let mut plain = String::new();
    let mut chars = text.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch == '\x1b' && chars.peek() == Some(&'[') {
            chars.next();
            for ch in chars.by_ref() {
                if ch == 'm' {
                    break;
                }
            }
            continue;
        }
        plain.push(ch);
    }
    plain
}

#[test]
fn usage_lists_current_commands_alphabetically() {
    let plain = strip_ansi(&usage());
    let names = plain
        .split_once("\nCommands:\n")
        .expect("top-level commands section")
        .1
        .split_once("\nOptions:\n")
        .expect("top-level options section")
        .0
        .lines()
        .filter_map(|line| line.split_whitespace().next())
        .collect::<Vec<_>>();
    let mut sorted = names.clone();
    sorted.sort_unstable();

    assert_eq!(names, sorted);
    assert_eq!(
        names,
        [
            "admission",
            "app",
            "auth",
            "backup",
            "blob-storage",
            "build",
            "cycles",
            "diagnostic",
            "evidence",
            "fleet",
            "info",
            "inspect",
            "medic",
            "network",
            "replica",
            "restore",
            "scaffold",
            "state",
            "status",
            "token",
        ]
    );
    assert!(plain.contains("Usage: canic [OPTIONS] <COMMAND>"));
    assert!(plain.contains("Converge one Fleet from current desired state"));
    assert!(!plain.contains("  deploy"));
    assert!(!plain.contains("  install"));
    assert!(!plain.contains("retained"));
}

#[test]
fn current_command_help_and_versions_return_ok() {
    for args in [
        &["admission", "--help"][..],
        &["app", "--help"],
        &["auth", "--help"],
        &["backup", "--help"],
        &["blob-storage", "--help"],
        &["build", "--help"],
        &["cycles", "--help"],
        &["diagnostic", "--help"],
        &["evidence", "--help"],
        &["fleet", "--help"],
        &["fleet", "generate", "--help"],
        &["fleet", "ensure", "--help"],
        &["info", "--help"],
        &["inspect", "--help"],
        &["inspect", "canister", "--help"],
        &["inspect", "fleet", "--help"],
        &["medic", "--help"],
        &["medic", "fleet", "--help"],
        &["network", "--help"],
        &["replica", "--help"],
        &["restore", "--help"],
        &["scaffold", "--help"],
        &["state", "--help"],
        &["status", "--help"],
        &["token", "--help"],
    ] {
        assert!(run(args.iter().map(OsString::from)).is_ok(), "{args:?}");
    }
    assert!(run([OsString::from("--version")]).is_ok());
    assert!(run([OsString::from("fleet"), OsString::from("--version")]).is_ok());
}

#[test]
fn global_options_are_forwarded_only_once() {
    let mut tail = vec![OsString::from("ensure"), OsString::from("staging")];
    apply_global_icp("fleet", &mut tail, Some("/tmp/icp".to_string()));
    apply_global_environment("fleet", &mut tail, Some("local".to_string()));
    assert_eq!(
        tail,
        vec![
            OsString::from("ensure"),
            OsString::from("staging"),
            OsString::from(INTERNAL_ICP_OPTION),
            OsString::from("/tmp/icp"),
            OsString::from(INTERNAL_ENVIRONMENT_OPTION),
            OsString::from("local"),
        ]
    );

    apply_global_icp("fleet", &mut tail, Some("/other/icp".to_string()));
    apply_global_environment("fleet", &mut tail, Some("other".to_string()));
    assert_eq!(
        tail.iter()
            .filter(|value| value.as_os_str() == INTERNAL_ICP_OPTION)
            .count(),
        1
    );
    assert_eq!(
        tail.iter()
            .filter(|value| value.as_os_str() == INTERNAL_ENVIRONMENT_OPTION)
            .count(),
        1
    );
}

#[test]
fn current_read_only_commands_receive_global_target_options() {
    for (command, raw_tail) in [
        ("admission", &["status", "staging"][..]),
        ("auth", &["renewal", "status", "staging"][..]),
        ("blob-storage", &["status", "staging", "root"]),
        ("cycles", &["balance"]),
        ("info", &["list", "staging"][..]),
        ("inspect", &["fleet", "staging", "--role", "root"]),
        ("medic", &["fleet", "staging"]),
        ("status", &[][..]),
        ("token", &["balance"]),
    ] {
        let mut tail = raw_tail.iter().map(OsString::from).collect::<Vec<_>>();
        apply_global_icp(command, &mut tail, Some("/tmp/icp".to_string()));
        apply_global_environment(command, &mut tail, Some("local".to_string()));

        assert!(tail.iter().any(|value| value == INTERNAL_ICP_OPTION));
        assert!(
            tail.iter()
                .any(|value| value == INTERNAL_ENVIRONMENT_OPTION)
        );
    }
}

#[cfg(unix)]
#[test]
fn icp_backed_command_rejects_unparseable_icp_before_effects() {
    use std::{fs, os::unix::fs::PermissionsExt};

    let root = TempDir::new("canic-cli-unsupported-icp");
    fs::create_dir_all(&root).expect("create temp dir");
    let icp = root.join("icp");
    fs::write(
        &icp,
        "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo 'icp development build'; exit 0; fi\necho 'unexpected effect' >&2\nexit 42\n",
    )
    .expect("write fake icp");
    fs::set_permissions(&icp, fs::Permissions::from_mode(0o755)).expect("chmod fake icp");

    let error = run([
        OsString::from("--icp"),
        icp.into_os_string(),
        OsString::from("replica"),
        OsString::from("status"),
    ])
    .expect_err("unsupported icp rejected");
    assert!(error.to_string().contains("unsupported icp-cli version"));
    assert!(!error.to_string().contains("unexpected effect"));
}

#[test]
fn state_report_failure_remains_silent_and_nonzero() {
    let error = CliError::State(state::StateCommandError::AuditFailed);
    assert_eq!(render_cli_error(&error), "");
    assert_eq!(cli_error_exit_code(&error), 1);
}