whyno-cli 0.2.0

Linux permission debugger
use super::*;
use crate::error::CliError;
use whyno_core::operation::Operation;

// --- parse_cap_name ---

#[test]
fn parse_cap_name_cap_dac_override() {
    assert_eq!(parse_cap_name("CAP_DAC_OVERRIDE").unwrap(), 1u64 << 1);
}

#[test]
fn parse_cap_name_case_insensitive() {
    assert_eq!(parse_cap_name("cap_dac_override").unwrap(), 1u64 << 1);
}

#[test]
fn parse_cap_name_unknown_returns_error() {
    let err = parse_cap_name("CAP_BOGUS").unwrap_err();
    assert_eq!(err, CliError::InvalidCap("CAP_BOGUS".to_string()));
}

#[test]
fn with_cap_flag_parses_multiple() {
    let cli = Cli::try_parse_from([
        "whyno",
        "nginx",
        "read",
        "/path",
        "--with-cap",
        "CAP_DAC_OVERRIDE",
        "--with-cap",
        "CAP_FOWNER",
    ])
    .unwrap();
    assert_eq!(
        cli.with_cap,
        vec!["CAP_DAC_OVERRIDE".to_string(), "CAP_FOWNER".to_string()]
    );
}

// --- parse_subject: bare inputs ---

#[test]
fn parse_subject_bare_username() {
    assert_eq!(
        parse_subject("nginx").unwrap(),
        SubjectInput::Username("nginx".to_string())
    );
}

#[test]
fn parse_subject_bare_number_is_uid() {
    assert_eq!(parse_subject("33").unwrap(), SubjectInput::Uid(33));
}

#[test]
fn parse_subject_bare_zero_is_uid() {
    assert_eq!(parse_subject("0").unwrap(), SubjectInput::Uid(0));
}

// --- parse_subject: prefixed inputs ---

#[test]
fn parse_subject_user_prefix() {
    assert_eq!(
        parse_subject("user:nginx").unwrap(),
        SubjectInput::Username("nginx".to_string())
    );
}

#[test]
fn parse_subject_uid_prefix() {
    assert_eq!(parse_subject("uid:33").unwrap(), SubjectInput::Uid(33));
}

#[test]
fn parse_subject_pid_prefix() {
    assert_eq!(parse_subject("pid:1234").unwrap(), SubjectInput::Pid(1234));
}

#[test]
fn parse_subject_svc_prefix() {
    assert_eq!(
        parse_subject("svc:nginx").unwrap(),
        SubjectInput::Service("nginx".to_string())
    );
}

// --- parse_subject: error cases ---

#[test]
fn parse_subject_uid_non_numeric_is_error() {
    let err = parse_subject("uid:abc").unwrap_err();
    assert_eq!(
        err,
        CliError::InvalidSubject("invalid uid: abc".to_string())
    );
}

#[test]
fn parse_subject_pid_non_numeric_is_error() {
    let err = parse_subject("pid:abc").unwrap_err();
    assert_eq!(
        err,
        CliError::InvalidSubject("invalid pid: abc".to_string())
    );
}

#[test]
fn parse_subject_empty_is_error() {
    let err = parse_subject("").unwrap_err();
    assert_eq!(
        err,
        CliError::InvalidSubject("subject cannot be empty".to_string())
    );
}

#[test]
fn parse_subject_user_prefix_empty_name_is_error() {
    let err = parse_subject("user:").unwrap_err();
    assert_eq!(
        err,
        CliError::InvalidSubject("user: prefix requires a username".to_string())
    );
}

#[test]
fn parse_subject_svc_prefix_empty_name_is_error() {
    let err = parse_subject("svc:").unwrap_err();
    assert_eq!(
        err,
        CliError::InvalidSubject("svc: prefix requires a service name".to_string())
    );
}

#[test]
fn parse_subject_uid_negative_is_error() {
    let err = parse_subject("uid:-1").unwrap_err();
    assert_eq!(err, CliError::InvalidSubject("invalid uid: -1".to_string()));
}

#[test]
fn parse_subject_uid_overflow_is_error() {
    let err = parse_subject("uid:99999999999").unwrap_err();
    assert_eq!(
        err,
        CliError::InvalidSubject("invalid uid: 99999999999".to_string())
    );
}

// --- parse_operation ---

#[test]
fn parse_operation_read() {
    assert_eq!(parse_operation("read").unwrap(), Operation::Read);
}

#[test]
fn parse_operation_write() {
    assert_eq!(parse_operation("write").unwrap(), Operation::Write);
}

#[test]
fn parse_operation_execute() {
    assert_eq!(parse_operation("execute").unwrap(), Operation::Execute);
}

#[test]
fn parse_operation_delete() {
    assert_eq!(parse_operation("delete").unwrap(), Operation::Delete);
}

#[test]
fn parse_operation_create() {
    assert_eq!(parse_operation("create").unwrap(), Operation::Create);
}

