forjar 1.4.2

Rust-native Infrastructure as Code — bare-metal first, BLAKE3 state, provenance tracing
Documentation
//! Tests: Coverage for fleet_ops and fleet_reporting.

use super::fleet_ops::*;
use super::fleet_reporting::*;

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::Path;

    fn write_yaml(dir: &Path, name: &str, content: &str) -> std::path::PathBuf {
        let p = dir.join(name);
        if let Some(parent) = p.parent() {
            std::fs::create_dir_all(parent).unwrap();
        }
        std::fs::write(&p, content).unwrap();
        p
    }

    // ── Minimal config YAML snippets ──

    fn minimal_config_yaml() -> &'static str {
        "version: \"1.0\"\nname: t\nmachines:\n  m:\n    hostname: m\n    addr: 127.0.0.1\nresources:\n  a:\n    type: file\n    machine: m\n    path: /tmp/a\n    content: a\n"
    }

    fn state_lock_yaml() -> &'static str {
        "schema: \"1.0\"\nmachine: web\nhostname: web\ngenerated_at: \"2026-02-28T00:00:00Z\"\ngenerator: forjar\nblake3_version: \"1.8\"\nresources:\n  f:\n    type: file\n    status: converged\n    hash: \"blake3:abc\"\n"
    }

    fn state_lock_yaml_db() -> &'static str {
        "schema: \"1.0\"\nmachine: db\nhostname: db\ngenerated_at: \"2026-02-28T00:00:00Z\"\ngenerator: forjar\nblake3_version: \"1.8\"\nresources:\n  g:\n    type: service\n    status: converged\n    hash: \"blake3:def\"\n"
    }

    fn state_lock_yaml_cache() -> &'static str {
        "schema: \"1.0\"\nmachine: cache\nhostname: cache\ngenerated_at: \"2026-02-28T00:00:00Z\"\ngenerator: forjar\nblake3_version: \"1.8\"\nresources:\n  h:\n    type: file\n    status: failed\n    hash: \"blake3:ghi\"\n"
    }

    // ========================================================================
    // cmd_retry_failed tests
    // ========================================================================

    #[test]
    fn test_cov_retry_failed_empty_state_dir() {
        let dir = tempfile::tempdir().unwrap();
        let state_dir = dir.path().join("state");
        std::fs::create_dir_all(&state_dir).unwrap();
        let cfg = write_yaml(dir.path(), "forjar.yaml", minimal_config_yaml());
        let result = cmd_retry_failed(&cfg, &state_dir, &[], None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_retry_failed_no_event_logs() {
        let dir = tempfile::tempdir().unwrap();
        let state_dir = dir.path().join("state");
        // Create machine dir without events.jsonl
        std::fs::create_dir_all(state_dir.join("m")).unwrap();
        let cfg = write_yaml(dir.path(), "forjar.yaml", minimal_config_yaml());
        let result = cmd_retry_failed(&cfg, &state_dir, &[], None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_retry_failed_with_single_line_event_log() {
        let dir = tempfile::tempdir().unwrap();
        let state_dir = dir.path().join("state");
        std::fs::create_dir_all(state_dir.join("m")).unwrap();
        // Single event line (no ApplyCompleted) — exercises boundary
        let events = r#"{"ts":"2026-01-01T00:00:00Z","event":"apply_started","machine":"m","run_id":"r1","forjar_version":"0.1"}"#;
        write_yaml(&state_dir, "m/events.jsonl", events);
        let cfg = write_yaml(dir.path(), "forjar.yaml", minimal_config_yaml());
        let result = cmd_retry_failed(&cfg, &state_dir, &[], None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_retry_failed_with_converged_events_only() {
        let dir = tempfile::tempdir().unwrap();
        let state_dir = dir.path().join("state");
        std::fs::create_dir_all(state_dir.join("m")).unwrap();
        let events = concat!(
            r#"{"ts":"2026-01-01T00:00:00Z","event":"apply_started","machine":"m","run_id":"r1","forjar_version":"0.1"}"#,
            "\n",
            r#"{"ts":"2026-01-01T00:00:01Z","event":"resource_converged","machine":"m","resource":"a","duration_seconds":0.5,"hash":"abc"}"#,
            "\n",
            r#"{"ts":"2026-01-01T00:00:02Z","event":"apply_completed","machine":"m","run_id":"r1","resources_converged":1,"resources_unchanged":0,"resources_failed":0,"total_seconds":2.0}"#,
            "\n",
        );
        write_yaml(&state_dir, "m/events.jsonl", events);
        let cfg = write_yaml(dir.path(), "forjar.yaml", minimal_config_yaml());
        let result = cmd_retry_failed(&cfg, &state_dir, &[], None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_retry_failed_invalid_config() {
        let dir = tempfile::tempdir().unwrap();
        let state_dir = dir.path().join("state");
        std::fs::create_dir_all(&state_dir).unwrap();
        let cfg = write_yaml(dir.path(), "bad.yaml", "not: valid: yaml: [[[");
        let result = cmd_retry_failed(&cfg, &state_dir, &[], None);
        assert!(result.is_err());
    }

    #[test]
    fn test_cov_retry_failed_with_timeout() {
        let dir = tempfile::tempdir().unwrap();
        let state_dir = dir.path().join("state");
        std::fs::create_dir_all(&state_dir).unwrap();
        let cfg = write_yaml(dir.path(), "forjar.yaml", minimal_config_yaml());
        let result = cmd_retry_failed(&cfg, &state_dir, &[], Some(10));
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_retry_failed_with_params() {
        let dir = tempfile::tempdir().unwrap();
        let state_dir = dir.path().join("state");
        std::fs::create_dir_all(&state_dir).unwrap();
        let cfg = write_yaml(dir.path(), "forjar.yaml", minimal_config_yaml());
        let result = cmd_retry_failed(&cfg, &state_dir, &["x=y".to_string()], None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_retry_failed_with_malformed_event_lines() {
        let dir = tempfile::tempdir().unwrap();
        let state_dir = dir.path().join("state");
        std::fs::create_dir_all(state_dir.join("m")).unwrap();
        let events = concat!(
            "this is not json\n",
            "{\"broken json\n",
            r#"{"ts":"2026-01-01T00:00:00Z","event":"apply_completed","machine":"m","run_id":"r1","resources_converged":0,"resources_unchanged":0,"resources_failed":0,"total_seconds":0.0}"#,
            "\n",
        );
        write_yaml(&state_dir, "m/events.jsonl", events);
        let cfg = write_yaml(dir.path(), "forjar.yaml", minimal_config_yaml());
        let result = cmd_retry_failed(&cfg, &state_dir, &[], None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_retry_failed_event_log_with_failed_resource() {
        let dir = tempfile::tempdir().unwrap();
        let state_dir = dir.path().join("state");
        std::fs::create_dir_all(state_dir.join("m")).unwrap();
        let events = concat!(
            r#"{"ts":"2026-01-01T00:00:00Z","event":"apply_started","machine":"m","run_id":"r1","forjar_version":"0.1"}"#,
            "\n",
            r#"{"ts":"2026-01-01T00:00:01Z","event":"resource_failed","machine":"m","resource":"a","error":"timeout"}"#,
            "\n",
            r#"{"ts":"2026-01-01T00:00:02Z","event":"apply_completed","machine":"m","run_id":"r1","resources_converged":0,"resources_unchanged":0,"resources_failed":1,"total_seconds":2.0}"#,
            "\n",
        );
        write_yaml(&state_dir, "m/events.jsonl", events);
        let cfg = write_yaml(dir.path(), "forjar.yaml", minimal_config_yaml());
        // This will find the failed resource and try to retry, which calls cmd_apply
        // on a local machine -- it may succeed or fail depending on the resource,
        // but the function's branching is exercised.
        let _result = cmd_retry_failed(&cfg, &state_dir, &[], None);
    }

    // ========================================================================
    // cmd_export tests
    // ========================================================================

    #[test]
    fn test_cov_export_csv_empty_state() {
        let dir = tempfile::tempdir().unwrap();
        let result = cmd_export(dir.path(), "csv", None, None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_export_csv_with_data() {
        let dir = tempfile::tempdir().unwrap();
        write_yaml(dir.path(), "web/state.lock.yaml", state_lock_yaml());
        let result = cmd_export(dir.path(), "csv", None, None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_export_terraform_empty() {
        let dir = tempfile::tempdir().unwrap();
        let result = cmd_export(dir.path(), "terraform", None, None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_export_terraform_with_data() {
        let dir = tempfile::tempdir().unwrap();
        write_yaml(dir.path(), "web/state.lock.yaml", state_lock_yaml());
        let result = cmd_export(dir.path(), "terraform", None, None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_export_ansible_empty() {
        let dir = tempfile::tempdir().unwrap();
        let result = cmd_export(dir.path(), "ansible", None, None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_export_ansible_with_data() {
        let dir = tempfile::tempdir().unwrap();
        write_yaml(dir.path(), "web/state.lock.yaml", state_lock_yaml());
        let result = cmd_export(dir.path(), "ansible", None, None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_export_unknown_format() {
        let dir = tempfile::tempdir().unwrap();
        let result = cmd_export(dir.path(), "xml", None, None);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("Unknown export format"));
    }

    #[test]
    fn test_cov_export_csv_machine_filter() {
        let dir = tempfile::tempdir().unwrap();
        write_yaml(dir.path(), "web/state.lock.yaml", state_lock_yaml());
        write_yaml(dir.path(), "db/state.lock.yaml", state_lock_yaml_db());
        let result = cmd_export(dir.path(), "csv", Some("web"), None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_export_csv_machine_filter_no_match() {
        let dir = tempfile::tempdir().unwrap();
        write_yaml(dir.path(), "web/state.lock.yaml", state_lock_yaml());
        let result = cmd_export(dir.path(), "csv", Some("nonexistent"), None);
        assert!(result.is_ok());
    }

    #[test]
    fn test_cov_export_csv_to_file() {
        let dir = tempfile::tempdir().unwrap();
        write_yaml(dir.path(), "web/state.lock.yaml", state_lock_yaml());
        let out = dir.path().join("export.csv");
        let result = cmd_export(dir.path(), "csv", None, Some(&out));
        assert!(result.is_ok());
        assert!(out.exists());
    }

    #[test]
    fn test_cov_export_terraform_to_file() {
        let dir = tempfile::tempdir().unwrap();
        write_yaml(dir.path(), "web/state.lock.yaml", state_lock_yaml());
        let out = dir.path().join("export.tf");
        let result = cmd_export(dir.path(), "terraform", None, Some(&out));
        assert!(result.is_ok());
        assert!(out.exists());
    }

    #[test]
    fn test_cov_export_ansible_to_file() {
        let dir = tempfile::tempdir().unwrap();
        write_yaml(dir.path(), "web/state.lock.yaml", state_lock_yaml());
        let out = dir.path().join("inventory.yaml");
        let result = cmd_export(dir.path(), "ansible", None, Some(&out));
        assert!(result.is_ok());
        assert!(out.exists());
    }

    #[test]
    fn test_cov_export_multi_machine() {
        let dir = tempfile::tempdir().unwrap();
        write_yaml(dir.path(), "web/state.lock.yaml", state_lock_yaml());
        write_yaml(dir.path(), "db/state.lock.yaml", state_lock_yaml_db());
        write_yaml(dir.path(), "cache/state.lock.yaml", state_lock_yaml_cache());
        let result = cmd_export(dir.path(), "csv", None, None);
        assert!(result.is_ok());
    }
}