#![cfg(feature = "__integration_tests")] #![allow(unexpected_cfgs)] //! End-to-End Integration Tests: TLI → API Gateway → Trading Service //! //! This test suite validates the complete flow from a TLI client through the API Gateway //! to the Trading Service, including: //! - JWT authentication and authorization //! - Order submission and lifecycle //! - Position management queries //! - Market data subscriptions //! - Real-time order updates //! //! Test Count: 15 tests //! Coverage: Full trading service integration use anyhow::Result; use std::time::Duration as StdDuration; use tokio::time::timeout; use tonic::{transport::Channel, Request, Status}; use uuid::Uuid; // Common test utilities (JWT auth helpers and DBN data) mod common; use common::auth_helpers::{create_auth_interceptor, get_api_addr, TestAuthConfig}; #[path = "common/dbn_helpers.rs"] mod dbn_helpers; use dbn_helpers::get_dbn_manager; // Generated proto code pub mod trading { tonic::include_proto!("trading"); } use trading::{ trading_service_client::TradingServiceClient, CancelOrderRequest, GetAccountInfoRequest, GetOrderStatusRequest, GetPositionsRequest, MarketDataType, OrderSide, OrderType, SubmitOrderRequest, SubscribeMarketDataRequest, SubscribeOrderUpdatesRequest, }; /// Create an authenticated trading service client async fn create_authenticated_client() -> Result< TradingServiceClient< tonic::service::interceptor::InterceptedService< Channel, impl Fn(Request<()>) -> Result, Status> + Clone, >, >, > { let config = TestAuthConfig::trader() .with_user_id("test_trader_001") .with_roles(vec!["trader".to_string()]) .with_permissions(vec![ "api.access".to_string(), "trading.submit".to_string(), "trading.view".to_string(), "trading.cancel".to_string(), ]); let interceptor = create_auth_interceptor(config)?; let api_addr = get_api_addr(); let channel = Channel::from_shared(api_addr)?.connect().await?; let client = TradingServiceClient::with_interceptor(channel, interceptor); Ok(client) } // ============================================================================ // SECTION 1: ORDER SUBMISSION E2E TESTS (5 tests) // ============================================================================ #[tokio::test] async fn test_e2e_order_submission_market_order() -> Result<()> { println!("\n=== E2E Test: Market Order Submission via API Gateway ==="); let mut client = create_authenticated_client().await?; // Use ES.FUT with realistic data from DBN files // Note: ES.FUT is available in test data, quantity scaled appropriately for futures let request = Request::new(SubmitOrderRequest { symbol: "ES.FUT".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 1.0, // 1 contract for ES.FUT futures price: None, stop_price: None, time_in_force: "GTC".to_string(), client_order_id: Uuid::new_v4().to_string(), }); let response = client.submit_order(request).await?; let order = response.into_inner(); assert!(order.success, "Order submission should succeed"); assert!(!order.order_id.is_empty(), "Should return order ID"); println!("✓ Market order submitted successfully"); println!(" Order ID: {}", order.order_id); println!(" Symbol: ES.FUT (Real DBN data)"); println!(" Message: {}", order.message); Ok(()) } #[tokio::test] async fn test_e2e_order_submission_limit_order() -> Result<()> { println!("\n=== E2E Test: Limit Order Submission via API Gateway ==="); let mut client = create_authenticated_client().await?; // Get realistic price from DBN data let dbn_manager = get_dbn_manager().await?; let realistic_price = dbn_manager .create_realistic_order_price("ES.FUT", "sell", 10) .await?; println!( " Using realistic limit price from DBN data: ${:.2}", realistic_price ); let request = Request::new(SubmitOrderRequest { symbol: "ES.FUT".to_string(), side: OrderSide::Sell as i32, order_type: OrderType::Limit as i32, quantity: 1.0, price: Some(realistic_price), stop_price: None, time_in_force: "GTC".to_string(), client_order_id: Uuid::new_v4().to_string(), }); let response = client.submit_order(request).await?; let order = response.into_inner(); assert!(order.success, "Limit order submission should succeed"); assert!(!order.order_id.is_empty(), "Should return order ID"); println!("✓ Limit order submitted successfully"); println!(" Order ID: {}", order.order_id); println!(" Symbol: ES.FUT (Real DBN data)"); println!(" Limit Price: ${:.2}", realistic_price); Ok(()) } #[tokio::test] async fn test_e2e_order_submission_without_auth() -> Result<()> { println!("\n=== E2E Test: Order Submission Without Authentication ==="); // Create unauthenticated client (no JWT) let api_addr = get_api_addr(); let channel = Channel::from_shared(api_addr)?.connect().await?; let mut client = TradingServiceClient::new(channel); let request = Request::new(SubmitOrderRequest { symbol: "ES.FUT".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 1.0, price: None, stop_price: None, time_in_force: "GTC".to_string(), client_order_id: Uuid::new_v4().to_string(), }); let result = client.submit_order(request).await; assert!(result.is_err(), "Unauthenticated request should fail"); if let Err(status) = result { assert_eq!(status.code(), tonic::Code::Unauthenticated); println!("✓ Unauthenticated request correctly rejected"); println!(" Symbol: ES.FUT (Real DBN data)"); println!(" Error: {}", status.message()); } Ok(()) } #[tokio::test] async fn test_e2e_order_cancellation() -> Result<()> { println!("\n=== E2E Test: Order Cancellation via API Gateway ==="); let mut client = create_authenticated_client().await?; // Get realistic price from DBN data let dbn_manager = get_dbn_manager().await?; let realistic_price = dbn_manager .create_realistic_order_price("ES.FUT", "buy", 50) .await?; println!( " Using realistic limit price from DBN data: ${:.2}", realistic_price ); // First, submit an order let submit_request = Request::new(SubmitOrderRequest { symbol: "ES.FUT".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 1.0, price: Some(realistic_price), stop_price: None, time_in_force: "GTC".to_string(), client_order_id: Uuid::new_v4().to_string(), }); let submit_response = client.submit_order(submit_request).await?; let order_id = submit_response.into_inner().order_id; println!("✓ Order submitted: {} (ES.FUT, Real DBN data)", order_id); // Wait a moment for order to be processed tokio::time::sleep(StdDuration::from_millis(100)).await; // Now cancel the order let cancel_request = Request::new(CancelOrderRequest { order_id: order_id.clone(), symbol: "ES.FUT".to_string(), }); let cancel_response = client.cancel_order(cancel_request).await?; let cancel_result = cancel_response.into_inner(); assert!(cancel_result.success, "Order cancellation should succeed"); println!("✓ Order cancelled successfully"); println!(" Message: {}", cancel_result.message); Ok(()) } #[tokio::test] async fn test_e2e_order_status_query() -> Result<()> { println!("\n=== E2E Test: Order Status Query via API Gateway ==="); let mut client = create_authenticated_client().await?; // Submit an order first let submit_request = Request::new(SubmitOrderRequest { symbol: "ES.FUT".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 1.0, price: None, stop_price: None, time_in_force: "IOC".to_string(), client_order_id: Uuid::new_v4().to_string(), }); let submit_response = client.submit_order(submit_request).await?; let order_id = submit_response.into_inner().order_id; // Query order status let status_request = Request::new(GetOrderStatusRequest { order_id: order_id.clone(), }); let status_response = client.get_order_status(status_request).await?; let order_status = status_response.into_inner(); assert_eq!(order_status.order_id, order_id); assert_eq!(order_status.symbol, "ES.FUT"); println!("✓ Order status retrieved successfully"); println!(" Order ID: {}", order_status.order_id); println!(" Symbol: {} (Real DBN data)", order_status.symbol); println!(" Status: {:?}", order_status.status); Ok(()) } // ============================================================================ // SECTION 2: POSITION MANAGEMENT E2E TESTS (3 tests) // ============================================================================ #[tokio::test] async fn test_e2e_get_all_positions() -> Result<()> { println!("\n=== E2E Test: Get All Positions via API Gateway ==="); let mut client = create_authenticated_client().await?; let request = Request::new(GetPositionsRequest { symbol: None, // Get all positions }); let response = client.get_positions(request).await?; let positions = response.into_inner(); println!("✓ Positions retrieved successfully"); println!(" Total positions: {}", positions.positions.len()); for position in &positions.positions { println!( " - {}: {} @ ${}", position.symbol, position.quantity, position.market_price ); } Ok(()) } #[tokio::test] async fn test_e2e_get_position_by_symbol() -> Result<()> { println!("\n=== E2E Test: Get Position by Symbol via API Gateway ==="); let mut client = create_authenticated_client().await?; let request = Request::new(GetPositionsRequest { symbol: Some("ES.FUT".to_string()), }); let response = client.get_positions(request).await?; let positions = response.into_inner(); println!("✓ ES.FUT position retrieved (Real DBN data)"); if let Some(position) = positions.positions.first() { assert_eq!(position.symbol, "ES.FUT"); println!(" Quantity: {}", position.quantity); println!(" Market Value: ${}", position.market_value); println!(" Unrealized PnL: ${}", position.unrealized_pnl); } Ok(()) } #[tokio::test] async fn test_e2e_get_account_info() -> Result<()> { println!("\n=== E2E Test: Get Account Info via API Gateway ==="); let mut client = create_authenticated_client().await?; let request = Request::new(GetAccountInfoRequest { account_id: "test_trader_001".to_string(), }); let response = client.get_account_info(request).await?; let account = response.into_inner(); assert_eq!(account.account_id, "test_trader_001"); println!("✓ Account information retrieved"); println!(" Account ID: {}", account.account_id); println!(" Total Value: ${}", account.total_value); println!(" Cash Balance: ${}", account.cash_balance); println!(" Buying Power: ${}", account.buying_power); Ok(()) } // ============================================================================ // SECTION 3: REAL-TIME DATA STREAMING E2E TESTS (4 tests) // ============================================================================ #[tokio::test] async fn test_e2e_market_data_subscription() -> Result<()> { println!("\n=== E2E Test: Market Data Subscription via API Gateway ==="); let mut client = create_authenticated_client().await?; // Subscribe to ES.FUT with real DBN data available let request = Request::new(SubscribeMarketDataRequest { symbols: vec!["ES.FUT".to_string()], data_types: vec![ MarketDataType::Trades as i32, MarketDataType::Quotes as i32, MarketDataType::Bars as i32, ], }); let mut stream = client.subscribe_market_data(request).await?.into_inner(); println!("✓ Market data stream established for ES.FUT (Real DBN data)"); // Try to receive market data events (with timeout) // NOTE: Market data is external - we may not receive events in test environment let mut events_received = 0; while let Ok(event_result) = timeout(StdDuration::from_secs(2), stream.message()).await { if let Ok(Some(_event)) = event_result { events_received += 1; println!(" Received market data event #{}", events_received); if events_received >= 3 { break; } } } // In E2E test, just verify stream was established successfully // Actual market data reception depends on external data feeds if events_received > 0 { println!( "✓ Received {} market data events from ES.FUT", events_received ); } else { println!("✓ Stream established (no market data available in test environment)"); println!(" Note: Real DBN data (ES.FUT) available for backtesting"); } Ok(()) } #[tokio::test] async fn test_e2e_order_updates_subscription() -> Result<()> { println!("\n=== E2E Test: Order Updates Subscription via API Gateway ==="); let mut client = create_authenticated_client().await?; let request = Request::new(SubscribeOrderUpdatesRequest { account_id: Some("test_trader_001".to_string()), }); let mut stream = client.subscribe_order_updates(request).await?.into_inner(); println!("✓ Order updates stream established"); // Submit an order to generate an update (using ES.FUT with real DBN data) let submit_request = Request::new(SubmitOrderRequest { symbol: "ES.FUT".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 1.0, price: None, stop_price: None, time_in_force: "IOC".to_string(), client_order_id: Uuid::new_v4().to_string(), }); client.submit_order(submit_request).await?; println!("✓ Test order submitted (ES.FUT, Real DBN data)"); // Wait for order update (with timeout) if let Ok(Ok(Some(update))) = timeout(StdDuration::from_secs(3), stream.message()).await { println!("✓ Received order update"); println!(" Order ID: {}", update.order_id); println!(" Symbol: ES.FUT (Real DBN data)"); println!(" Status: {:?}", update.status); println!(" Message: {}", update.message); } Ok(()) } #[tokio::test] async fn test_e2e_concurrent_order_submissions() -> Result<()> { println!("\n=== E2E Test: Concurrent Order Submissions via API Gateway ==="); let mut handles = vec![]; // Submit 10 concurrent orders (all ES.FUT with real DBN data) for i in 0..10 { let handle = tokio::spawn(async move { let mut client = create_authenticated_client().await.unwrap(); let request = Request::new(SubmitOrderRequest { symbol: "ES.FUT".to_string(), side: if i % 2 == 0 { OrderSide::Buy } else { OrderSide::Sell } as i32, order_type: OrderType::Market as i32, quantity: 1.0, price: None, stop_price: None, time_in_force: "IOC".to_string(), client_order_id: Uuid::new_v4().to_string(), }); client.submit_order(request).await }); handles.push(handle); } let results = futures::future::join_all(handles).await; let successful = results .iter() .filter(|r| { if let Ok(Ok(response)) = r { response.get_ref().success } else { false } }) .count(); println!("✓ Concurrent order submission test completed"); println!(" Symbol: ES.FUT (Real DBN data)"); println!(" Total orders: 10"); println!(" Successful: {}", successful); assert!( successful >= 8, "At least 80% of concurrent orders should succeed" ); Ok(()) } #[tokio::test] async fn test_e2e_gateway_request_routing() -> Result<()> { println!("\n=== E2E Test: API Gateway Request Routing ==="); let mut client = create_authenticated_client().await?; // Test multiple different request types to verify routing // 1. Account info request let account_request = Request::new(GetAccountInfoRequest { account_id: "test_trader_001".to_string(), }); let account_response = client.get_account_info(account_request).await; assert!( account_response.is_ok(), "Account info request should be routed correctly" ); println!("✓ Account info routed successfully"); // 2. Positions request let positions_request = Request::new(GetPositionsRequest { symbol: None }); let positions_response = client.get_positions(positions_request).await; assert!( positions_response.is_ok(), "Positions request should be routed correctly" ); println!("✓ Positions request routed successfully"); // 3. Order status request (for non-existent order) let status_request = Request::new(GetOrderStatusRequest { order_id: "non_existent_order_123".to_string(), }); let _status_routed = client.get_order_status(status_request).await; // This might fail (order doesn't exist), but gateway routing should work println!("✓ Order status request routed successfully"); println!("✓ All API Gateway routing tests passed"); Ok(()) } // ============================================================================ // SECTION 4: ERROR HANDLING AND RESILIENCE E2E TESTS (3 tests) // ============================================================================ #[tokio::test] async fn test_e2e_invalid_symbol_handling() -> Result<()> { println!("\n=== E2E Test: Invalid Symbol Error Handling ==="); let mut client = create_authenticated_client().await?; let request = Request::new(SubmitOrderRequest { symbol: "INVALID_SYMBOL_XYZ".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 0.1, price: None, stop_price: None, time_in_force: "GTC".to_string(), client_order_id: Uuid::new_v4().to_string(), }); let result = client.submit_order(request).await; // Should either reject with validation error or return unsuccessful response if let Err(status) = result { println!("✓ Invalid symbol correctly rejected"); println!(" Error code: {:?}", status.code()); println!(" Error message: {}", status.message()); } else if let Ok(response) = result { assert!( !response.into_inner().success, "Invalid symbol should not succeed" ); println!("✓ Invalid symbol rejected in response"); } Ok(()) } #[tokio::test] async fn test_e2e_negative_quantity_validation() -> Result<()> { println!("\n=== E2E Test: Negative Quantity Validation ==="); let mut client = create_authenticated_client().await?; let request = Request::new(SubmitOrderRequest { symbol: "ES.FUT".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: -0.5, // Invalid negative quantity price: None, stop_price: None, time_in_force: "GTC".to_string(), client_order_id: Uuid::new_v4().to_string(), }); let result = client.submit_order(request).await; assert!( result.is_err() || !result.unwrap().into_inner().success, "Negative quantity should be rejected" ); println!("✓ Negative quantity correctly rejected"); println!(" Symbol: ES.FUT (Real DBN data)"); Ok(()) } #[tokio::test] async fn test_e2e_gateway_timeout_handling() -> Result<()> { println!("\n=== E2E Test: Gateway Timeout Handling ==="); let mut client = create_authenticated_client().await?; // Set a very short timeout to simulate timeout scenario let request = Request::new(GetPositionsRequest { symbol: None }); // Wrap request in a timeout match timeout(StdDuration::from_millis(1), client.get_positions(request)).await { Ok(Ok(_)) => { println!("✓ Request completed within timeout"); }, Ok(Err(status)) => { println!("✓ Request failed gracefully: {:?}", status.code()); }, Err(_) => { println!("✓ Timeout handled correctly"); }, } Ok(()) }