sal-rhai 0.1.0

SAL Rhai - Rhai scripting integration for the System Abstraction Layer
Documentation
//! Tests for sal-rhai error handling functionality
//!
//! These tests verify that error handling works correctly across all SAL modules.

use rhai::Engine;
use sal_rhai::{
    error::{SalError, ToRhaiError},
    register,
};
use std::error::Error;

/// Test SalError creation and display
#[test]
fn test_sal_error_creation() {
    let error = SalError::new("TestError", "This is a test error message");
    assert_eq!(error.to_string(), "TestError: This is a test error message");

    let fs_error = SalError::FsError("File system operation failed".to_string());
    assert_eq!(
        fs_error.to_string(),
        "File system error: File system operation failed"
    );

    let download_error = SalError::DownloadError("Download failed".to_string());
    assert_eq!(
        download_error.to_string(),
        "Download error: Download failed"
    );

    let package_error = SalError::PackageError("Package installation failed".to_string());
    assert_eq!(
        package_error.to_string(),
        "Package error: Package installation failed"
    );
}

/// Test SalError conversion to Rhai error
#[test]
fn test_sal_error_to_rhai_conversion() {
    let sal_error = SalError::new("TestError", "Test message");
    let rhai_error: Box<rhai::EvalAltResult> = sal_error.into();

    let error_str = rhai_error.to_string();
    assert!(
        error_str.contains("TestError: Test message"),
        "Error message should be preserved: {}",
        error_str
    );
}

