//! Service Proxy Integration Tests //! //! Tests for backend service proxying with circuit breakers: //! - Connection pooling //! - Circuit breaker activation //! - Request forwarding //! - Health checking #[path = "common/mod.rs"] mod common; use anyhow::Result; use std::time::Duration; #[tokio::test] async fn test_ml_training_proxy_config() -> Result<()> { println!("\n=== Test: ML Training Proxy Configuration ==="); use api_gateway::grpc::server::MlTrainingBackendConfig; let config = MlTrainingBackendConfig::default(); println!(" Default configuration:"); println!(" ├─ Address: {}", config.address); println!(" ├─ Connect timeout: {}ms", config.connect_timeout_ms); println!(" ├─ Request timeout: {}ms", config.request_timeout_ms); println!(" ├─ CB failures: {}", config.circuit_breaker_failures); println!(" └─ CB reset: {}s", config.circuit_breaker_reset_secs); assert_eq!(config.address, "http://localhost:50053"); assert_eq!(config.connect_timeout_ms, 5000); assert_eq!(config.request_timeout_ms, 30000); assert_eq!(config.circuit_breaker_failures, 5); assert_eq!(config.circuit_breaker_reset_secs, 30); Ok(()) } #[tokio::test] async fn test_ml_training_proxy_custom_config() -> Result<()> { println!("\n=== Test: ML Training Proxy Custom Configuration ==="); use api_gateway::grpc::server::MlTrainingBackendConfig; let config = MlTrainingBackendConfig { address: "http://custom-service:9999".to_string(), connect_timeout_ms: 1000, request_timeout_ms: 5000, circuit_breaker_failures: 3, circuit_breaker_reset_secs: 60, tls_ca_cert_path: None, tls_client_cert_path: None, tls_client_key_path: None, }; println!(" Custom configuration:"); println!(" ├─ Address: {}", config.address); println!(" ├─ Connect timeout: {}ms", config.connect_timeout_ms); println!(" ├─ Request timeout: {}ms", config.request_timeout_ms); println!(" ├─ CB failures: {}", config.circuit_breaker_failures); println!(" └─ CB reset: {}s", config.circuit_breaker_reset_secs); assert_eq!(config.address, "http://custom-service:9999"); assert_eq!(config.connect_timeout_ms, 1000); assert_eq!(config.circuit_breaker_failures, 3); Ok(()) } #[tokio::test] async fn test_circuit_breaker_config_validation() -> Result<()> { println!("\n=== Test: Circuit Breaker Configuration Validation ==="); use api_gateway::grpc::server::MlTrainingBackendConfig; let configs = vec![ (1, 5, "Minimal failure threshold"), (5, 10, "Moderate failure threshold"), (10, 30, "High failure threshold"), ]; for (failures, reset_secs, description) in configs { let config = MlTrainingBackendConfig { circuit_breaker_failures: failures, circuit_breaker_reset_secs: reset_secs, ..Default::default() }; println!( " ✓ Valid config: {} (failures={}, reset={}s)", description, config.circuit_breaker_failures, config.circuit_breaker_reset_secs ); assert!(config.circuit_breaker_failures > 0); assert!(config.circuit_breaker_reset_secs > 0); } Ok(()) } #[tokio::test] async fn test_connection_timeout_behavior() -> Result<()> { println!("\n=== Test: Connection Timeout Behavior ==="); use api_gateway::grpc::server::{setup_ml_training_client, MlTrainingBackendConfig}; // Test with invalid address (should timeout) let config = MlTrainingBackendConfig { address: "http://non-existent-service:9999".to_string(), connect_timeout_ms: 100, // Very short timeout ..Default::default() }; println!(" Attempting connection to non-existent service..."); let start = std::time::Instant::now(); let result = setup_ml_training_client(config).await; let elapsed = start.elapsed(); println!(" Connection attempt took: {:?}", elapsed); assert!( result.is_err(), "Connection to non-existent service should fail" ); assert!( elapsed < Duration::from_millis(500), "Should timeout quickly (within 500ms)" ); println!(" ✓ Connection timeout worked correctly"); Ok(()) } #[tokio::test] async fn test_service_proxy_error_handling() -> Result<()> { println!("\n=== Test: Service Proxy Error Handling ==="); use api_gateway::grpc::server::{setup_ml_training_client, MlTrainingBackendConfig}; let test_cases = vec![ ("http://localhost:1", "Connection refused (port 1)"), ("http://192.0.2.1:50053", "Network unreachable (TEST-NET-1)"), ( "http://10.255.255.1:50053", "Connection timeout (non-routable)", ), ]; for (address, description) in test_cases { let config = MlTrainingBackendConfig { address: address.to_string(), connect_timeout_ms: 100, ..Default::default() }; let result = setup_ml_training_client(config).await; assert!(result.is_err(), "{} should fail", description); println!(" ✓ Handled: {}", description); } Ok(()) } #[tokio::test] async fn test_backend_config_serialization() -> Result<()> { println!("\n=== Test: Backend Config Serialization ==="); use api_gateway::grpc::server::MlTrainingBackendConfig; let config = MlTrainingBackendConfig { address: "http://ml-service:50053".to_string(), connect_timeout_ms: 2000, request_timeout_ms: 10000, circuit_breaker_failures: 3, circuit_breaker_reset_secs: 45, tls_ca_cert_path: None, tls_client_cert_path: None, tls_client_key_path: None, }; // Test Debug formatting let debug_str = format!("{:?}", config); assert!(debug_str.contains("ml-service:50053")); assert!(debug_str.contains("2000")); println!(" ✓ Debug format: {}", debug_str); // Test Clone let cloned = config.clone(); assert_eq!(cloned.address, config.address); assert_eq!(cloned.connect_timeout_ms, config.connect_timeout_ms); println!(" ✓ Clone works correctly"); Ok(()) } #[tokio::test] async fn test_multiple_backend_configs() -> Result<()> { println!("\n=== Test: Multiple Backend Service Configurations ==="); use api_gateway::grpc::server::MlTrainingBackendConfig; // Simulate configurations for different environments let dev_config = MlTrainingBackendConfig { address: "http://localhost:50053".to_string(), connect_timeout_ms: 5000, request_timeout_ms: 30000, circuit_breaker_failures: 5, circuit_breaker_reset_secs: 30, tls_ca_cert_path: None, tls_client_cert_path: None, tls_client_key_path: None, }; let staging_config = MlTrainingBackendConfig { address: "http://ml-training-staging:50053".to_string(), connect_timeout_ms: 3000, request_timeout_ms: 20000, circuit_breaker_failures: 3, circuit_breaker_reset_secs: 60, tls_ca_cert_path: None, tls_client_cert_path: None, tls_client_key_path: None, }; let prod_config = MlTrainingBackendConfig { address: "http://ml-training-prod:50053".to_string(), connect_timeout_ms: 2000, request_timeout_ms: 15000, circuit_breaker_failures: 3, circuit_breaker_reset_secs: 120, tls_ca_cert_path: None, tls_client_cert_path: None, tls_client_key_path: None, }; println!(" Development: {}", dev_config.address); println!(" Staging: {}", staging_config.address); println!(" Production: {}", prod_config.address); // Verify configurations are independent assert_ne!( dev_config.connect_timeout_ms, prod_config.connect_timeout_ms ); assert_ne!( staging_config.circuit_breaker_reset_secs, prod_config.circuit_breaker_reset_secs ); println!(" ✓ Multiple environment configurations validated"); Ok(()) } #[tokio::test] async fn test_proxy_performance_overhead() -> Result<()> { println!("\n=== Test: Proxy Configuration Performance ==="); use api_gateway::grpc::server::MlTrainingBackendConfig; let mut config_creation_times = Vec::new(); // Measure config creation overhead for _ in 0..1000 { let start = std::time::Instant::now(); let _config = MlTrainingBackendConfig::default(); let elapsed = start.elapsed(); config_creation_times.push(elapsed); } config_creation_times.sort(); let p50 = config_creation_times[499]; let p99 = config_creation_times[989]; println!("\n Config Creation Performance:"); println!(" ├─ P50: {:?}", p50); println!(" └─ P99: {:?}", p99); assert!( p99 < Duration::from_micros(10), "Config creation should be <10μs" ); println!(" ✓ Config creation overhead is minimal"); Ok(()) } #[tokio::test] async fn test_circuit_breaker_threshold_edge_cases() -> Result<()> { println!("\n=== Test: Circuit Breaker Threshold Edge Cases ==="); use api_gateway::grpc::server::MlTrainingBackendConfig; // Test with threshold of 1 (opens after single failure) let sensitive_config = MlTrainingBackendConfig { circuit_breaker_failures: 1, circuit_breaker_reset_secs: 5, ..Default::default() }; println!(" ✓ Sensitive CB (failures=1): Valid"); assert_eq!(sensitive_config.circuit_breaker_failures, 1); // Test with high threshold (tolerates many failures) let tolerant_config = MlTrainingBackendConfig { circuit_breaker_failures: 100, circuit_breaker_reset_secs: 300, ..Default::default() }; println!(" ✓ Tolerant CB (failures=100): Valid"); assert_eq!(tolerant_config.circuit_breaker_failures, 100); Ok(()) }