elasticctl 0.1.3

Operate Elastic Security as code with a safety-first CLI for security engineers.
use assert_cmd::Command;

fn bin() -> Command {
    Command::cargo_bin("elasticctl").unwrap()
}

/// Checking only nonempty output and an `elasticctl` token would accept the
/// wrong shell. Pin each shell to one unique token and one forbidden token.
#[test]
fn completion_emits_the_right_script_for_each_shell() {
    let cases = [
        ("bash", "complete -F _elasticctl", "#compdef"),
        ("zsh", "#compdef elasticctl", "complete -c elasticctl"),
        ("fish", "complete -c elasticctl", "#compdef"),
        (
            "elvish",
            "edit:completion:arg-completer[elasticctl]",
            "#compdef",
        ),
        ("powershell", "Register-ArgumentCompleter", "complete -F"),
    ];

    for (shell, must_contain, must_not_contain) in cases {
        let out = bin().args(["completion", shell]).output().unwrap();
        assert!(out.status.success(), "{shell} completion must succeed");
        let script = String::from_utf8_lossy(&out.stdout);
        assert!(
            script.contains(must_contain),
            "{shell} script must contain {must_contain:?}: {script}"
        );
        assert!(
            !script.contains(must_not_contain),
            "{shell} script must not contain another shell's marker \
             {must_not_contain:?}: {script}"
        );
    }
}

#[test]
fn an_unsupported_shell_exits_two() {
    bin().args(["completion", "tcsh"]).assert().code(2);
}

#[test]
fn the_command_tree_lists_every_top_level_group() {
    let out = bin().args(["commands", "--json"]).output().unwrap();
    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
    let names: Vec<&str> = v["commands"]
        .as_array()
        .unwrap()
        .iter()
        .map(|c| c["name"].as_str().unwrap())
        .collect();
    for expected in [
        "config",
        "doctor",
        "info",
        "rules",
        "state",
        "completion",
        "commands",
    ] {
        assert!(
            names.contains(&expected),
            "command tree must list {expected}: {names:?}"
        );
    }
}

#[test]
fn the_command_tree_marks_which_commands_mutate() {
    let out = bin().args(["commands", "--json"]).output().unwrap();
    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
    let rules = v["commands"]
        .as_array()
        .unwrap()
        .iter()
        .find(|c| c["name"] == "rules")
        .unwrap();
    let subs = rules["subcommands"].as_array().unwrap();

    let find = |n: &str| subs.iter().find(|s| s["name"] == n).unwrap();
    assert_eq!(
        find("delete")["mutates"],
        true,
        "delete must be marked as mutating"
    );
    assert_eq!(find("enable")["mutates"], true);
    assert_eq!(find("list")["mutates"], false, "list is read-only");
    assert_eq!(
        find("preview")["mutates"],
        false,
        "preview writes no alerts"
    );
}

#[test]
fn the_command_tree_records_arguments() {
    let out = bin().args(["commands", "--json"]).output().unwrap();
    let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
    let state = v["commands"]
        .as_array()
        .unwrap()
        .iter()
        .find(|c| c["name"] == "state")
        .unwrap();
    let push = state["subcommands"]
        .as_array()
        .unwrap()
        .iter()
        .find(|s| s["name"] == "push")
        .unwrap();
    let args: Vec<&str> = push["args"]
        .as_array()
        .unwrap()
        .iter()
        .map(|a| a["name"].as_str().unwrap())
        .collect();
    assert!(args.contains(&"dir"), "{args:?}");
    assert!(args.contains(&"report"), "{args:?}");
}