tonin-client 0.13.2

Client-side primitives shared between generated tonin service clients: AuthCtx, retry/circuit-breaker config, OTel propagation. Tiny dep tree — peer services depend on this without pulling in the server framework.
Documentation
//! End-to-end deployment tests for tonin using PlatformClient.
//!
//! These tests verify:
//! - Full deployment workflow: deploy ecommerce services to local k3d cluster
//! - Wave ordering: users deployed before orders before products
//! - Health checks: services report healthy status
//! - Trace integration: deployment traces captured via baggage propagation
//!
//! Prerequisites:
//! - k3d cluster running (run: `k3d cluster create tonin-e2e`)
//! - tonin CLI available on $PATH
//! - Workspace configured with ecommerce services (users, orders, products)

use std::{env, process::Command, thread, time::Duration};
use tonin_client::{PlatformClient, PlatformClientConfig};

/// Helper to run a shell command and capture output
fn run_command(cmd: &str, args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
    let output = Command::new(cmd).args(args).output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!("{} failed: {}", cmd, stderr).into());
    }

    Ok(String::from_utf8(output.stdout)?)
}

/// Helper to wait for a service to become healthy.
/// Polls the platform status endpoint up to `max_attempts` times with delay between attempts.
fn wait_for_healthy(
    client: &PlatformClient,
    env: &str,
    service: &str,
    max_attempts: u32,
) -> Result<(), Box<dyn std::error::Error>> {
    for attempt in 1..=max_attempts {
        let statuses = client.get_status(env)?;
        if let Some(status) = statuses.iter().find(|s| s.service == service) {
            if status.health == "healthy" {
                println!("{}: healthy after {} attempts", service, attempt);
                return Ok(());
            }
            println!(
                "{}: health={}, attempt {}/{}",
                service, status.health, attempt, max_attempts
            );
        } else {
            println!(
                "{}: not found in status, attempt {}/{}",
                service, attempt, max_attempts
            );
        }

        if attempt < max_attempts {
            thread::sleep(Duration::from_secs(2));
        }
    }

    Err(format!(
        "Service {} did not become healthy after {} attempts",
        service, max_attempts
    )
    .into())
}

/// Phase 5c Test 1: Deploy ecommerce services in wave order (users → orders → products)
///
/// This test verifies:
/// - Services deploy in correct dependency order
/// - Health checks pass for each wave
/// - Platform status correctly reports health
#[test]
#[ignore] // Requires k3d cluster running; run with: cargo test e2e_deploy_wave_ordering -- --ignored
fn e2e_deploy_wave_ordering() {
    // Check for k3d cluster availability
    if run_command("kubectl", &["cluster-info"]).is_err() {
        eprintln!(
            "WARNING: kubectl not available or k3d cluster not running. \
            Start with: k3d cluster create tonin-e2e"
        );
        return; // Gracefully skip if no cluster
    }

    // Initialize platform client
    let workspace = env::var("TONIN_WORKSPACE").unwrap_or_else(|_| ".".to_string());
    let config = PlatformClientConfig {
        tonin_bin: "tonin".to_string(),
        workspace,
    };
    let client = PlatformClient::with_config(config);

    // Wave 1: Deploy users service (no dependencies)
    println!("\n=== Wave 1: Deploying users service ===");
    match client.deploy_service("e2e-test", "users") {
        Ok(result) => {
            println!(
                "Deploy result: status={}, digest={}",
                result.status, result.digest
            );
            assert_eq!(
                result.status, "success",
                "users service deployment failed: {:?}",
                result.message
            );

            // Wait for users to become healthy
            if let Err(e) = wait_for_healthy(&client, "e2e-test", "users", 30) {
                eprintln!("ERROR: {}", e);
                panic!("users service failed to become healthy");
            }
        }
        Err(e) => {
            eprintln!("Failed to deploy users: {}", e);
            panic!("Deploy failed");
        }
    }

    // Wave 2: Deploy products service (no dependencies)
    println!("\n=== Wave 2: Deploying products service ===");
    match client.deploy_service("e2e-test", "products") {
        Ok(result) => {
            println!(
                "Deploy result: status={}, digest={}",
                result.status, result.digest
            );
            assert_eq!(
                result.status, "success",
                "products service deployment failed: {:?}",
                result.message
            );

            if let Err(e) = wait_for_healthy(&client, "e2e-test", "products", 30) {
                eprintln!("ERROR: {}", e);
                panic!("products service failed to become healthy");
            }
        }
        Err(e) => {
            eprintln!("Failed to deploy products: {}", e);
            panic!("Deploy failed");
        }
    }

    // Wave 3: Deploy orders service (depends on users + products)
    println!("\n=== Wave 3: Deploying orders service ===");
    match client.deploy_service("e2e-test", "orders") {
        Ok(result) => {
            println!(
                "Deploy result: status={}, digest={}",
                result.status, result.digest
            );
            assert_eq!(
                result.status, "success",
                "orders service deployment failed: {:?}",
                result.message
            );

            if let Err(e) = wait_for_healthy(&client, "e2e-test", "orders", 30) {
                eprintln!("ERROR: {}", e);
                panic!("orders service failed to become healthy");
            }
        }
        Err(e) => {
            eprintln!("Failed to deploy orders: {}", e);
            panic!("Deploy failed");
        }
    }

    // Verify all services are healthy
    println!("\n=== Final Status Verification ===");
    match client.get_status("e2e-test") {
        Ok(statuses) => {
            for status in &statuses {
                println!(
                    "Service: {}, Health: {}, Version: {:?}",
                    status.service, status.health, status.running_version
                );
            }

            // Verify each service exists and is healthy
            for service in &["users", "products", "orders"] {
                let found = statuses
                    .iter()
                    .any(|s| s.service == *service && s.health == "healthy");
                assert!(
                    found,
                    "Service {} not found or not healthy in final status",
                    service
                );
            }

            println!("SUCCESS: All services deployed and healthy!");
        }
        Err(e) => {
            panic!("Failed to get status: {}", e);
        }
    }
}

