whyno-core 0.5.0

Permission check pipeline, fix engine, and state types
Documentation
//! Targeted tests for mount and fsflags fix generator branches.

use super::generators;
use crate::checks::CoreLayer;
use crate::fix::scoring;
use crate::fix::FixAction;
use crate::operation::Operation;
use crate::state::fsflags::FsFlags;
use crate::test_helpers::StateBuilder;

// --- mount_fixes ---

#[test]
fn mount_fixes_noexec_generates_exec_remount() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Execute)
        .mount("/mnt", "tmpfs", "noexec")
        .component("/mnt", 0, 0, 0o755)
        .component_file("/mnt/script.sh", 1000, 1000, 0o755)
        .build();

    let fixes = generators::mount_fixes(&state);
    assert_eq!(fixes.len(), 1);
    assert!(matches!(
        &fixes[0].action,
        FixAction::Remount { options, .. } if options == "exec"
    ));
    assert_eq!(fixes[0].impact, scoring::REMOUNT);
    assert_eq!(fixes[0].layer, CoreLayer::Mount);
}

#[test]
fn mount_fixes_ro_write_generates_rw_remount() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Write)
        .mount("/mnt", "ext4", "ro")
        .component("/mnt", 0, 0, 0o755)
        .component_file("/mnt/data.txt", 1000, 1000, 0o644)
        .build();

    let fixes = generators::mount_fixes(&state);
    assert_eq!(fixes.len(), 1);
    assert!(matches!(
        &fixes[0].action,
        FixAction::Remount { options, .. } if options == "rw"
    ));
}

#[test]
fn mount_fixes_ro_delete_generates_rw_remount() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Delete)
        .mount("/mnt", "ext4", "ro")
        .component("/mnt", 0, 0, 0o755)
        .component_file("/mnt/data.txt", 1000, 1000, 0o644)
        .build();

    let fixes = generators::mount_fixes(&state);
    assert_eq!(fixes.len(), 1);
    assert!(matches!(
        &fixes[0].action,
        FixAction::Remount { options, .. } if options == "rw"
    ));
}

#[test]
fn mount_fixes_ro_create_generates_rw_remount() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Create)
        .mount("/mnt", "ext4", "ro")
        .component("/mnt", 0, 0, 0o755)
        .component_file("/mnt/new.txt", 1000, 1000, 0o644)
        .build();

    let fixes = generators::mount_fixes(&state);
    assert_eq!(fixes.len(), 1);
    assert!(matches!(
        &fixes[0].action,
        FixAction::Remount { options, .. } if options == "rw"
    ));
}

#[test]
fn mount_fixes_no_target_returns_empty_for_single_delete() {
    // Delete with single component -> target_component returns None
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Delete)
        .mount("/", "ext4", "ro")
        .component("/", 0, 0, 0o755)
        .build();

    let fixes = generators::mount_fixes(&state);
    assert!(fixes.is_empty());
}

#[test]
fn mount_fixes_no_mount_resolved_returns_empty() {
    // component_unknown has no mount linked
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Write)
        .component_file("/data.txt", 1000, 1000, 0o644)
        .build();

    let fixes = generators::mount_fixes(&state);
    assert!(fixes.is_empty());
}

// --- fsflags_fixes ---

#[test]
fn fsflags_fixes_append_only_without_immutable() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Write)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_file_with_flags(
            "/append.txt",
            1000,
            1000,
            0o644,
            FsFlags {
                immutable: false,
                append_only: true,
            },
        )
        .build();

    let fixes = generators::fsflags_fixes(&state);
    assert_eq!(fixes.len(), 1);
    assert!(matches!(
        &fixes[0].action,
        FixAction::Chattr { flags, .. } if flags == "-a"
    ));
    assert_eq!(fixes[0].layer, CoreLayer::FsFlags);
}

#[test]
fn fsflags_fixes_immutable_and_append_only_only_immutable_fix() {
    // When immutable=true, append_only fix is skipped (immutable covers it)
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Write)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_file_with_flags(
            "/both.txt",
            1000,
            1000,
            0o644,
            FsFlags {
                immutable: true,
                append_only: true,
            },
        )
        .build();

    let fixes = generators::fsflags_fixes(&state);
    // Only the immutable fix is generated (not append_only when immutable is set)
    assert_eq!(fixes.len(), 1);
    assert!(matches!(
        &fixes[0].action,
        FixAction::Chattr { flags, .. } if flags == "-i"
    ));
}

#[test]
fn fsflags_fixes_no_flags_set_returns_empty() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Write)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_file_with_flags(
            "/normal.txt",
            1000,
            1000,
            0o644,
            FsFlags {
                immutable: false,
                append_only: false,
            },
        )
        .build();

    let fixes = generators::fsflags_fixes(&state);
    assert!(fixes.is_empty());
}

#[test]
fn fsflags_fixes_no_target_single_delete_returns_empty() {
    // Delete with single component -> no parent -> target_component returns None
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Delete)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .build();

    let fixes = generators::fsflags_fixes(&state);
    assert!(fixes.is_empty());
}

#[test]
fn fsflags_fixes_unknown_flags_probe_returns_empty() {
    // component_unknown has Unknown flags probe
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Write)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_unknown("/mystery.txt")
        .build();

    let fixes = generators::fsflags_fixes(&state);
    assert!(fixes.is_empty());
}