/// Test error handling in file operations
#[test]
fn test_file_operation_errors() {
    let mut engine = Engine::new();
    register(&mut engine).expect("Failed to register SAL modules");

    // Test accessing non-existent file
    let result = engine.eval::<i64>(r#"file_size("definitely_nonexistent_file_xyz123.txt")"#);
    assert!(result.is_err(), "Should return error for non-existent file");

    let error = result.unwrap_err();
    let error_str = error.to_string();
    assert!(
        error_str.contains("No files found")
            || error_str.contains("File not found")
            || error_str.contains("File system error"),
        "Error should indicate file issue: {}",
        error_str
    );
}

/// Test error handling in process operations
#[test]
fn test_process_operation_errors() {
    let mut engine = Engine::new();
    register(&mut engine).expect("Failed to register SAL modules");

    // Test running non-existent command
    let result =
        engine.eval::<rhai::Dynamic>(r#"run_command("definitely_nonexistent_command_xyz123")"#);
    // Note: This might not always fail depending on the system, so we check if it's handled gracefully
    if result.is_err() {
        let error = result.unwrap_err();
        let error_str = error.to_string();
        assert!(!error_str.is_empty(), "Error message should not be empty");
    }
}

/// Test error handling in text operations
#[test]
fn test_text_operation_errors() {
    let mut engine = Engine::new();
    register(&mut engine).expect("Failed to register SAL modules");

    // Test text operations with invalid input (most text operations are quite robust)
    // Test template rendering with invalid template
    let result = engine.eval::<String>(
        r#"
        let builder = template_builder_new();
        builder = template_string(builder, "{{ invalid_syntax }}");
        let template = build_template(builder);
        render_template(template, #{})
    "#,
    );

    // This should either work or fail gracefully
    if result.is_err() {
        let error = result.unwrap_err();
        let error_str = error.to_string();
        assert!(!error_str.is_empty(), "Error message should not be empty");
    }
}

/// Test error handling in network operations
#[test]
fn test_network_operation_errors() {
    let mut engine = Engine::new();
    register(&mut engine).expect("Failed to register SAL modules");

    // Test connecting to invalid host
    let result = engine.eval::<bool>(r#"tcp_check("invalid.host.that.does.not.exist.xyz", 80)"#);
    assert!(
        result.is_ok(),
        "TCP check should handle invalid hosts gracefully"
    );
    // Should return false for invalid hosts (or might return true if DNS resolves)
    let tcp_result = result.unwrap();
    assert!(
        tcp_result == false || tcp_result == true,
        "Should return a boolean value"
    );

    // Test HTTP request to invalid URL
    let result =
        engine.eval::<String>(r#"http_get("http://invalid.host.that.does.not.exist.xyz")"#);
    // This should either return an error response or handle gracefully
    if result.is_err() {
        let error = result.unwrap_err();
        let error_str = error.to_string();
        assert!(!error_str.is_empty(), "Error message should not be empty");
    }
}

/// Test error handling in git operations
#[test]
fn test_git_operation_errors() {
    let mut engine = Engine::new();
    register(&mut engine).expect("Failed to register SAL modules");

    // Test git operations with invalid repository
    let result = engine
        .eval::<rhai::Dynamic>(r#"git_clone("invalid://not.a.real.repo.xyz", "/tmp/nonexistent")"#);
    // Git operations should handle invalid URLs gracefully
    if result.is_err() {
        let error = result.unwrap_err();
        let error_str = error.to_string();
        assert!(!error_str.is_empty(), "Error message should not be empty");
    }
}

/// Test error handling in crypto operations
#[test]
fn test_crypto_operation_errors() {
    let mut engine = Engine::new();
    register(&mut engine).expect("Failed to register SAL modules");

    // Test crypto operations with invalid input
    let result = engine.eval::<String>(r#"decrypt("invalid_encrypted_data", "wrong_key")"#);
    // Crypto operations should handle invalid input gracefully
    if result.is_err() {
        let error = result.unwrap_err();
        let error_str = error.to_string();
        assert!(!error_str.is_empty(), "Error message should not be empty");
    }
}

/// Test error handling in database operations
#[test]
fn test_database_operation_errors() {
    let mut engine = Engine::new();
    register(&mut engine).expect("Failed to register SAL modules");

    // Test Redis operations with invalid connection
    let result = engine.eval::<String>(r#"redis_get("nonexistent_key")"#);
    // Database operations should handle connection issues gracefully
    if result.is_err() {
        let error = result.unwrap_err();
        let error_str = error.to_string();
        assert!(!error_str.is_empty(), "Error message should not be empty");
    }
}

/// Test error handling in virtualization operations
#[test]
fn test_virt_operation_errors() {
    let mut engine = Engine::new();
    register(&mut engine).expect("Failed to register SAL modules");

    // Test buildah operations without buildah installed
    let result = engine.eval::<rhai::Dynamic>(
        r#"
        let builder = bah_new();
        builder
    "#,
    );

    // This should work even if buildah is not installed (returns builder object)
    // If the function is not found, that's also acceptable for this test
    if result.is_err() {
        let error_str = result.unwrap_err().to_string();
        assert!(
            error_str.contains("ErrorFunctionNotFound") || error_str.contains("Function not found"),
            "Should be a function not found error: {}",
            error_str
        );
    } else {
        // If it works, that's fine too
        assert!(
            result.is_ok(),
            "Builder creation should work if function is available"
        );
    }
}

/// Test error propagation through exec function
#[test]
fn test_exec_error_propagation() {
    let mut engine = Engine::new();
    register(&mut engine).expect("Failed to register SAL modules");

    // Test that errors from SAL functions are properly propagated through exec
    let result = engine.eval::<i64>(r#"exec(`file_size("nonexistent_file_xyz123.txt")`)"#);
    assert!(result.is_err(), "Errors should propagate through exec");

    let error = result.unwrap_err();
    let error_str = error.to_string();
    assert!(
        error_str.contains("No files found")
            || error_str.contains("File not found")
            || error_str.contains("File system error")
            || error_str.contains("Invalid character"),
        "Error should indicate file issue: {}",
        error_str
    );
}

/// Test ToRhaiError trait with different error types
#[test]
fn test_to_rhai_error_different_types() {
    // Test with std::io::Error
    let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Permission denied");
    let result: Result<(), std::io::Error> = Err(io_error);
    let rhai_result = result.to_rhai_error();
    assert!(rhai_result.is_err());
    assert!(rhai_result
        .unwrap_err()
        .to_string()
        .contains("Permission denied"));

    // Test with custom error type
    #[derive(Debug)]
    struct CustomError(String);

    impl std::fmt::Display for CustomError {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "Custom error: {}", self.0)
        }
    }

    impl Error for CustomError {}

    let custom_error = CustomError("test error".to_string());
    let result: Result<(), CustomError> = Err(custom_error);
    let rhai_result = result.to_rhai_error();
    assert!(rhai_result.is_err());
    assert!(rhai_result
        .unwrap_err()
        .to_string()
        .contains("Custom error: test error"));
}

/// Test error handling with concurrent operations
#[test]
fn test_concurrent_error_handling() {
    use std::sync::Arc;
    use std::thread;

    let mut engine = Engine::new();
    register(&mut engine).expect("Failed to register SAL modules");

    // Test that error handling works correctly in multi-threaded context
    let engine = Arc::new(engine);
    let handles: Vec<_> = (0..5)
        .map(|i| {
            let engine = Arc::clone(&engine);
            thread::spawn(move || {
                let result =
                    engine.eval::<i64>(&format!(r#"file_size("nonexistent_file_{}.txt")"#, i));
                assert!(result.is_err(), "Thread {} should return error", i);
            })
        })
        .collect();

    for handle in handles {
        handle.join().expect("Thread should complete successfully");
    }
}

/// Test error message formatting and consistency
#[test]
fn test_error_message_consistency() {
    let mut engine = Engine::new();
    register(&mut engine).expect("Failed to register SAL modules");

    // Test that similar errors have consistent formatting
    let errors = vec![
        engine.eval::<i64>(r#"file_size("nonexistent1.txt")"#),
        engine.eval::<i64>(r#"file_size("nonexistent2.txt")"#),
        engine.eval::<i64>(r#"file_size("nonexistent3.txt")"#),
    ];

    for (i, result) in errors.iter().enumerate() {
        assert!(result.is_err(), "Error {} should fail", i);
        let error_str = result.as_ref().unwrap_err().to_string();
        assert!(
            !error_str.is_empty(),
            "Error message {} should not be empty",
            i
        );
        // All should contain similar error patterns
        assert!(
            error_str.contains("No files found")
                || error_str.contains("File not found")
                || error_str.contains("File system error"),
            "Error {} should have consistent format: {}",
            i,
            error_str
        );
    }
}