guardy 0.2.4

Fast, secure git hooks in Rust with secret scanning and protected file synchronization
Documentation
use std::{env, process::Command};

/// Test sync config environment variable precedence
#[test]
fn test_sync_config_env_vars() {
    let guardy_binary = find_guardy_binary();

    // Test boolean environment variables
    let output = Command::new(&guardy_binary)
        .args(["sync", "status"])
        .env("GUARDY_SYNC_FORCE", "1")
        .env("GUARDY_SYNC_SHOW_DIFF", "0")
        .env("RUST_LOG", "debug")
        .output()
        .expect("Failed to execute guardy");

    let logs = get_command_logs(&output);

    // Verify environment variables were parsed
    assert!(
        logs.contains("Found env var GUARDY_SYNC_FORCE (type: bool): 1"),
        "GUARDY_SYNC_FORCE should be parsed. Output: {logs}"
    );

    assert!(
        logs.contains("Found env var GUARDY_SYNC_SHOW_DIFF (type: bool): 0"),
        "GUARDY_SYNC_SHOW_DIFF should be parsed. Output: {logs}"
    );
}

/// Test sync config numeric environment variables
#[test]
fn test_sync_config_numeric_env_vars() {
    let guardy_binary = find_guardy_binary();

    let output = Command::new(&guardy_binary)
        .args(["sync", "status"])
        .env("GUARDY_SYNC_GIT_TIMEOUT", "60")
        .env("GUARDY_SYNC_DIFF_CONTEXT", "5")
        .env("RUST_LOG", "debug")
        .output()
        .expect("Failed to execute guardy");

    let logs = get_command_logs(&output);

    // Verify numeric environment variables were parsed
    assert!(
        logs.contains("Found env var GUARDY_SYNC_GIT_TIMEOUT"),
        "GUARDY_SYNC_GIT_TIMEOUT should be parsed. Output: {logs}"
    );

    assert!(
        logs.contains("Found env var GUARDY_SYNC_DIFF_CONTEXT"),
        "GUARDY_SYNC_DIFF_CONTEXT should be parsed. Output: {logs}"
    );
}

/// Test sync config string environment variables
#[test]
fn test_sync_config_string_env_vars() {
    let guardy_binary = find_guardy_binary();

    let output = Command::new(&guardy_binary)
        .args(["sync", "status"])
        .env("GUARDY_SYNC_CACHE_DIR", "/tmp/custom_cache")
        .env("RUST_LOG", "debug")
        .output()
        .expect("Failed to execute guardy");

    let logs = get_command_logs(&output);

    // Verify string environment variables were parsed
    assert!(
        logs.contains("Found env var GUARDY_SYNC_CACHE_DIR"),
        "GUARDY_SYNC_CACHE_DIR should be parsed. Output: {logs}"
    );
}

/// Test CLI args override environment variables
#[test]
fn test_sync_cli_overrides_env() {
    let guardy_binary = find_guardy_binary();

    // Set force=false in env, but use --force in CLI
    let output = Command::new(&guardy_binary)
        .args(["sync", "update", "--force"])
        .env("GUARDY_SYNC_FORCE", "0")
        .env("RUST_LOG", "debug")
        .output()
        .expect("Failed to execute guardy");

    let logs = get_command_logs(&output);

    // CLI --force should override env GUARDY_SYNC_FORCE=0
    // This test verifies CLI precedence over environment variables
    assert!(
        output.status.success() || logs.contains("No sync configuration"),
        "Command should handle CLI override of env vars. Output: {logs}"
    );
}

/// Test that boolean environment variables support various formats
#[test]
fn test_sync_boolean_env_formats() {
    let guardy_binary = find_guardy_binary();

    // Test truthy values
    for truthy_val in ["1", "true", "TRUE", "yes", "YES", "on", "ON"] {
        let output = Command::new(&guardy_binary)
            .args(["sync", "status"])
            .env("GUARDY_SYNC_FORCE", truthy_val)
            .env("RUST_LOG", "debug")
            .output()
            .expect("Failed to execute guardy");

        let logs = get_command_logs(&output);
        assert!(
            logs.contains("Parsed GUARDY_SYNC_FORCE as boolean: true"),
            "Value '{truthy_val}' should be parsed as true. Output: {logs}"
        );
    }

    // Test falsy values
    for falsy_val in ["0", "false", "FALSE", "no", "NO", "off", "OFF"] {
        let output = Command::new(&guardy_binary)
            .args(["sync", "status"])
            .env("GUARDY_SYNC_FORCE", falsy_val)
            .env("RUST_LOG", "debug")
            .output()
            .expect("Failed to execute guardy");

        let logs = get_command_logs(&output);
        assert!(
            logs.contains("Parsed GUARDY_SYNC_FORCE as boolean: false"),
            "Value '{falsy_val}' should be parsed as false. Output: {logs}"
        );
    }
}

/// Test sync config with file overrides using YAML format
#[test]
fn test_sync_config_file_override() {
    use std::fs;
    use tempfile::tempdir;

    let guardy_binary = find_guardy_binary();
    let temp_dir = tempdir().expect("Failed to create temp dir");
    let config_file = temp_dir.path().join("guardy.yaml");

    // Write test config file with sync settings
    let config_content = r#"
sync:
  force: true
  show-diff: false  # Test dash format
  git_timeout_seconds: 120  # Test underscore format
  cache_dir: "/tmp/test_cache"
  repolist: []
"#;
    fs::write(&config_file, config_content).expect("Failed to write config file");

    let output = Command::new(&guardy_binary)
        .args(["sync", "status", "--config", config_file.to_str().unwrap()])
        .env("RUST_LOG", "debug")
        .current_dir(temp_dir.path())
        .output()
        .expect("Failed to execute guardy");

    let logs = get_command_logs(&output);

    // Verify file config was loaded and applied
    // The exact debug output format may vary, but config should be loaded
    assert!(
        output.status.success() || logs.contains("No sync configuration"),
        "Config file should be loaded successfully. Output: {logs}"
    );
}

// Helper functions

fn find_guardy_binary() -> String {
    env::var("CARGO_BIN_EXE_guardy")
        .or_else(|_| {
            let paths = [
                "target/release/guardy",
                "target/debug/guardy",
                "../../target/release/guardy",
                "../../target/debug/guardy",
                "../../../target/release/guardy",
                "../../../target/debug/guardy",
                "../../../../target/release/guardy",
                "../../../../target/debug/guardy",
                "../../../guardy/target/release/guardy",
                "../../../guardy/target/debug/guardy",
            ];
            for path in &paths {
                let path_buf = std::path::Path::new(path);
                if path_buf.exists() {
                    // Convert to absolute path to avoid execution issues
                    match path_buf.canonicalize() {
                        Ok(abs_path) => return Ok(abs_path.to_string_lossy().to_string()),
                        Err(_) => continue,
                    }
                }
            }
            Err("Could not find guardy binary")
        })
        .expect("Failed to locate guardy binary for testing")
}

fn get_command_logs(output: &std::process::Output) -> String {
    let stderr = String::from_utf8_lossy(&output.stderr);
    let stdout = String::from_utf8_lossy(&output.stdout);
    format!("{stdout}{stderr}")
}