osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
#[cfg(unix)]
use crate::output_support::{first_json_row, parse_json_stdout};
#[cfg(unix)]
use crate::temp_support::TestTempDir;
use assert_cmd::Command;

#[cfg(unix)]
#[test]
fn ldap_user_json_contract() {
    let fixture = LdapPluginFixture::new();

    let mut cmd = fixture.osp();
    cmd.args(["--json", "ldap", "user", "oistes"]);
    let output = cmd.assert().success().get_output().clone();
    let payload = parse_json_stdout(&output.stdout);
    let row = first_json_row(&payload, "ldap user");
    assert_eq!(row["uid"], "oistes");
    assert_eq!(row["cn"], "Mock LDAP User");
    assert_eq!(row["homeDirectory"], "/mock/home/oistes");
    assert_eq!(row["netgroups"], serde_json::json!(["ucore", "usit"]));
    assert!(
        output.stderr.is_empty(),
        "unexpected stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

#[cfg(unix)]
#[test]
fn ldap_user_defaults_to_global_user() {
    let fixture = LdapPluginFixture::new();

    let mut cmd = fixture.osp();
    cmd.args(["-u", "oistes", "--json", "ldap", "user"]);
    let output = cmd.assert().success().get_output().clone();
    let payload = parse_json_stdout(&output.stdout);
    let row = first_json_row(&payload, "ldap user default subject");
    assert_eq!(row["uid"], "oistes");
    assert!(
        output.stderr.is_empty(),
        "unexpected stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

#[cfg(unix)]
#[test]
fn ldap_user_supports_attributes_and_filter() {
    let fixture = LdapPluginFixture::new();

    let mut cmd = fixture.osp();
    cmd.args([
        "-u",
        "oistes",
        "--json",
        "ldap",
        "user",
        "oistes",
        "--filter",
        "uid=oistes",
        "--attributes",
        "uid,cn",
    ]);
    let output = cmd.assert().success().get_output().clone();
    let payload = parse_json_stdout(&output.stdout);
    let row = first_json_row(&payload, "ldap user attribute projection");
    assert_eq!(row["uid"], "oistes");
    assert_eq!(row["cn"], "Mock LDAP User");
    let object = row
        .as_object()
        .expect("ldap projected row should render as an object");
    assert!(!object.contains_key("homeDirectory"));
    assert!(!object.contains_key("netgroups"));
    assert!(
        output.stderr.is_empty(),
        "unexpected stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

#[cfg(unix)]
#[test]
fn ldap_netgroup_json_contract() {
    let fixture = LdapPluginFixture::new();

    let mut cmd = fixture.osp();
    cmd.args(["--json", "ldap", "netgroup", "ucore"]);
    let output = cmd.assert().success().get_output().clone();
    let payload = parse_json_stdout(&output.stdout);
    let row = first_json_row(&payload, "ldap netgroup");
    assert_eq!(row["cn"], "ucore");
    assert_eq!(row["members"], serde_json::json!(["oistes", "trondham"]));
    assert!(
        output.stderr.is_empty(),
        "unexpected stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

#[cfg(unix)]
#[test]
fn ldap_plugin_completes_subcommands_and_flags_contract() {
    let fixture = LdapPluginFixture::new();

    let mut subcommands = fixture.osp();
    subcommands.args(["repl", "debug-complete", "--line", "ldap "]);
    let subcommands_output = subcommands.assert().success().get_output().clone();
    let subcommands_payload = parse_json_stdout(&subcommands_output.stdout);
    let subcommand_matches = subcommands_payload["matches"]
        .as_array()
        .expect("subcommand matches should render as an array");
    assert!(
        subcommand_matches
            .iter()
            .any(|item| item["label"] == "user")
    );
    assert!(
        subcommand_matches
            .iter()
            .any(|item| item["label"] == "netgroup")
    );

    let mut long_flags = fixture.osp();
    long_flags.args(["repl", "debug-complete", "--line", "ldap user --a"]);
    let long_flags_output = long_flags.assert().success().get_output().clone();
    let long_flags_payload = parse_json_stdout(&long_flags_output.stdout);
    let long_flag_matches = long_flags_payload["matches"]
        .as_array()
        .expect("long-flag matches should render as an array");
    assert!(
        long_flag_matches
            .iter()
            .any(|item| item["label"] == "--attributes")
    );

    let mut short_flags = fixture.osp();
    short_flags.args(["repl", "debug-complete", "--line", "ldap user -"]);
    let short_flags_output = short_flags.assert().success().get_output().clone();
    let short_flags_payload = parse_json_stdout(&short_flags_output.stdout);
    let short_flag_matches = short_flags_payload["matches"]
        .as_array()
        .expect("short-flag matches should render as an array");
    assert!(short_flag_matches.iter().any(|item| item["label"] == "-a"));
}

#[cfg(unix)]
struct LdapPluginFixture {
    plugin_dir: TestTempDir,
    home_dir: TestTempDir,
}

#[cfg(unix)]
impl LdapPluginFixture {
    fn new() -> Self {
        use std::os::unix::fs::PermissionsExt;
        let plugin_dir = crate::temp_support::make_temp_dir("osp-cli-ldap-plugin");
        let home_dir = crate::temp_support::make_temp_dir("osp-cli-ldap-home");

        let plugin_path = plugin_dir.join("osp-ldap");
        std::fs::write(&plugin_path, ldap_plugin_script())
            .expect("plugin script should be written");
        let mut perms = std::fs::metadata(&plugin_path)
            .expect("plugin metadata should be readable")
            .permissions();
        perms.set_mode(0o755);
        std::fs::set_permissions(&plugin_path, perms).expect("plugin should be executable");

        Self {
            plugin_dir,
            home_dir,
        }
    }

    fn osp(&self) -> Command {
        let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
        cmd.envs(crate::test_env::isolated_env(&self.home_dir))
            .env("OSP_PLUGIN_PATH", &self.plugin_dir)
            .env("PATH", "/usr/bin:/bin");
        cmd
    }
}

#[cfg(unix)]
fn ldap_plugin_script() -> &'static str {
    r#"#!/usr/bin/env bash
PATH=/usr/bin:/bin:$PATH
set -euo pipefail

if [ "${1:-}" = "--describe" ]; then
  cat <<'JSON'
{"protocol_version":1,"plugin_id":"ldap","plugin_version":"0.1.0","min_osp_version":"0.1.0","commands":[{"name":"ldap","about":"LDAP plugin","args":[],"flags":{},"subcommands":[{"name":"user","about":"Lookup LDAP users","args":[{"name":"uid","about":"User id","multi":false,"value_type":null,"suggestions":[]}],"flags":{"--filter":{"about":"LDAP filter","flag_only":false,"multi":false,"value_type":null,"suggestions":[]},"--attributes":{"about":"Comma-separated attributes","flag_only":false,"multi":false,"value_type":null,"suggestions":[{"value":"uid","meta":"User id","display":null,"sort":null},{"value":"cn","meta":"Common name","display":null,"sort":null}]},"-a":{"about":"Comma-separated attributes","flag_only":false,"multi":false,"value_type":null,"suggestions":[{"value":"uid","meta":"User id","display":null,"sort":null},{"value":"cn","meta":"Common name","display":null,"sort":null}]}},"subcommands":[]},{"name":"netgroup","about":"Lookup LDAP netgroups","args":[{"name":"name","about":"Netgroup name","multi":false,"value_type":null,"suggestions":[]}],"flags":{},"subcommands":[]}]}]}
JSON
  exit 0
fi

selected_command="${OSP_COMMAND:-${1:-}}"
if [ "${1:-}" = "$selected_command" ]; then
  shift || true
fi
cmd="${1:-}"
shift || true

case "$cmd" in
  user)
    uid=""
    filter=""
    attrs=""
    while [ "$#" -gt 0 ]; do
      case "$1" in
        --filter)
          filter="${2:-}"
          shift 2
          ;;
        --attributes|-a)
          attrs="${2:-}"
          shift 2
          ;;
        *)
          if [ -z "$uid" ]; then
            uid="$1"
          fi
          shift
          ;;
      esac
    done

    if [ -z "$uid" ]; then
      uid="oistes"
    fi

    if [[ "$filter" == uid=* ]]; then
      wanted="${filter#uid=}"
      if [ "$wanted" != "$uid" ]; then
        cat <<JSON
{"protocol_version":1,"ok":true,"data":[],"error":null,"meta":{"format_hint":"table","columns":["uid","cn"]}}
JSON
        exit 0
      fi
    fi

    if [ "$attrs" = "uid,cn" ] || [ "$attrs" = "cn,uid" ]; then
      cat <<JSON
{"protocol_version":1,"ok":true,"data":[{"uid":"$uid","cn":"Mock LDAP User"}],"error":null,"meta":{"format_hint":"table","columns":["uid","cn"]}}
JSON
    else
      cat <<JSON
{"protocol_version":1,"ok":true,"data":[{"uid":"$uid","cn":"Mock LDAP User","homeDirectory":"/mock/home/$uid","netgroups":["ucore","usit"]}],"error":null,"meta":{"format_hint":"table","columns":["uid","cn"]}}
JSON
    fi
    ;;

  netgroup)
    name="${1:-ucore}"
    cat <<JSON
{"protocol_version":1,"ok":true,"data":[{"cn":"$name","members":["oistes","trondham"]}],"error":null,"meta":{"format_hint":"table","columns":["cn","members"]}}
JSON
    ;;

  *)
    cat <<JSON
{"protocol_version":1,"ok":false,"data":{},"error":{"code":"UNKNOWN_COMMAND","message":"unknown command: $cmd","details":{}},"meta":{}}
JSON
    ;;
esac
"#
}