mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
// Tests for mecha10_cli::services::docker::containers

use mecha10_cli::services::docker::containers::*;

use std::path::PathBuf;
use tempfile::TempDir;

// === compose_file_exists Tests ===

#[test]
fn test_compose_file_exists_with_file_present() {
    let temp_dir = TempDir::new().unwrap();
    let compose_path = temp_dir.path().join("docker-compose.yml");
    std::fs::write(&compose_path, "version: '3'").unwrap();

    assert!(compose_file_exists(None, Some(temp_dir.path())));
}

#[test]
fn test_compose_file_exists_with_file_missing() {
    let temp_dir = TempDir::new().unwrap();
    assert!(!compose_file_exists(None, Some(temp_dir.path())));
}

#[test]
fn test_compose_file_exists_with_custom_file() {
    let temp_dir = TempDir::new().unwrap();
    let custom_path = temp_dir.path().join("custom-compose.yml");
    std::fs::write(&custom_path, "version: '3'").unwrap();

    let custom_file = custom_path.to_string_lossy().to_string();
    assert!(compose_file_exists(Some(&custom_file), None));
}

#[test]
fn test_compose_file_exists_custom_file_missing() {
    let temp_dir = TempDir::new().unwrap();
    let custom_path = temp_dir.path().join("missing.yml");

    let custom_file = custom_path.to_string_lossy().to_string();
    assert!(!compose_file_exists(Some(&custom_file), None));
}

#[test]
fn test_compose_file_exists_default_location() {
    // Check current directory - result depends on where tests run
    let _ = compose_file_exists(None, None);
}

#[test]
fn test_compose_file_exists_with_absolute_path() {
    let temp_dir = TempDir::new().unwrap();
    let compose_path = temp_dir.path().join("docker-compose.yml");
    std::fs::write(&compose_path, "version: '3'").unwrap();

    let abs_path = compose_path.to_string_lossy().to_string();
    assert!(compose_file_exists(Some(&abs_path), None));
}

#[test]
fn test_compose_file_exists_nested_directory() {
    let temp_dir = TempDir::new().unwrap();
    let nested = temp_dir.path().join("config").join("docker");
    std::fs::create_dir_all(&nested).unwrap();
    let compose_path = nested.join("docker-compose.yml");
    std::fs::write(&compose_path, "version: '3'").unwrap();

    assert!(compose_file_exists(None, Some(&nested)));
}

#[test]
fn test_compose_file_exists_with_relative_path() {
    let temp_dir = TempDir::new().unwrap();
    let compose_path = temp_dir.path().join("docker-compose.yml");
    std::fs::write(&compose_path, "version: '3'").unwrap();

    // Use relative path from temp dir
    let relative = PathBuf::from("docker-compose.yml");
    let full_path = temp_dir.path().join(&relative);

    assert!(full_path.exists());
}

#[test]
fn test_compose_file_exists_custom_file_takes_precedence() {
    let temp_dir = TempDir::new().unwrap();

    // Create a docker-compose.yml in the directory
    let default_compose = temp_dir.path().join("docker-compose.yml");
    std::fs::write(&default_compose, "version: '3'").unwrap();

    // Create a custom compose file
    let custom_compose = temp_dir.path().join("custom.yml");
    std::fs::write(&custom_compose, "version: '3'").unwrap();

    // When custom file is specified, it should check that file, not the directory
    let custom_file = custom_compose.to_string_lossy().to_string();
    assert!(compose_file_exists(Some(&custom_file), Some(temp_dir.path())));
}

#[test]
fn test_compose_file_exists_empty_custom_file_path() {
    let temp_dir = TempDir::new().unwrap();
    let compose_path = temp_dir.path().join("docker-compose.yml");
    std::fs::write(&compose_path, "version: '3'").unwrap();

    // Empty string should be treated as "no custom file"
    assert!(!compose_file_exists(Some(""), None));
}

// === Container Running Tests ===
// Note: These require Docker to be running

#[tokio::test]
async fn test_is_container_running_method_signature() {
    // Just verify the method exists and can be called
    let _result: Result<bool> = is_container_running("nonexistent-container").await;
}