#[test]
fn parse_operation_stat() {
    assert_eq!(parse_operation("stat").unwrap(), Operation::Stat);
}

#[test]
fn parse_operation_case_insensitive_upper() {
    assert_eq!(parse_operation("READ").unwrap(), Operation::Read);
}

#[test]
fn parse_operation_case_insensitive_mixed() {
    assert_eq!(parse_operation("Write").unwrap(), Operation::Write);
}

#[test]
fn parse_operation_unknown_is_error() {
    let err = parse_operation("foo").unwrap_err();
    assert_eq!(err, CliError::InvalidOperation("foo".to_string()));
}

#[test]
fn parse_operation_empty_is_error() {
    let err = parse_operation("").unwrap_err();
    assert_eq!(err, CliError::InvalidOperation(String::new()));
}

// --- flag validation ---

#[test]
fn validate_flags_json_and_explain_conflict() {
    let cli = Cli {
        subject: Some("nginx".to_string()),
        operation: Some("read".to_string()),
        path: Some("/tmp".into()),
        json: true,
        explain: true,
        no_color: false,
        self_test: false,
        with_cap: vec![],
        command: None,
    };
    assert_eq!(
        validate_flags(&cli).unwrap_err(),
        CliError::ConflictingFlags
    );
}

#[test]
fn validate_flags_json_only_ok() {
    let cli = Cli {
        subject: Some("nginx".to_string()),
        operation: Some("read".to_string()),
        path: Some("/tmp".into()),
        json: true,
        explain: false,
        no_color: false,
        self_test: false,
        with_cap: vec![],
        command: None,
    };
    assert!(validate_flags(&cli).is_ok());
}

#[test]
fn validate_flags_explain_only_ok() {
    let cli = Cli {
        subject: Some("nginx".to_string()),
        operation: Some("read".to_string()),
        path: Some("/tmp".into()),
        json: false,
        explain: true,
        no_color: false,
        self_test: false,
        with_cap: vec![],
        command: None,
    };
    assert!(validate_flags(&cli).is_ok());
}

#[test]
fn validate_flags_neither_ok() {
    let cli = Cli {
        subject: Some("nginx".to_string()),
        operation: Some("read".to_string()),
        path: Some("/tmp".into()),
        json: false,
        explain: false,
        no_color: false,
        self_test: false,
        with_cap: vec![],
        command: None,
    };
    assert!(validate_flags(&cli).is_ok());
}

// --- clap parsing integration ---

#[test]
fn clap_parse_check_mode() {
    let cli = Cli::try_parse_from(["whyno", "nginx", "read", "/tmp"]).unwrap();
    assert_eq!(cli.subject.as_deref(), Some("nginx"));
    assert_eq!(cli.operation.as_deref(), Some("read"));
    assert_eq!(cli.path.as_deref(), Some(std::path::Path::new("/tmp")));
    assert!(cli.command.is_none());
}

#[test]
fn clap_parse_check_mode_with_json() {
    let cli = Cli::try_parse_from(["whyno", "nginx", "read", "/tmp", "--json"]).unwrap();
    assert!(cli.json);
    assert!(!cli.explain);
}

#[test]
fn clap_parse_check_mode_with_explain() {
    let cli = Cli::try_parse_from(["whyno", "nginx", "read", "/tmp", "--explain"]).unwrap();
    assert!(!cli.json);
    assert!(cli.explain);
}

#[test]
fn clap_parse_caps_install() {
    let cli = Cli::try_parse_from(["whyno", "caps", "install"]).unwrap();
    assert_eq!(
        cli.command,
        Some(Commands::Caps {
            action: CapsAction::Install,
        })
    );
}

#[test]
fn clap_parse_caps_uninstall() {
    let cli = Cli::try_parse_from(["whyno", "caps", "uninstall"]).unwrap();
    assert_eq!(
        cli.command,
        Some(Commands::Caps {
            action: CapsAction::Uninstall,
        })
    );
}

#[test]
fn clap_parse_caps_check() {
    let cli = Cli::try_parse_from(["whyno", "caps", "check"]).unwrap();
    assert_eq!(
        cli.command,
        Some(Commands::Caps {
            action: CapsAction::Check,
        })
    );
}

#[test]
fn clap_parse_schema_subcommand() {
    let cli = Cli::try_parse_from(["whyno", "schema"]).unwrap();
    assert_eq!(cli.command, Some(Commands::Schema));
}

#[test]
fn clap_parse_no_color_long_flag() {
    let cli = Cli::try_parse_from(["whyno", "nginx", "read", "/tmp", "--no-color"]).unwrap();
    assert!(cli.no_color);
}

#[test]
fn clap_parse_no_color_env_alias() {
    // --no-color is the canonical form; NO_COLOR env is the standard.
    // clap derives --no-color from field name `no_color`.
    let cli = Cli::try_parse_from(["whyno", "nginx", "read", "/tmp", "--no-color"]).unwrap();
    assert!(cli.no_color);
}