/// Phase 5c Test 2: Test rollback on deployment failure
///
/// This test verifies:
/// - Failed deployments are properly reported
/// - Rollback restores previous state
/// - Status reflects rolled-back state
#[test]
#[ignore] // Requires k3d cluster and pre-existing deployment
fn e2e_rollback_on_failure() {
    // Check for k3d cluster availability
    if run_command("kubectl", &["cluster-info"]).is_err() {
        eprintln!("WARNING: kubectl not available; skipping rollback test");
        return;
    }

    let workspace = env::var("TONIN_WORKSPACE").unwrap_or_else(|_| ".".to_string());
    let config = PlatformClientConfig {
        tonin_bin: "tonin".to_string(),
        workspace,
    };
    let client = PlatformClient::with_config(config);

    // Get initial status
    println!("\n=== Checking pre-deployment status ===");
    let initial_statuses = match client.get_status("e2e-test") {
        Ok(s) => s,
        Err(e) => {
            eprintln!("Cannot query initial status: {}", e);
            return;
        }
    };

    println!("Initial status: {} services", initial_statuses.len());
    for status in &initial_statuses {
        println!("  - {}: {}", status.service, status.health);
    }

    // Deploy and verify
    println!("\n=== Deploying users service ===");
    if let Ok(result) = client.deploy_service("e2e-test", "users") {
        println!("Deployment status: {}", result.status);

        if result.status == "failed" {
            println!("Deployment failed as expected, rolling back...");

            // Attempt rollback
            if client.rollback_service("users", "e2e-test").is_ok() {
                println!("Rollback succeeded");

                // Verify rollback status
                if let Ok(final_statuses) = client.get_status("e2e-test")
                    && let Some(status) = final_statuses.iter().find(|s| s.service == "users")
                {
                    println!("Post-rollback status: {}", status.health);
                }
            }
        }
    }
}

/// Phase 5c Test 3: Deployment tracing with zradar baggage integration
///
/// This test verifies:
/// - Deployment triggers are traced via baggage propagation
/// - Traces contain deployment metadata (service, env, digest)
/// - Traces are queryable in zradar
#[test]
#[ignore] // Requires k3d + zradar tracing setup
fn e2e_deployment_tracing() {
    // This test assumes:
    // - zradar collector is running at ZRADAR_ENDPOINT (or localhost:4317)
    // - Baggage propagation is enabled in tonin
    // - Traces can be queried via zradar API

    let workspace = env::var("TONIN_WORKSPACE").unwrap_or_else(|_| ".".to_string());
    let config = PlatformClientConfig {
        tonin_bin: "tonin".to_string(),
        workspace,
    };
    let client = PlatformClient::with_config(config);

    println!("\n=== Deployment Tracing Test ===");
    println!("This test verifies deployment traces are captured via zradar baggage.");

    // Deploy with baggage context
    // Note: In a full implementation, this would inject baggage headers
    // via the tonin CLI or via environment variables before deployment
    match client.deploy_service("e2e-test", "users") {
        Ok(result) => {
            println!("Deployment succeeded: digest={}", result.digest);

            // In a production scenario, we would:
            // 1. Query zradar API with the trace_id from baggage context
            // 2. Verify deployment span attributes
            // 3. Verify child spans for health checks and log queries

            println!(
                "NOTE: To verify traces, query zradar with digest: {}",
                result.digest
            );
            println!(
                "Example: curl http://localhost:4317/api/traces?service=tonin&attribute=deployment.digest={}",
                result.digest
            );
        }
        Err(e) => {
            eprintln!("Deployment failed: {}", e);
        }
    }
}