//! Comprehensive service integration tests for TLI system //! //! This module tests the communication between TLI client and various services //! including Trading Service, Risk Management, Market Data, and Backtesting Service. use fake::faker::company::en::*; use fake::faker::finance::en::*; use fake::{Fake, Faker}; use std::collections::HashMap; use std::time::Duration; use tokio::time::{sleep, timeout}; use tonic::{Request, Response, Status}; use uuid::Uuid; use crate::integration::{TestConfig, TestUtilities}; use crate::mocks::grpc_server::{MockBacktestingServer, MockRiskServer, MockTradingServer}; use fxt::client::{ BacktestingClient, BacktestingClientConfig, TliClientBuilder, TradingClient, TradingClientConfig, }; // Database imports removed - TLI is pure client use fxt::prelude::*; /// Service integration test suite pub struct ServiceIntegrationTests { config: TestConfig, trading_client: Option, backtesting_client: Option, mock_servers: Vec>, } /// Mock server trait for test servers pub trait MockServer: Send + Sync { fn start(&mut self) -> FxtResult; fn stop(&mut self) -> FxtResult<()>; fn is_running(&self) -> bool; fn get_port(&self) -> Option; } impl ServiceIntegrationTests { pub fn new(config: TestConfig) -> Self { Self { config, trading_client: None, backtesting_client: None, mock_servers: Vec::new(), } } /// Setup test environment with mock services pub async fn setup(&mut self) -> FxtResult<()> { tracing::info!("Setting up service integration test environment"); // Start mock trading service let mut trading_server = MockTradingServer::new(self.config.mock_server_port)?; let trading_port = trading_server.start()?; self.mock_servers.push(Box::new(trading_server)); // Start mock backtesting service let mut backtesting_server = MockBacktestingServer::new(self.config.mock_server_port + 100)?; let backtesting_port = backtesting_server.start()?; self.mock_servers.push(Box::new(backtesting_server)); // Wait for services to be ready sleep(Duration::from_millis(500)).await; // Create TLI client suite let client_suite = TliClientBuilder::new() .with_service_endpoint( "trading_service".to_string(), format!("http://localhost:{}", trading_port), ) .with_service_endpoint( "backtesting_service".to_string(), format!("http://localhost:{}", backtesting_port), ) .with_trading_config(TradingClientConfig::default()) .with_backtesting_config(BacktestingClientConfig::default()) .build() .await?; self.trading_client = client_suite.trading_client; self.backtesting_client = client_suite.backtesting_client; tracing::info!("Service integration test environment setup complete"); Ok(()) } /// Cleanup test environment pub async fn teardown(&mut self) -> FxtResult<()> { tracing::info!("Tearing down service integration test environment"); // Shutdown clients if let Some(client) = self.trading_client.take() { client.shutdown().await; } if let Some(client) = self.backtesting_client.take() { client.shutdown().await; } // Stop mock servers for server in &mut self.mock_servers { let _ = server.stop(); } self.mock_servers.clear(); tracing::info!("Service integration test environment teardown complete"); Ok(()) } } /// Test TLI Client ↔ Trading Service communication #[cfg(test)] mod trading_service_tests { use super::*; #[tokio::test] async fn test_order_submission_flow() { let mut test_env = ServiceIntegrationTests::new(TestConfig::default()); test_env .setup() .await .expect("Failed to setup test environment"); let trading_client = test_env .trading_client .as_ref() .expect("Trading client not available"); // Test order submission let order_request = SubmitOrderRequest { symbol: TestUtilities::generate_test_symbol("AAPL"), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 100.0, price: Some(150.50), stop_price: None, time_in_force: "DAY".to_string(), client_order_id: Uuid::new_v4().to_string(), }; let result = timeout( Duration::from_secs(10), trading_client.submit_order(order_request), ) .await; assert!(result.is_ok(), "Order submission timed out"); let response = result.unwrap().expect("Order submission failed"); assert!(response.success, "Order submission was not successful"); assert!( !response.order_id.is_empty(), "Order ID should not be empty" ); test_env .teardown() .await .expect("Failed to teardown test environment"); } #[tokio::test] async fn test_order_cancellation_flow() { let mut test_env = ServiceIntegrationTests::new(TestConfig::default()); test_env .setup() .await .expect("Failed to setup test environment"); let trading_client = test_env .trading_client .as_ref() .expect("Trading client not available"); // First submit an order let order_request = SubmitOrderRequest { symbol: TestUtilities::generate_test_symbol("GOOGL"), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 50.0, price: Some(2800.00), stop_price: None, time_in_force: "GTC".to_string(), client_order_id: Uuid::new_v4().to_string(), }; let submit_response = trading_client .submit_order(order_request) .await .expect("Failed to submit order"); assert!(submit_response.success, "Order submission failed"); // Then cancel the order let cancel_request = CancelOrderRequest { order_id: submit_response.order_id.clone(), symbol: TestUtilities::generate_test_symbol("GOOGL"), }; let cancel_response = trading_client .cancel_order(cancel_request) .await .expect("Failed to cancel order"); assert!(cancel_response.success, "Order cancellation failed"); test_env .teardown() .await .expect("Failed to teardown test environment"); } #[tokio::test] async fn test_position_monitoring() { let mut test_env = ServiceIntegrationTests::new(TestConfig::default()); test_env .setup() .await .expect("Failed to setup test environment"); let trading_client = test_env .trading_client .as_ref() .expect("Trading client not available"); // Test position query let position_request = GetPositionsRequest { account_id: Some("test_account".to_string()), symbol_filter: None, include_zero_positions: false, }; let positions_response = trading_client .get_positions(position_request) .await .expect("Failed to get positions"); // Validate response structure assert!( !positions_response.positions.is_empty(), "Should have at least mock positions" ); for position in &positions_response.positions { assert!( !position.symbol.is_empty(), "Position symbol should not be empty" ); assert!( position.quantity != 0.0, "Position quantity should not be zero" ); } test_env .teardown() .await .expect("Failed to teardown test environment"); } #[tokio::test] async fn test_market_data_subscription() { let mut test_env = ServiceIntegrationTests::new(TestConfig::default()); test_env .setup() .await .expect("Failed to setup test environment"); let trading_client = test_env .trading_client .as_ref() .expect("Trading client not available"); // Test market data subscription let subscription_request = MarketDataSubscriptionRequest { symbols: vec!["AAPL".to_string(), "GOOGL".to_string(), "MSFT".to_string()], data_types: vec![MarketDataType::Quote as i32, MarketDataType::Trade as i32], include_level2: false, }; let mut stream = trading_client .subscribe_market_data(subscription_request) .await .expect("Failed to subscribe to market data"); // Collect a few market data updates let mut updates_received = 0; let timeout_duration = Duration::from_secs(5); let start_time = std::time::Instant::now(); while updates_received < 3 && start_time.elapsed() < timeout_duration { match timeout(Duration::from_millis(500), stream.recv()).await { Ok(Some(update)) => { assert!( !update.symbol.is_empty(), "Market data symbol should not be empty" ); assert!( update.timestamp > 0, "Market data timestamp should be valid" ); updates_received += 1; tracing::info!("Received market data update for {}", update.symbol); } Ok(None) => break, Err(_) => { tracing::warn!("Market data update timed out"); break; } } } assert!( updates_received > 0, "Should have received at least one market data update" ); test_env .teardown() .await .expect("Failed to teardown test environment"); } #[tokio::test] async fn test_risk_management_validation() { let mut test_env = ServiceIntegrationTests::new(TestConfig::default()); test_env .setup() .await .expect("Failed to setup test environment"); let trading_client = test_env .trading_client .as_ref() .expect("Trading client not available"); // Test pre-trade risk validation let risk_check_request = PreTradeRiskCheckRequest { symbol: TestUtilities::generate_test_symbol("TSLA"), side: OrderSide::Buy as i32, quantity: 1000.0, // Large quantity to trigger risk limits estimated_price: Some(800.0), order_type: OrderType::Market as i32, account_id: "test_account".to_string(), }; let risk_response = trading_client .check_pre_trade_risk(risk_check_request) .await .expect("Failed to perform risk check"); // Validate risk check response assert!( risk_response.risk_score >= 0.0 && risk_response.risk_score <= 1.0, "Risk score should be between 0 and 1" ); if !risk_response.approved { assert!( !risk_response.rejection_reasons.is_empty(), "Rejection reasons should be provided when order is not approved" ); } test_env .teardown() .await .expect("Failed to teardown test environment"); } #[tokio::test] async fn test_portfolio_analytics() { let mut test_env = ServiceIntegrationTests::new(TestConfig::default()); test_env .setup() .await .expect("Failed to setup test environment"); let trading_client = test_env .trading_client .as_ref() .expect("Trading client not available"); // Test portfolio analytics let analytics_request = PortfolioAnalyticsRequest { account_id: "test_account".to_string(), calculation_date: chrono::Utc::now().timestamp(), include_realized_pnl: true, include_unrealized_pnl: true, include_risk_metrics: true, }; let analytics_response = trading_client .get_portfolio_analytics(analytics_request) .await .expect("Failed to get portfolio analytics"); // Validate analytics response assert!( analytics_response.total_portfolio_value.is_some(), "Total portfolio value should be provided" ); assert!( analytics_response.daily_pnl.is_some(), "Daily P&L should be provided" ); assert!( !analytics_response.position_analytics.is_empty(), "Position analytics should not be empty" ); test_env .teardown() .await .expect("Failed to teardown test environment"); } } /// Test TLI Client ↔ Backtesting Service communication #[cfg(test)] mod backtesting_service_tests { use super::*; #[tokio::test] async fn test_backtest_execution_flow() { let mut test_env = ServiceIntegrationTests::new(TestConfig::default()); test_env .setup() .await .expect("Failed to setup test environment"); let backtesting_client = test_env .backtesting_client .as_ref() .expect("Backtesting client not available"); // Create backtest configuration let backtest_request = CreateBacktestRequest { name: format!("Integration Test Backtest {}", Uuid::new_v4()), strategy_id: "test_strategy".to_string(), start_date: "2024-01-01".to_string(), end_date: "2024-01-31".to_string(), initial_capital: 100000.0, symbols: vec!["AAPL".to_string(), "GOOGL".to_string()], parameters: HashMap::new(), }; // Start backtest let create_response = backtesting_client .create_backtest(backtest_request) .await .expect("Failed to create backtest"); assert!(create_response.success, "Backtest creation failed"); assert!( !create_response.backtest_id.is_empty(), "Backtest ID should not be empty" ); let backtest_id = create_response.backtest_id; // Start backtest execution let start_request = StartBacktestRequest { backtest_id: backtest_id.clone(), async_execution: true, }; let start_response = backtesting_client .start_backtest(start_request) .await .expect("Failed to start backtest"); assert!(start_response.success, "Backtest start failed"); // Monitor backtest progress let mut progress_checks = 0; let max_progress_checks = 10; while progress_checks < max_progress_checks { let status_request = GetBacktestStatusRequest { backtest_id: backtest_id.clone(), }; let status_response = backtesting_client .get_backtest_status(status_request) .await .expect("Failed to get backtest status"); tracing::info!( "Backtest status: {:?}, Progress: {}%", status_response.status, status_response.progress_percentage ); if status_response.status == BacktestStatus::Completed as i32 { break; } progress_checks += 1; sleep(Duration::from_millis(100)).await; } // Get backtest results let results_request = GetBacktestResultsRequest { backtest_id: backtest_id.clone(), include_trades: true, include_metrics: true, }; let results_response = backtesting_client .get_backtest_results(results_request) .await .expect("Failed to get backtest results"); // Validate results assert!( results_response.performance_summary.is_some(), "Performance summary should be provided" ); assert!( !results_response.trades.is_empty(), "Should have executed some trades" ); test_env .teardown() .await .expect("Failed to teardown test environment"); } #[tokio::test] async fn test_strategy_optimization() { let mut test_env = ServiceIntegrationTests::new(TestConfig::default()); test_env .setup() .await .expect("Failed to setup test environment"); let backtesting_client = test_env .backtesting_client .as_ref() .expect("Backtesting client not available"); // Create optimization request let optimization_request = StrategyOptimizationRequest { strategy_id: "test_strategy".to_string(), start_date: "2024-01-01".to_string(), end_date: "2024-03-31".to_string(), symbols: vec!["AAPL".to_string()], parameter_ranges: create_test_parameter_ranges(), optimization_metric: OptimizationMetric::SharpeRatio as i32, max_iterations: 50, }; // Start optimization let optimization_response = backtesting_client .optimize_strategy(optimization_request) .await .expect("Failed to start strategy optimization"); assert!( optimization_response.success, "Strategy optimization start failed" ); assert!( !optimization_response.optimization_id.is_empty(), "Optimization ID should not be empty" ); test_env .teardown() .await .expect("Failed to teardown test environment"); } #[tokio::test] async fn test_backtest_data_management() { let mut test_env = ServiceIntegrationTests::new(TestConfig::default()); test_env .setup() .await .expect("Failed to setup test environment"); let backtesting_client = test_env .backtesting_client .as_ref() .expect("Backtesting client not available"); // Test data availability check let data_request = CheckDataAvailabilityRequest { symbols: vec!["AAPL".to_string(), "GOOGL".to_string(), "MSFT".to_string()], start_date: "2024-01-01".to_string(), end_date: "2024-01-31".to_string(), data_types: vec![DataType::OHLCV as i32, DataType::Trades as i32], }; let data_response = backtesting_client .check_data_availability(data_request) .await .expect("Failed to check data availability"); // Validate data availability response assert!( !data_response.symbol_availability.is_empty(), "Symbol availability should not be empty" ); for (symbol, availability) in &data_response.symbol_availability { assert!(!symbol.is_empty(), "Symbol should not be empty"); assert!( availability.coverage_percentage >= 0.0 && availability.coverage_percentage <= 100.0, "Coverage percentage should be between 0 and 100" ); } test_env .teardown() .await .expect("Failed to teardown test environment"); } fn create_test_parameter_ranges() -> HashMap { let mut ranges = HashMap::new(); ranges.insert( "lookback_period".to_string(), ParameterRange { min_value: 5.0, max_value: 50.0, step_size: 5.0, parameter_type: ParameterType::Integer as i32, }, ); ranges.insert( "threshold".to_string(), ParameterRange { min_value: 0.01, max_value: 0.1, step_size: 0.01, parameter_type: ParameterType::Float as i32, }, ); ranges } } /// Configuration management integration tests #[cfg(test)] mod config_integration_tests { use super::*; #[tokio::test] async fn test_configuration_lifecycle() { let mut test_env = ServiceIntegrationTests::new(TestConfig::default()); test_env .setup() .await .expect("Failed to setup test environment"); let trading_client = test_env .trading_client .as_ref() .expect("Trading client not available"); // Test configuration retrieval let get_config_request = GetConfigurationRequest { config_type: ConfigurationType::TradingLimits as i32, account_id: Some("test_account".to_string()), }; let config_response = trading_client .get_configuration(get_config_request) .await .expect("Failed to get configuration"); assert!( !config_response.configurations.is_empty(), "Should have configurations" ); // Test configuration update let updated_config = Configuration { id: config_response.configurations[0].id.clone(), config_type: ConfigurationType::TradingLimits as i32, key: "max_position_size".to_string(), value: "1000000".to_string(), account_id: Some("test_account".to_string()), last_updated: chrono::Utc::now().timestamp(), }; let update_request = UpdateConfigurationRequest { configurations: vec![updated_config], validate_before_update: true, }; let update_response = trading_client .update_configuration(update_request) .await .expect("Failed to update configuration"); assert!( update_response.success, "Configuration update should succeed" ); assert!( update_response.validation_errors.is_empty(), "Should have no validation errors" ); test_env .teardown() .await .expect("Failed to teardown test environment"); } #[tokio::test] async fn test_hot_reload_configuration() { let mut test_env = ServiceIntegrationTests::new(TestConfig::default()); test_env .setup() .await .expect("Failed to setup test environment"); let trading_client = test_env .trading_client .as_ref() .expect("Trading client not available"); // Test hot reload trigger let reload_request = TriggerConfigReloadRequest { services: vec!["trading_service".to_string(), "risk_service".to_string()], config_types: vec![ConfigurationType::TradingLimits as i32], }; let reload_response = trading_client .trigger_config_reload(reload_request) .await .expect("Failed to trigger config reload"); assert!(reload_response.success, "Config reload should succeed"); for result in &reload_response.service_results { assert!(result.success, "Each service reload should succeed"); } test_env .teardown() .await .expect("Failed to teardown test environment"); } } /// Event streaming integration tests #[cfg(test)] mod event_streaming_tests { use super::*; #[tokio::test] async fn test_event_streaming_lifecycle() { let mut test_env = ServiceIntegrationTests::new(TestConfig::default()); test_env .setup() .await .expect("Failed to setup test environment"); let trading_client = test_env .trading_client .as_ref() .expect("Trading client not available"); // Subscribe to events let subscription_request = EventSubscriptionRequest { event_types: vec![ EventType::OrderExecuted as i32, EventType::PositionChanged as i32, EventType::RiskLimitBreached as i32, ], filters: HashMap::new(), }; let mut event_stream = trading_client .subscribe_to_events(subscription_request) .await .expect("Failed to subscribe to events"); // Generate some events by placing orders let order_request = SubmitOrderRequest { symbol: TestUtilities::generate_test_symbol("AAPL"), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 100.0, price: None, stop_price: None, time_in_force: "DAY".to_string(), client_order_id: Uuid::new_v4().to_string(), }; // Submit order to generate events let _order_response = trading_client .submit_order(order_request) .await .expect("Failed to submit order"); // Collect events let mut events_received = 0; let timeout_duration = Duration::from_secs(5); let start_time = std::time::Instant::now(); while events_received < 2 && start_time.elapsed() < timeout_duration { match timeout(Duration::from_millis(500), event_stream.recv()).await { Ok(Some(event)) => { assert!(!event.event_id.is_empty(), "Event ID should not be empty"); assert!(event.timestamp > 0, "Event timestamp should be valid"); events_received += 1; tracing::info!("Received event: {:?}", event.event_type); } Ok(None) => break, Err(_) => { tracing::warn!("Event stream timed out"); break; } } } assert!( events_received > 0, "Should have received at least one event" ); test_env .teardown() .await .expect("Failed to teardown test environment"); } }