#![cfg(feature = "__integration_tests")] #![allow(unexpected_cfgs)] //! End-to-End Integration Tests: API Gateway → Backtesting Service //! //! This test suite validates the complete backtesting service flow through the API Gateway: //! - Backtest start/stop/status operations //! - Strategy execution with historical data //! - Results retrieval and validation //! - Real-time progress monitoring //! - Backtest listing and filtering //! //! Test Count: 12 tests //! Coverage: Full backtesting service integration use anyhow::Result; use chrono::{Duration, Utc}; use std::collections::HashMap; use std::time::Duration as StdDuration; use tokio::time::timeout; use tonic::{metadata::MetadataValue, transport::Channel, Request, Status}; // Common test utilities (JWT auth helpers) mod common; use common::auth_helpers::{create_test_jwt, get_api_addr, TestAuthConfig}; // Generated proto code pub mod trading { tonic::include_proto!("trading"); } use trading::{ backtesting_service_client::BacktestingServiceClient, BacktestStatus, GetBacktestResultsRequest, GetBacktestStatusRequest, ListBacktestsRequest, StartBacktestRequest, StopBacktestRequest, SubscribeBacktestProgressRequest, }; /// Create an authenticated backtesting service client async fn create_authenticated_client() -> Result< BacktestingServiceClient< tonic::service::interceptor::InterceptedService< Channel, impl Fn(Request<()>) -> Result, Status> + Clone, >, >, > { let user_id = "test_backtester_001"; let role = "analyst"; // Use auth_helpers to create JWT token with correct secret let config = TestAuthConfig::trader() .with_user_id(user_id) .with_roles(vec![role.to_string()]) .with_permissions(vec![ "api.access".to_string(), "backtesting.execute".to_string(), "backtesting.view".to_string(), ]); let token = create_test_jwt(config)?; let api_addr = get_api_addr(); let channel = Channel::from_shared(api_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, Status> { // JWT token in authorization header let token_value = format!("Bearer {}", token); let metadata_value = MetadataValue::try_from(token_value) .map_err(|_| 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(|_| 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(|_| Status::internal("Failed to create role metadata"))?; req.metadata_mut().insert("x-user-role", role_value); Ok(req) }; let client = BacktestingServiceClient::with_interceptor(channel, interceptor); Ok(client) } // ============================================================================ // SECTION 1: BACKTEST LIFECYCLE E2E TESTS (5 tests) // ============================================================================ #[tokio::test] async fn test_e2e_backtest_start() -> Result<()> { println!("\n=== E2E Test: Start Backtest via API Gateway ==="); let mut client = create_authenticated_client().await?; let start_date = (Utc::now() - Duration::days(30)) .timestamp_nanos_opt() .unwrap_or(0); let end_date = Utc::now().timestamp_nanos_opt().unwrap_or(0); let mut parameters = HashMap::new(); parameters.insert("fast_ma".to_string(), "10".to_string()); parameters.insert("slow_ma".to_string(), "30".to_string()); parameters.insert("risk_per_trade".to_string(), "0.02".to_string()); let request = Request::new(StartBacktestRequest { strategy_name: "moving_average_crossover".to_string(), symbols: vec!["BTC/USD".to_string(), "ETH/USD".to_string()], start_date_unix_nanos: start_date, end_date_unix_nanos: end_date, initial_capital: 100000.0, parameters, save_results: true, description: "E2E test backtest - MA crossover strategy".to_string(), }); let response = client.start_backtest(request).await?; let result = response.into_inner(); assert!(result.success, "Backtest should start successfully"); assert!(!result.backtest_id.is_empty(), "Should return backtest ID"); println!("✓ Backtest started successfully"); println!(" Backtest ID: {}", result.backtest_id); println!( " Estimated Duration: {}s", result.estimated_duration_seconds ); println!(" Message: {}", result.message); Ok(()) } #[tokio::test] async fn test_e2e_backtest_status() -> Result<()> { println!("\n=== E2E Test: Get Backtest Status via API Gateway ==="); let mut client = create_authenticated_client().await?; // First start a backtest let start_date = (Utc::now() - Duration::days(7)) .timestamp_nanos_opt() .unwrap_or(0); let end_date = Utc::now().timestamp_nanos_opt().unwrap_or(0); let start_request = Request::new(StartBacktestRequest { strategy_name: "mean_reversion".to_string(), symbols: vec!["BTC/USD".to_string()], start_date_unix_nanos: start_date, end_date_unix_nanos: end_date, initial_capital: 50000.0, parameters: HashMap::new(), save_results: true, description: "E2E status test".to_string(), }); let start_response = client.start_backtest(start_request).await?; let backtest_id = start_response.into_inner().backtest_id; println!("✓ Backtest started: {}", backtest_id); // Wait a moment for backtest to start processing tokio::time::sleep(StdDuration::from_millis(500)).await; // Query status let status_request = Request::new(GetBacktestStatusRequest { backtest_id: backtest_id.clone(), }); let status_response = client.get_backtest_status(status_request).await?; let status = status_response.into_inner(); assert_eq!(status.backtest_id, backtest_id); assert!(status.status != BacktestStatus::Unspecified as i32); println!("✓ Backtest status retrieved"); println!(" Status: {:?}", status.status); println!(" Progress: {:.2}%", status.progress_percentage); println!(" Trades Executed: {}", status.trades_executed); println!(" Current PnL: ${:.2}", status.current_pnl); Ok(()) } #[tokio::test] async fn test_e2e_backtest_stop() -> Result<()> { println!("\n=== E2E Test: Stop Running Backtest via API Gateway ==="); let mut client = create_authenticated_client().await?; // Start a long-running backtest let start_date = (Utc::now() - Duration::days(90)) .timestamp_nanos_opt() .unwrap_or(0); let end_date = Utc::now().timestamp_nanos_opt().unwrap_or(0); let start_request = Request::new(StartBacktestRequest { strategy_name: "momentum".to_string(), symbols: vec!["BTC/USD".to_string(), "ETH/USD".to_string()], start_date_unix_nanos: start_date, end_date_unix_nanos: end_date, initial_capital: 100000.0, parameters: HashMap::new(), save_results: true, description: "E2E stop test - long backtest".to_string(), }); let start_response = client.start_backtest(start_request).await?; let backtest_id = start_response.into_inner().backtest_id; println!("✓ Long backtest started: {}", backtest_id); // Wait for backtest to begin processing tokio::time::sleep(StdDuration::from_secs(1)).await; // Stop the backtest let stop_request = Request::new(StopBacktestRequest { backtest_id: backtest_id.clone(), save_partial_results: true, }); let stop_response = client.stop_backtest(stop_request).await?; let stop_result = stop_response.into_inner(); assert!(stop_result.success, "Backtest should stop successfully"); println!("✓ Backtest stopped successfully"); println!(" Message: {}", stop_result.message); println!(" Partial Results Saved: {}", stop_result.results_saved); Ok(()) } #[tokio::test] async fn test_e2e_backtest_results() -> Result<()> { println!("\n=== E2E Test: Get Backtest Results via API Gateway ==="); let mut client = create_authenticated_client().await?; // Start a short backtest that will complete quickly let start_date = (Utc::now() - Duration::days(7)) .timestamp_nanos_opt() .unwrap_or(0); let end_date = (Utc::now() - Duration::days(6)) .timestamp_nanos_opt() .unwrap_or(0); let start_request = Request::new(StartBacktestRequest { strategy_name: "buy_and_hold".to_string(), symbols: vec!["BTC/USD".to_string()], start_date_unix_nanos: start_date, end_date_unix_nanos: end_date, initial_capital: 10000.0, parameters: HashMap::new(), save_results: true, description: "E2E results test - short backtest".to_string(), }); let start_response = client.start_backtest(start_request).await?; let backtest_id = start_response.into_inner().backtest_id; println!("✓ Short backtest started: {}", backtest_id); // Wait for backtest to complete (with timeout) let mut completed = false; for _ in 0..20 { tokio::time::sleep(StdDuration::from_millis(500)).await; let status_request = Request::new(GetBacktestStatusRequest { backtest_id: backtest_id.clone(), }); if let Ok(response) = client.get_backtest_status(status_request).await { let status = response.into_inner(); if status.status == BacktestStatus::Completed as i32 { completed = true; println!("✓ Backtest completed"); break; } } } if !completed { println!("⚠ Backtest did not complete in expected time, skipping results check"); return Ok(()); } // Get results let results_request = Request::new(GetBacktestResultsRequest { backtest_id: backtest_id.clone(), include_trades: true, include_metrics: true, }); let results_response = client.get_backtest_results(results_request).await?; let results = results_response.into_inner(); assert_eq!(results.backtest_id, backtest_id); if let Some(metrics) = results.metrics { println!("✓ Backtest results retrieved"); println!(" Total Return: {:.2}%", metrics.total_return * 100.0); println!(" Sharpe Ratio: {:.2}", metrics.sharpe_ratio); println!(" Max Drawdown: {:.2}%", metrics.max_drawdown * 100.0); println!(" Total Trades: {}", metrics.total_trades); println!(" Win Rate: {:.2}%", metrics.win_rate * 100.0); } println!(" Trade Count: {}", results.trades.len()); println!(" Equity Curve Points: {}", results.equity_curve.len()); Ok(()) } #[tokio::test] async fn test_e2e_backtest_list() -> Result<()> { println!("\n=== E2E Test: List Backtests via API Gateway ==="); let mut client = create_authenticated_client().await?; let request = Request::new(ListBacktestsRequest { limit: 10, offset: 0, strategy_name: None, status_filter: None, }); let response = client.list_backtests(request).await?; let list_result = response.into_inner(); println!("✓ Backtest list retrieved"); println!(" Total Count: {}", list_result.total_count); println!(" Returned: {}", list_result.backtests.len()); for (i, backtest) in list_result.backtests.into_iter().enumerate().take(5) { println!( " {}. {} - {} ({})", i + 1, backtest.backtest_id, backtest.strategy_name, backtest.status ); } Ok(()) } // ============================================================================ // SECTION 2: REAL-TIME MONITORING E2E TESTS (3 tests) // ============================================================================ #[tokio::test] async fn test_e2e_backtest_progress_subscription() -> Result<()> { println!("\n=== E2E Test: Backtest Progress Subscription via API Gateway ==="); let mut client = create_authenticated_client().await?; // Start a backtest let start_date = (Utc::now() - Duration::days(14)) .timestamp_nanos_opt() .unwrap_or(0); let end_date = Utc::now().timestamp_nanos_opt().unwrap_or(0); // WAVE 152: Use moving_average_crossover strategy (grid_trading doesn't exist) let mut parameters = HashMap::new(); parameters.insert("fast_ma".to_string(), "10".to_string()); parameters.insert("slow_ma".to_string(), "30".to_string()); parameters.insert("risk_per_trade".to_string(), "0.02".to_string()); let start_request = Request::new(StartBacktestRequest { strategy_name: "moving_average_crossover".to_string(), symbols: vec!["BTC/USD".to_string()], start_date_unix_nanos: start_date, end_date_unix_nanos: end_date, initial_capital: 50000.0, parameters, save_results: true, description: "E2E progress subscription test".to_string(), }); let start_response = client.start_backtest(start_request).await?; let backtest_id = start_response.into_inner().backtest_id; println!("✓ Backtest started: {}", backtest_id); // Subscribe to progress updates let progress_request = Request::new(SubscribeBacktestProgressRequest { backtest_id: backtest_id.clone(), }); let mut stream = client .subscribe_backtest_progress(progress_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!( " Progress Update #{}: {:.1}% - {} trades, PnL: ${:.2}", updates_received, update.progress_percentage, update.trades_executed, update.current_pnl ); // Stop after receiving 5 updates or if completed if updates_received >= 5 || update.status == BacktestStatus::Completed as i32 { break; } } } assert!( updates_received > 0, "Should receive at least one progress update" ); println!("✓ Received {} progress updates", updates_received); Ok(()) } #[tokio::test] async fn test_e2e_backtest_filtering_by_strategy() -> Result<()> { println!("\n=== E2E Test: Filter Backtests by Strategy via API Gateway ==="); let mut client = create_authenticated_client().await?; let request = Request::new(ListBacktestsRequest { limit: 10, offset: 0, strategy_name: Some("moving_average_crossover".to_string()), status_filter: None, }); let response = client.list_backtests(request).await?; let list_result = response.into_inner(); println!("✓ Filtered backtest list retrieved"); println!(" Strategy: moving_average_crossover"); println!(" Count: {}", list_result.backtests.len()); for backtest in &list_result.backtests { assert_eq!(backtest.strategy_name, "moving_average_crossover"); } Ok(()) } #[tokio::test] async fn test_e2e_backtest_filtering_by_status() -> Result<()> { println!("\n=== E2E Test: Filter Backtests by Status via API Gateway ==="); let mut client = create_authenticated_client().await?; let request = Request::new(ListBacktestsRequest { limit: 10, offset: 0, strategy_name: None, status_filter: Some(BacktestStatus::Completed as i32), }); let response = client.list_backtests(request).await?; let list_result = response.into_inner(); println!("✓ Filtered backtest list retrieved"); println!(" Status Filter: COMPLETED"); println!(" Count: {}", list_result.backtests.len()); for backtest in &list_result.backtests { assert_eq!(backtest.status, BacktestStatus::Completed as i32); } Ok(()) } // ============================================================================ // SECTION 3: ERROR HANDLING E2E TESTS (4 tests) // ============================================================================ #[tokio::test] async fn test_e2e_backtest_invalid_date_range() -> Result<()> { println!("\n=== E2E Test: Invalid Date Range Handling ==="); let mut client = create_authenticated_client().await?; // Start date after end date (invalid) let start_date = Utc::now().timestamp_nanos_opt().unwrap_or(0); let end_date = (Utc::now() - Duration::days(30)) .timestamp_nanos_opt() .unwrap_or(0); let request = Request::new(StartBacktestRequest { strategy_name: "test_strategy".to_string(), symbols: vec!["BTC/USD".to_string()], start_date_unix_nanos: start_date, end_date_unix_nanos: end_date, initial_capital: 10000.0, parameters: HashMap::new(), save_results: false, description: "Invalid date range test".to_string(), }); let result = client.start_backtest(request).await; // Should either return error or unsuccessful response if let Err(status) = result { println!("✓ Invalid date range rejected with error"); println!(" Error: {}", status.message()); } else if let Ok(response) = result { assert!( !response.into_inner().success, "Invalid date range should not succeed" ); println!("✓ Invalid date range rejected in response"); } Ok(()) } #[tokio::test] async fn test_e2e_backtest_invalid_capital() -> Result<()> { println!("\n=== E2E Test: Invalid Initial Capital Handling ==="); let mut client = create_authenticated_client().await?; let start_date = (Utc::now() - Duration::days(7)) .timestamp_nanos_opt() .unwrap_or(0); let end_date = Utc::now().timestamp_nanos_opt().unwrap_or(0); let request = Request::new(StartBacktestRequest { strategy_name: "test_strategy".to_string(), symbols: vec!["BTC/USD".to_string()], start_date_unix_nanos: start_date, end_date_unix_nanos: end_date, initial_capital: -1000.0, // Invalid negative capital parameters: HashMap::new(), save_results: false, description: "Invalid capital test".to_string(), }); let result = client.start_backtest(request).await; assert!( result.is_err() || !result.unwrap().into_inner().success, "Negative initial capital should be rejected" ); println!("✓ Negative initial capital correctly rejected"); Ok(()) } #[tokio::test] async fn test_e2e_backtest_nonexistent_status() -> Result<()> { println!("\n=== E2E Test: Query Status of Nonexistent Backtest ==="); let mut client = create_authenticated_client().await?; let request = Request::new(GetBacktestStatusRequest { backtest_id: "nonexistent_backtest_12345".to_string(), }); let result = client.get_backtest_status(request).await; assert!(result.is_err(), "Nonexistent backtest should return error"); if let Err(status) = result { println!("✓ Nonexistent backtest correctly rejected"); println!(" Error code: {:?}", status.code()); println!(" Error message: {}", status.message()); } Ok(()) } #[tokio::test] async fn test_e2e_backtest_unauthenticated_access() -> Result<()> { println!("\n=== E2E Test: Unauthenticated Backtest Access ==="); // Create client without authentication let api_addr = get_api_addr(); let channel = Channel::from_shared(api_addr)?.connect().await?; let mut client = BacktestingServiceClient::new(channel); let start_date = (Utc::now() - Duration::days(7)) .timestamp_nanos_opt() .unwrap_or(0); let end_date = Utc::now().timestamp_nanos_opt().unwrap_or(0); let request = Request::new(StartBacktestRequest { strategy_name: "test".to_string(), symbols: vec!["BTC/USD".to_string()], start_date_unix_nanos: start_date, end_date_unix_nanos: end_date, initial_capital: 10000.0, parameters: HashMap::new(), save_results: false, description: "Unauthenticated test".to_string(), }); let result = client.start_backtest(request).await; assert!( result.is_err(), "Unauthenticated request should be rejected" ); if let Err(status) = result { assert_eq!(status.code(), tonic::Code::Unauthenticated); println!("✓ Unauthenticated request correctly rejected"); println!(" Error: {}", status.message()); } Ok(()) }