whyno-cli 0.3.0

Linux permission debugger
//! Integration tests for whyno check mode (permission checks with tempfiles).
//!
//! Each test creates a temp directory, sets up file permissions, runs
//! The binary via `std::process::Command`, and verifies exit codes + output.

mod helpers;

use std::fs;
use std::os::unix::fs::PermissionsExt;

use helpers::{current_username, run_whyno};
use tempfile::TempDir;

#[test]
fn readable_file_owner_check_exits_zero() {
    let dir = TempDir::new().expect("create tempdir");
    let file = dir.path().join("readable.txt");
    fs::write(&file, "hello").expect("write file");
    fs::set_permissions(&file, fs::Permissions::from_mode(0o644)).expect("chmod");

    let user = current_username();
    let path_str = file.to_str().expect("path to str");
    let output = run_whyno(&[&user, "read", path_str]);

    assert_eq!(
        output.status.code(),
        Some(0),
        "expected exit 0 for readable file, got stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("[PASS]"),
        "expected [PASS] in output, got: {stdout}"
    );
}

#[test]
fn json_mode_produces_valid_json() {
    let dir = TempDir::new().expect("create tempdir");
    let file = dir.path().join("json_test.txt");
    fs::write(&file, "content").expect("write file");
    fs::set_permissions(&file, fs::Permissions::from_mode(0o644)).expect("chmod");

    let user = current_username();
    let path_str = file.to_str().expect("path to str");
    let output = run_whyno(&[&user, "read", path_str, "--json"]);

    assert_eq!(
        output.status.code(),
        Some(0),
        "expected exit 0 for JSON mode, stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    let parsed: serde_json::Value =
        serde_json::from_str(&stdout).expect("stdout should be valid JSON");

    assert_eq!(parsed["version"], 1, "JSON schema version should be 1");
    assert_eq!(
        parsed["result"], "allowed",
        "result should be 'allowed' for readable file"
    );
    assert!(parsed["layers"].is_array(), "layers should be an array");
}

#[test]
fn json_mode_denied_has_fixes() {
    let dir = TempDir::new().expect("create tempdir");
    let file = dir.path().join("denied.txt");
    fs::write(&file, "secret").expect("write file");
    // mode 0o000 ensures denial for everyone (including owner for non-root).
    fs::set_permissions(&file, fs::Permissions::from_mode(0o000)).expect("chmod");

    let user = current_username();
    let path_str = file.to_str().expect("path to str");
    let output = run_whyno(&[&user, "read", path_str, "--json"]);

    let stdout = String::from_utf8_lossy(&output.stdout);
    let parsed: serde_json::Value =
        serde_json::from_str(&stdout).expect("stdout should be valid JSON");

    // for non-root user with mode 0o000, DAC should deny
    if parsed["result"] == "denied" {
        assert_eq!(output.status.code(), Some(1), "denied should exit 1");
        assert!(
            parsed["fixes"].is_array(),
            "denied result should include fixes array"
        );
    }
    // if running as root (uid 0), result may be 'allowed' due to
    // CAP_DAC_OVERRIDE, which is acceptable.
}

#[test]
fn explain_mode_has_sections() {
    let dir = TempDir::new().expect("create tempdir");
    let file = dir.path().join("explain_test.txt");
    fs::write(&file, "content").expect("write file");
    fs::set_permissions(&file, fs::Permissions::from_mode(0o644)).expect("chmod");

    let user = current_username();
    let path_str = file.to_str().expect("path to str");
    let output = run_whyno(&[&user, "read", path_str, "--explain"]);

    assert_eq!(
        output.status.code(),
        Some(0),
        "expected exit 0 for explain mode"
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("=== Subject ==="),
        "explain mode should have Subject section"
    );
    assert!(
        stdout.contains("=== Layer Results ==="),
        "explain mode should have Layer Results section"
    );
    assert!(
        stdout.contains("=== Fix Plan ==="),
        "explain mode should have Fix Plan section"
    );
}