#![cfg(feature = "__integration_tests")] #![allow(unexpected_cfgs)] //! End-to-End Integration Tests: API Gateway → ML Training Service //! //! This test suite validates the complete ML training service flow through the API Gateway: //! - Training job management (start, stop, list) //! - Real-time training progress monitoring //! - Resource utilization tracking //! - Training configuration validation //! - Model deployment workflow //! //! Test Count: 12 tests //! Coverage: Full ML training service integration use anyhow::Result; use chrono::{Duration, Utc}; use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration as StdDuration; use tokio::time::timeout; use tonic::{metadata::MetadataValue, transport::Channel, Request}; use uuid::Uuid; // Generated proto code pub mod ml { tonic::include_proto!("ml"); } use ml::{ ml_training_service_client::MlTrainingServiceClient, ListTrainingJobsRequest, ResourceRequest, ResourceRequirements, StartTrainingRequest, StopTrainingRequest, TrainingConfigRequest, TrainingHyperparameters, TrainingStatus, TrainingTemplatesRequest, WatchTrainingRequest, }; const API_GATEWAY_ADDR: &str = "http://localhost:50050"; const JWT_SECRET: &str = "dev_secret_key_change_in_production"; #[derive(Debug, Serialize, Deserialize)] struct Claims { sub: String, exp: usize, iat: usize, roles: Vec, permissions: Vec, session_id: Option, jti: String, } /// Generate a test JWT token for authentication fn generate_test_token( user_id: &str, roles: Vec, permissions: Vec, ) -> Result { let now = Utc::now(); let claims = Claims { sub: user_id.to_string(), exp: (now + Duration::hours(1)).timestamp() as usize, iat: now.timestamp() as usize, roles, permissions, session_id: Some(Uuid::new_v4().to_string()), jti: Uuid::new_v4().to_string(), }; let token = encode( &Header::new(Algorithm::HS256), &claims, &EncodingKey::from_secret(JWT_SECRET.as_ref()), )?; Ok(token) } /// Create an authenticated ML training service client async fn create_authenticated_client() -> Result< MlTrainingServiceClient< tonic::service::interceptor::InterceptedService< Channel, impl Fn(Request<()>) -> Result, tonic::Status> + Clone, >, >, > { let user_id = "test_ml_engineer_001"; let role = "ml_engineer"; let token = generate_test_token( user_id, vec![role.to_string()], vec![ "api.access".to_string(), "ml.train".to_string(), "ml.view".to_string(), "ml.manage".to_string(), ], )?; let channel = Channel::from_static(API_GATEWAY_ADDR).connect().await?; // Create interceptor that injects JWT token AND user context into request metadata let user_id_owned = user_id.to_string(); let role_owned = role.to_string(); let interceptor = move |mut req: Request<()>| -> Result, tonic::Status> { // JWT token in authorization header let token_value = format!("Bearer {}", token); let metadata_value = MetadataValue::try_from(token_value) .map_err(|_| tonic::Status::internal("Failed to create metadata value"))?; req.metadata_mut().insert("authorization", metadata_value); // User context in metadata headers let user_id_value = MetadataValue::try_from(user_id_owned.clone()) .map_err(|_| tonic::Status::internal("Failed to create user_id metadata"))?; req.metadata_mut().insert("x-user-id", user_id_value); let role_value = MetadataValue::try_from(role_owned.clone()) .map_err(|_| tonic::Status::internal("Failed to create role metadata"))?; req.metadata_mut().insert("x-user-role", role_value); Ok(req) }; let client = MlTrainingServiceClient::with_interceptor(channel, interceptor); Ok(client) } // ============================================================================ // SECTION 1: TRAINING JOB LIFECYCLE E2E TESTS (5 tests) // ============================================================================ #[tokio::test] #[ignore = "Requires running services"] async fn test_e2e_training_job_start() -> Result<()> { println!("\n=== E2E Test: Start Training Job via API Gateway ==="); let mut client = create_authenticated_client().await?; let hyperparameters = TrainingHyperparameters { learning_rate: 0.001, batch_size: 32, epochs: 10, dropout_rate: Some(0.2), hidden_layers: Some(3), hidden_units: Some(128), custom_params: HashMap::new(), }; let resource_requirements = ResourceRequirements { gpu_count: 1, cpu_cores: 4, memory_gb: 16, gpu_type: Some("RTX 3050 Ti".to_string()), disk_gb: 50, }; let request = Request::new(StartTrainingRequest { model_name: "dqn_trading_model".to_string(), dataset_id: "btc_eth_historical_2024".to_string(), hyperparameters: Some(hyperparameters), resource_requirements: Some(resource_requirements), tags: vec!["e2e_test".to_string(), "dqn".to_string()], description: "E2E test training job - DQN model".to_string(), auto_deploy: false, }); let response = client.start_training(request).await?; let training_job = response.into_inner(); assert!(!training_job.job_id.is_empty(), "Should return job ID"); assert_eq!(training_job.model_name, "dqn_trading_model"); println!("✓ Training job started successfully"); println!(" Job ID: {}", training_job.job_id); println!(" Model: {}", training_job.model_name); println!(" Status: {:?}", training_job.status); Ok(()) } #[tokio::test] #[ignore = "Requires running services"] async fn test_e2e_training_job_stop() -> Result<()> { println!("\n=== E2E Test: Stop Training Job via API Gateway ==="); let mut client = create_authenticated_client().await?; // Start a training job first let start_request = Request::new(StartTrainingRequest { model_name: "ppo_model".to_string(), dataset_id: "crypto_dataset_2024".to_string(), hyperparameters: Some(TrainingHyperparameters { learning_rate: 0.001, batch_size: 64, epochs: 100, // Long training dropout_rate: None, hidden_layers: None, hidden_units: None, custom_params: HashMap::new(), }), resource_requirements: Some(ResourceRequirements { gpu_count: 1, cpu_cores: 2, memory_gb: 8, gpu_type: None, disk_gb: 20, }), tags: vec!["e2e_stop_test".to_string()], description: "E2E stop test - PPO model".to_string(), auto_deploy: false, }); let start_response = client.start_training(start_request).await?; let job_id = start_response.into_inner().job_id; println!("✓ Training job started: {}", job_id); // Wait for training to actually start tokio::time::sleep(StdDuration::from_secs(1)).await; // Stop the training job let stop_request = Request::new(StopTrainingRequest { job_id: job_id.clone(), force: false, }); let stop_response = client.stop_training(stop_request).await?; let stopped_job = stop_response.into_inner(); assert_eq!(stopped_job.job_id, job_id); println!("✓ Training job stopped successfully"); println!(" Final Status: {:?}", stopped_job.status); Ok(()) } #[tokio::test] #[ignore = "Requires running services"] async fn test_e2e_list_training_jobs() -> Result<()> { println!("\n=== E2E Test: List Training Jobs via API Gateway ==="); let mut client = create_authenticated_client().await?; let request = Request::new(ListTrainingJobsRequest { model_name: None, status: None, start_time_after: None, start_time_before: None, tags: vec![], limit: 10, cursor: String::new(), }); let response = client.list_training_jobs(request).await?; let list_result = response.into_inner(); println!("✓ Training jobs list retrieved"); println!(" Total Count: {}", list_result.total_count); println!(" Returned: {}", list_result.jobs.len()); for (i, job) in list_result.jobs.into_iter().enumerate().take(5) { println!( " {}. {} - {} (Status: {:?})", i + 1, job.job_id, job.model_name, job.status ); } Ok(()) } #[tokio::test] #[ignore = "Requires running services"] async fn test_e2e_filter_training_jobs_by_model() -> Result<()> { println!("\n=== E2E Test: Filter Training Jobs by Model Name ==="); let mut client = create_authenticated_client().await?; let request = Request::new(ListTrainingJobsRequest { model_name: Some("dqn_trading_model".to_string()), status: None, start_time_after: None, start_time_before: None, tags: vec![], limit: 10, cursor: String::new(), }); let response = client.list_training_jobs(request).await?; let list_result = response.into_inner(); println!("✓ Filtered training jobs retrieved"); println!(" Filter: model_name = dqn_trading_model"); println!(" Count: {}", list_result.jobs.len()); for job in &list_result.jobs { assert_eq!(job.model_name, "dqn_trading_model"); } Ok(()) } #[tokio::test] #[ignore = "Requires running services"] async fn test_e2e_filter_training_jobs_by_status() -> Result<()> { println!("\n=== E2E Test: Filter Training Jobs by Status ==="); let mut client = create_authenticated_client().await?; let request = Request::new(ListTrainingJobsRequest { model_name: None, status: Some(TrainingStatus::Completed as i32), start_time_after: None, start_time_before: None, tags: vec![], limit: 10, cursor: String::new(), }); let response = client.list_training_jobs(request).await?; let list_result = response.into_inner(); println!("✓ Filtered training jobs retrieved"); println!(" Filter: status = COMPLETED"); println!(" Count: {}", list_result.jobs.len()); for job in &list_result.jobs { assert_eq!(job.status, TrainingStatus::Completed as i32); } Ok(()) } // ============================================================================ // SECTION 2: REAL-TIME MONITORING E2E TESTS (3 tests) // ============================================================================ #[tokio::test] #[ignore = "Requires running services"] async fn test_e2e_watch_training_progress() -> Result<()> { println!("\n=== E2E Test: Watch Training Progress via API Gateway ==="); let mut client = create_authenticated_client().await?; // Start a training job let start_request = Request::new(StartTrainingRequest { model_name: "mamba_model".to_string(), dataset_id: "high_freq_trading_data".to_string(), hyperparameters: Some(TrainingHyperparameters { learning_rate: 0.0001, batch_size: 128, epochs: 50, dropout_rate: Some(0.3), hidden_layers: Some(4), hidden_units: Some(256), custom_params: HashMap::new(), }), resource_requirements: Some(ResourceRequirements { gpu_count: 1, cpu_cores: 8, memory_gb: 32, gpu_type: Some("RTX 3050 Ti".to_string()), disk_gb: 100, }), tags: vec!["e2e_progress_test".to_string(), "mamba".to_string()], description: "E2E progress monitoring test".to_string(), auto_deploy: false, }); let start_response = client.start_training(start_request).await?; let job_id = start_response.into_inner().job_id; println!("✓ Training job started: {}", job_id); // Watch training progress let watch_request = Request::new(WatchTrainingRequest { job_id: job_id.clone(), include_logs: true, include_metrics: true, }); let mut stream = client .watch_training_progress(watch_request) .await? .into_inner(); println!("✓ Progress stream established"); // Receive progress updates (with timeout) let mut updates_received = 0; while let Ok(update_result) = timeout(StdDuration::from_secs(10), stream.message()).await { if let Ok(Some(update)) = update_result { updates_received += 1; println!( " Update #{}: Epoch {}/{} - Progress: {:.1}%", updates_received, update.current_epoch, update.total_epochs, update.progress_percentage ); if let Some(metrics) = &update.metrics { println!( " Loss: {:.4}, Accuracy: {:.2}%", metrics.loss, metrics.accuracy * 100.0 ); } if let Some(log) = &update.log_message { println!(" Log: {}", log); } // Stop after 5 updates or if completed if updates_received >= 5 || update.status == TrainingStatus::Completed as i32 { break; } } } assert!( updates_received > 0, "Should receive at least one progress update" ); println!("✓ Received {} progress updates", updates_received); Ok(()) } #[tokio::test] #[ignore = "Requires running services"] async fn test_e2e_resource_utilization() -> Result<()> { println!("\n=== E2E Test: Get Resource Utilization via API Gateway ==="); let mut client = create_authenticated_client().await?; let request = Request::new(ResourceRequest {}); let response = client.get_resource_utilization(request).await?; let resources = response.into_inner(); println!("✓ Resource utilization retrieved"); if let Some(utilization) = resources.current_utilization { println!( " GPU Utilization: {:.1}%", utilization.gpu_utilization * 100.0 ); println!(" GPU Memory: {:.1}%", utilization.gpu_memory_used * 100.0); println!( " CPU Utilization: {:.1}%", utilization.cpu_utilization * 100.0 ); println!(" Memory Used: {:.1}%", utilization.memory_used * 100.0); } println!( " Available GPUs: {} / {}", resources.available_gpus, resources.total_gpus ); println!( " Active Training Jobs: {}", resources.active_training_jobs.len() ); Ok(()) } #[tokio::test] #[ignore = "Requires running services"] async fn test_e2e_stream_resource_metrics() -> Result<()> { println!("\n=== E2E Test: Stream Resource Metrics via API Gateway ==="); let mut client = create_authenticated_client().await?; let request = Request::new(ResourceRequest {}); let mut stream = client.stream_resource_metrics(request).await?.into_inner(); println!("✓ Resource metrics stream established"); // Receive resource metric updates (with timeout) let mut updates_received = 0; while let Ok(update_result) = timeout(StdDuration::from_secs(5), stream.message()).await { if let Ok(Some(update)) = update_result { updates_received += 1; if let Some(utilization) = &update.utilization { println!( " Metrics Update #{}: GPU: {:.1}%, CPU: {:.1}%", updates_received, utilization.gpu_utilization * 100.0, utilization.cpu_utilization * 100.0 ); } println!(" Active Jobs: {}", update.active_jobs.len()); if updates_received >= 3 { break; } } } assert!( updates_received > 0, "Should receive at least one resource metric update" ); println!("✓ Received {} resource metric updates", updates_received); Ok(()) } // ============================================================================ // SECTION 3: CONFIGURATION AND VALIDATION E2E TESTS (4 tests) // ============================================================================ #[tokio::test] #[ignore = "Requires running services"] async fn test_e2e_validate_training_config() -> Result<()> { println!("\n=== E2E Test: Validate Training Configuration ==="); let mut client = create_authenticated_client().await?; let hyperparameters = TrainingHyperparameters { learning_rate: 0.01, batch_size: 64, epochs: 20, dropout_rate: Some(0.5), hidden_layers: Some(2), hidden_units: Some(64), custom_params: HashMap::new(), }; let resource_requirements = ResourceRequirements { gpu_count: 1, cpu_cores: 4, memory_gb: 16, gpu_type: None, disk_gb: 50, }; let request = Request::new(TrainingConfigRequest { model_name: "test_model".to_string(), hyperparameters: Some(hyperparameters), resource_requirements: Some(resource_requirements), }); let response = client.validate_training_config(request).await?; let validation = response.into_inner(); println!("✓ Configuration validation completed"); println!(" Valid: {}", validation.valid); println!(" Errors: {}", validation.validation_errors.len()); println!(" Warnings: {}", validation.validation_warnings.len()); println!( " Estimated Duration: {:.1}h", validation.estimated_duration_hours ); for error in &validation.validation_errors { println!(" Error: {}", error); } for warning in &validation.validation_warnings { println!(" Warning: {}", warning); } Ok(()) } #[tokio::test] #[ignore = "Requires running services"] async fn test_e2e_get_training_templates() -> Result<()> { println!("\n=== E2E Test: Get Training Templates via API Gateway ==="); let mut client = create_authenticated_client().await?; let request = Request::new(TrainingTemplatesRequest { model_type: None, // Get all templates }); let response = client.get_training_templates(request).await?; let templates = response.into_inner(); println!("✓ Training templates retrieved"); println!(" Total Templates: {}", templates.templates.len()); for (i, template) in templates.templates.into_iter().enumerate().take(5) { println!(" {}. {} - {}", i + 1, template.template_id, template.name); println!(" Description: {}", template.description); println!(" Model Type: {}", template.model_type); } Ok(()) } #[tokio::test] #[ignore = "Requires running services"] async fn test_e2e_filter_templates_by_model_type() -> Result<()> { println!("\n=== E2E Test: Filter Templates by Model Type ==="); let mut client = create_authenticated_client().await?; let request = Request::new(TrainingTemplatesRequest { model_type: Some("DQN".to_string()), }); let response = client.get_training_templates(request).await?; let templates = response.into_inner(); println!("✓ Filtered templates retrieved"); println!(" Filter: model_type = DQN"); println!(" Count: {}", templates.templates.len()); for template in &templates.templates { assert_eq!(template.model_type, "DQN"); } Ok(()) } #[tokio::test] #[ignore = "Requires running services"] async fn test_e2e_training_with_auto_deploy() -> Result<()> { println!("\n=== E2E Test: Training with Auto-Deploy ==="); let mut client = create_authenticated_client().await?; let request = Request::new(StartTrainingRequest { model_name: "auto_deploy_model".to_string(), dataset_id: "quick_training_dataset".to_string(), hyperparameters: Some(TrainingHyperparameters { learning_rate: 0.001, batch_size: 32, epochs: 5, // Quick training for auto-deploy test dropout_rate: None, hidden_layers: None, hidden_units: None, custom_params: HashMap::new(), }), resource_requirements: Some(ResourceRequirements { gpu_count: 1, cpu_cores: 2, memory_gb: 8, gpu_type: None, disk_gb: 20, }), tags: vec!["e2e_auto_deploy".to_string()], description: "E2E test with auto-deploy enabled".to_string(), auto_deploy: true, // Enable auto-deploy }); let response = client.start_training(request).await?; let training_job = response.into_inner(); println!("✓ Training with auto-deploy started"); println!(" Job ID: {}", training_job.job_id); println!(" Auto Deploy: Enabled"); println!(" Description: {}", training_job.description); Ok(()) }