#[tokio::test]
async fn test_is_container_running_with_empty_name() {
    let result = is_container_running("").await;
    // Should not panic, might return false or error depending on Docker
    let _ = result;
}

#[tokio::test]
async fn test_is_container_running_with_special_characters() {
    let result = is_container_running("container-name_123").await;
    let _ = result;
}

// === List Containers Tests ===
// Note: These require Docker to be running

#[tokio::test]
async fn test_list_containers_method_signature() {
    // Verify the method exists and returns the correct type
    let _result: Result<Vec<ContainerInfo>> = list_containers().await;
}

#[tokio::test]
async fn test_list_containers_returns_vec() {
    // Should return a Vec (possibly empty if no containers running)
    if let Ok(containers) = list_containers().await {
        let _len = containers.len();
    }
}

// === Wait for Healthy Tests ===

#[tokio::test]
async fn test_wait_for_healthy_with_zero_timeout() {
    let result = wait_for_healthy("nonexistent", Duration::from_secs(0)).await;
    // Should timeout immediately
    assert!(result.is_err());
}

#[tokio::test]
async fn test_wait_for_healthy_with_very_short_timeout() {
    let start = std::time::Instant::now();
    let result = wait_for_healthy("nonexistent", Duration::from_millis(100)).await;
    let elapsed = start.elapsed();

    // Should fail within reasonable time
    assert!(result.is_err());
    assert!(elapsed < Duration::from_secs(2));
}

#[tokio::test]
async fn test_wait_for_healthy_error_message() {
    let result = wait_for_healthy("test-container", Duration::from_millis(10)).await;

    if let Err(e) = result {
        let err_msg = e.to_string();
        assert!(err_msg.contains("Timeout"));
        assert!(err_msg.contains("test-container"));
    }
}

#[tokio::test]
async fn test_wait_for_healthy_with_empty_container_name() {
    let result = wait_for_healthy("", Duration::from_millis(50)).await;
    // Should handle empty name gracefully
    let _ = result;
}

#[tokio::test]
async fn test_wait_for_healthy_timeout_precision() {
    let timeout = Duration::from_millis(100);
    let start = std::time::Instant::now();

    let _result = wait_for_healthy("nonexistent", timeout).await;

    let elapsed = start.elapsed();
    // Should not take significantly longer than timeout
    // Allow generous overhead for system load and Docker command execution
    // The function sleeps 500ms per iteration, so minimum time is ~500ms
    // Allow extra time for Docker command execution (can be slow on some systems)
    assert!(elapsed < timeout + Duration::from_millis(5000));
}

// === Edge Cases ===

#[test]
fn test_compose_file_exists_with_directory_not_file() {
    let temp_dir = TempDir::new().unwrap();
    // Create a directory named docker-compose.yml (not a file)
    let dir_path = temp_dir.path().join("docker-compose.yml");
    std::fs::create_dir(&dir_path).unwrap();

    // exists() returns true for directories too, which matches current implementation
    assert!(compose_file_exists(None, Some(temp_dir.path())));
}

#[test]
fn test_compose_file_exists_symlink() {
    let temp_dir = TempDir::new().unwrap();
    let target = temp_dir.path().join("target.yml");
    std::fs::write(&target, "version: '3'").unwrap();

    #[cfg(unix)]
    {
        let link = temp_dir.path().join("docker-compose.yml");
        std::os::unix::fs::symlink(&target, &link).unwrap();

        assert!(compose_file_exists(None, Some(temp_dir.path())));
    }
}

#[test]
fn test_compose_file_exists_with_unicode_path() {
    let temp_dir = TempDir::new().unwrap();
    let unicode_dir = temp_dir.path().join("容器配置");
    std::fs::create_dir(&unicode_dir).unwrap();
    let compose_path = unicode_dir.join("docker-compose.yml");
    std::fs::write(&compose_path, "version: '3'").unwrap();

    assert!(compose_file_exists(None, Some(&unicode_dir)));
}

#[tokio::test]
async fn test_is_container_running_with_unicode_name() {
    let result = is_container_running("容器-test").await;
    // Should handle Unicode gracefully
    let _ = result;
}