whyno-cli 0.5.0

Linux permission debugger
//! Integration tests: flags, error cases, subcommands. No temp files — exercises argument handling and error reporting.

mod helpers;

use helpers::{current_username, run_whyno};

#[test]
fn help_flag_exits_zero() {
    let output = run_whyno(&["--help"]);

    assert_eq!(output.status.code(), Some(0), "expected exit 0 for --help");
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("permission debugger"),
        "help should contain program description"
    );
}

#[test]
fn version_flag_exits_zero() {
    let output = run_whyno(&["--version"]);

    assert_eq!(
        output.status.code(),
        Some(0),
        "expected exit 0 for --version"
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("whyno"),
        "version output should contain binary name"
    );
}

#[test]
fn caps_check_exits_zero() {
    let output = run_whyno(&["caps", "check"]);

    assert_eq!(
        output.status.code(),
        Some(0),
        "expected exit 0 for caps check, stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("CAP_DAC_READ_SEARCH") || stdout.contains("No capabilities"),
        "caps check should report capability status, got: {stdout}"
    );
}

#[test]
fn nonexistent_user_exits_two() {
    let output = run_whyno(&["user:__nonexistent_whyno_test_user__", "read", "/tmp"]);

    assert_eq!(
        output.status.code(),
        Some(2),
        "expected exit 2 for nonexistent user, got stdout: {}",
        String::from_utf8_lossy(&output.stdout)
    );
}

#[test]
fn invalid_operation_exits_two() {
    let output = run_whyno(&[&current_username(), "foobar", "/tmp"]);

    assert_eq!(
        output.status.code(),
        Some(2),
        "expected exit 2 for invalid operation"
    );
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("unknown operation"),
        "error message should mention unknown operation, got: {stderr}"
    );
}

#[test]
fn missing_arguments_exits_cleanly() {
    let output = run_whyno(&[]);

    // clap shows help and exits 0 for some configurations, or exits 2 if
    // arguments are required. either way, verify it doesn't crash.
    let code = output.status.code().expect("should have exit code");
    assert!(
        code == 0 || code == 2,
        "expected exit 0 or 2 for no arguments, got {code}"
    );
}

#[test]
fn relative_path_exits_two() {
    let output = run_whyno(&[&current_username(), "read", "relative/path"]);

    assert_eq!(
        output.status.code(),
        Some(2),
        "expected exit 2 for relative path"
    );
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("not absolute"),
        "error should mention non-absolute path, got: {stderr}"
    );
}

#[test]
fn json_and_explain_conflict_exits_two() {
    let output = run_whyno(&[&current_username(), "read", "/tmp", "--json", "--explain"]);

    assert_eq!(
        output.status.code(),
        Some(2),
        "expected exit 2 for conflicting flags"
    );
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("mutually exclusive"),
        "should report flag conflict, got: {stderr}"
    );
}