//! gRPC Endpoints Integration Tests //! //! Comprehensive tests for gRPC service endpoints covering: //! - Request/Response validation //! - Error handling and status codes //! - Streaming endpoints (orders, positions, market data, executions) //! - Concurrent requests //! - Malformed request handling //! - Timeout and error scenarios use anyhow::Result; use std::sync::Arc; use std::time::Duration; use tokio_stream::StreamExt; use tonic::Request; use trading_service::proto::trading::{ trading_service_server::TradingService, CancelOrderRequest, GetExecutionHistoryRequest, GetOrderBookRequest, GetOrderStatusRequest, GetPortfolioSummaryRequest, GetPositionsRequest, MarketDataType, OrderSide, OrderType, StreamExecutionsRequest, StreamMarketDataRequest, StreamOrdersRequest, StreamPositionsRequest, SubmitOrderRequest, }; use trading_service::{services::trading::TradingServiceImpl, state::TradingServiceState}; /// Setup test trading service instance async fn setup_trading_service() -> Result { let state = Arc::new(TradingServiceState::new_for_testing().await?); Ok(TradingServiceImpl::new(state)) } // ============================================================================ // Request/Response Validation Tests // ============================================================================ #[tokio::test] async fn test_submit_order_endpoint_validation() -> Result<()> { println!("\n=== Test: SubmitOrder Endpoint Validation ==="); let service = setup_trading_service().await?; let mut metadata = std::collections::HashMap::new(); metadata.insert("time_in_force".to_string(), "GTC".to_string()); let request = Request::new(SubmitOrderRequest { account_id: "grpc_test_001".to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 100.0, price: None, stop_price: None, metadata, }); let response = service.submit_order(request).await?; let order = response.into_inner(); println!(" ✓ Response received"); println!(" ├─ Order ID: {}", order.order_id); println!(" ├─ Status: {:?}", order.status); println!(" └─ Message: {}", order.message); assert!(!order.order_id.is_empty()); assert!(order.timestamp > 0); Ok(()) } #[tokio::test] async fn test_cancel_order_endpoint() -> Result<()> { println!("\n=== Test: CancelOrder Endpoint ==="); let service = setup_trading_service().await?; // First submit an order let mut metadata = std::collections::HashMap::new(); metadata.insert("time_in_force".to_string(), "GTC".to_string()); let submit_request = Request::new(SubmitOrderRequest { account_id: "grpc_test_002".to_string(), symbol: "GOOGL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 50.0, price: Some(150.00), stop_price: None, metadata, }); let submit_response = service.submit_order(submit_request).await?; let order_id = submit_response.into_inner().order_id; // Cancel the order let cancel_request = Request::new(CancelOrderRequest { order_id: order_id.clone(), account_id: "grpc_test_002".to_string(), }); let cancel_response = service.cancel_order(cancel_request).await?; let cancel_result = cancel_response.into_inner(); println!(" ✓ Cancellation response received"); println!(" ├─ Success: {}", cancel_result.success); println!(" ├─ Message: {}", cancel_result.message); println!(" └─ Timestamp: {}", cancel_result.timestamp); assert!(cancel_result.timestamp > 0); Ok(()) } #[tokio::test] async fn test_get_order_status_endpoint() -> Result<()> { println!("\n=== Test: GetOrderStatus Endpoint ==="); let service = setup_trading_service().await?; // Submit order let mut metadata = std::collections::HashMap::new(); metadata.insert("time_in_force".to_string(), "GTC".to_string()); let submit_request = Request::new(SubmitOrderRequest { account_id: "grpc_test_003".to_string(), symbol: "MSFT".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 100.0, price: None, stop_price: None, metadata, }); let submit_response = service.submit_order(submit_request).await?; let order_id = submit_response.into_inner().order_id; // Get status let status_request = Request::new(GetOrderStatusRequest { order_id: order_id.clone(), }); let status_response = service.get_order_status(status_request).await?; let status = status_response.into_inner(); println!(" ✓ Order status received"); if let Some(order) = status.order { println!(" ├─ Order ID: {}", order.order_id); println!(" ├─ Symbol: {}", order.symbol); println!(" ├─ Quantity: {}", order.quantity); println!(" ├─ Status: {:?}", order.status); println!(" └─ Filled: {}", order.filled_quantity); } Ok(()) } #[tokio::test] async fn test_get_positions_endpoint() -> Result<()> { println!("\n=== Test: GetPositions Endpoint ==="); let service = setup_trading_service().await?; let request = Request::new(GetPositionsRequest { account_id: Some("grpc_test_004".to_string()), symbol: None, }); let response = service.get_positions(request).await?; let positions = response.into_inner(); println!(" ✓ Positions response received"); println!(" └─ Positions count: {}", positions.positions.len()); for pos in &positions.positions { println!( " ├─ {}: {} shares @ ${:.2}", pos.symbol, pos.quantity, pos.average_price ); } Ok(()) } #[tokio::test] async fn test_get_portfolio_summary_endpoint() -> Result<()> { println!("\n=== Test: GetPortfolioSummary Endpoint ==="); let service = setup_trading_service().await?; let request = Request::new(GetPortfolioSummaryRequest { account_id: "grpc_test_005".to_string(), }); let response = service.get_portfolio_summary(request).await?; let summary = response.into_inner(); println!(" ✓ Portfolio summary received"); println!(" ├─ Total Value: ${:.2}", summary.total_value); println!(" ├─ Unrealized PnL: ${:.2}", summary.unrealized_pnl); println!(" ├─ Realized PnL: ${:.2}", summary.realized_pnl); println!(" ├─ Day PnL: ${:.2}", summary.day_pnl); println!(" ├─ Buying Power: ${:.2}", summary.buying_power); println!(" ├─ Margin Used: ${:.2}", summary.margin_used); println!(" └─ Positions: {}", summary.positions.len()); Ok(()) } #[tokio::test] async fn test_get_order_book_endpoint() -> Result<()> { println!("\n=== Test: GetOrderBook Endpoint ==="); let service = setup_trading_service().await?; let request = Request::new(GetOrderBookRequest { symbol: "AAPL".to_string(), depth: Some(10), }); let response = service.get_order_book(request).await?; let order_book_response = response.into_inner(); println!(" ✓ Order book response received"); if let Some(order_book) = order_book_response.order_book { println!(" ├─ Symbol: {}", order_book.symbol); println!(" ├─ Bids: {}", order_book.bids.len()); println!(" ├─ Asks: {}", order_book.asks.len()); println!(" └─ Timestamp: {}", order_book.timestamp); if !order_book.bids.is_empty() { println!( " Best Bid: ${:.2} x {}", order_book.bids[0].price, order_book.bids[0].quantity ); } if !order_book.asks.is_empty() { println!( " Best Ask: ${:.2} x {}", order_book.asks[0].price, order_book.asks[0].quantity ); } } Ok(()) } #[tokio::test] async fn test_get_execution_history_endpoint() -> Result<()> { println!("\n=== Test: GetExecutionHistory Endpoint ==="); let service = setup_trading_service().await?; let request = Request::new(GetExecutionHistoryRequest { account_id: Some("grpc_test_006".to_string()), symbol: None, start_time: None, end_time: None, limit: Some(50), }); let response = service.get_execution_history(request).await?; let history = response.into_inner(); println!(" ✓ Execution history received"); println!(" └─ Executions: {}", history.executions.len()); for (i, exec) in history.executions.into_iter().take(5).enumerate() { println!( " {}. {}: {} @ ${:.2}", i + 1, exec.symbol, exec.quantity, exec.price ); } Ok(()) } // ============================================================================ // Streaming Endpoint Tests // ============================================================================ #[tokio::test] async fn test_stream_orders_endpoint() -> Result<()> { println!("\n=== Test: StreamOrders Endpoint ==="); let service = setup_trading_service().await?; let request = Request::new(StreamOrdersRequest { account_id: Some("grpc_test_007".to_string()), symbol: None, }); let mut stream = service.stream_orders(request).await?.into_inner(); println!(" ✓ Order stream established"); // Collect a few events with timeout let timeout_duration = Duration::from_secs(2); let timeout_future = tokio::time::sleep(timeout_duration); tokio::pin!(timeout_future); let mut event_count = 0; loop { tokio::select! { event_result = stream.next() => { match event_result { Some(Ok(event)) => { event_count += 1; println!(" Event {}: Order {} - {:?}", event_count, event.order_id, event.event_type); if event_count >= 5 { break; } } Some(Err(e)) => { println!(" Stream error: {}", e); break; } None => { println!(" Stream ended"); break; } } } _ = &mut timeout_future => { println!(" Timeout after {} events", event_count); break; } } } println!(" Total events received: {}", event_count); Ok(()) } #[tokio::test] async fn test_stream_positions_endpoint() -> Result<()> { println!("\n=== Test: StreamPositions Endpoint ==="); let service = setup_trading_service().await?; let request = Request::new(StreamPositionsRequest { account_id: Some("grpc_test_008".to_string()), }); let mut stream = service.stream_positions(request).await?.into_inner(); println!(" ✓ Position stream established"); let timeout_duration = Duration::from_secs(2); let timeout_future = tokio::time::sleep(timeout_duration); tokio::pin!(timeout_future); let mut event_count = 0; loop { tokio::select! { event_result = stream.next() => { match event_result { Some(Ok(event)) => { event_count += 1; if let Some(position) = event.position { println!(" Event {}: {} - {} shares", event_count, position.symbol, position.quantity); } if event_count >= 5 { break; } } Some(Err(e)) => { println!(" Stream error: {}", e); break; } None => { println!(" Stream ended"); break; } } } _ = &mut timeout_future => { println!(" Timeout after {} events", event_count); break; } } } println!(" Total events received: {}", event_count); Ok(()) } #[tokio::test] async fn test_stream_executions_endpoint() -> Result<()> { println!("\n=== Test: StreamExecutions Endpoint ==="); let service = setup_trading_service().await?; let request = Request::new(StreamExecutionsRequest { account_id: Some("grpc_test_009".to_string()), symbol: None, }); let mut stream = service.stream_executions(request).await?.into_inner(); println!(" ✓ Execution stream established"); let timeout_duration = Duration::from_secs(2); let timeout_future = tokio::time::sleep(timeout_duration); tokio::pin!(timeout_future); let mut event_count = 0; loop { tokio::select! { event_result = stream.next() => { match event_result { Some(Ok(event)) => { event_count += 1; println!(" Event {}: {} - {} @ ${:.2}", event_count, event.symbol, event.quantity, event.price); if event_count >= 5 { break; } } Some(Err(e)) => { println!(" Stream error: {}", e); break; } None => { println!(" Stream ended"); break; } } } _ = &mut timeout_future => { println!(" Timeout after {} events", event_count); break; } } } println!(" Total events received: {}", event_count); Ok(()) } #[tokio::test] async fn test_stream_market_data_endpoint() -> Result<()> { println!("\n=== Test: StreamMarketData Endpoint ==="); let service = setup_trading_service().await?; let request = Request::new(StreamMarketDataRequest { symbols: vec!["AAPL".to_string(), "GOOGL".to_string()], data_types: vec![MarketDataType::Trade as i32, MarketDataType::Quote as i32], }); let mut stream = service.stream_market_data(request).await?.into_inner(); println!(" ✓ Market data stream established"); let timeout_duration = Duration::from_secs(2); let timeout_future = tokio::time::sleep(timeout_duration); tokio::pin!(timeout_future); let mut event_count = 0; loop { tokio::select! { event_result = stream.next() => { match event_result { Some(Ok(event)) => { event_count += 1; println!(" Event {}: {} - Type: {:?}", event_count, event.symbol, event.data_type); if event_count >= 5 { break; } } Some(Err(e)) => { println!(" Stream error: {}", e); break; } None => { println!(" Stream ended"); break; } } } _ = &mut timeout_future => { println!(" Timeout after {} events", event_count); break; } } } println!(" Total events received: {}", event_count); Ok(()) } // ============================================================================ // Error Handling Tests // ============================================================================ #[tokio::test] async fn test_invalid_order_side_error() -> Result<()> { println!("\n=== Test: Invalid Order Side Error ==="); let service = setup_trading_service().await?; let request = Request::new(SubmitOrderRequest { account_id: "grpc_test_010".to_string(), symbol: "AAPL".to_string(), side: 999, // Invalid side order_type: OrderType::Market as i32, quantity: 100.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); let result = service.submit_order(request).await; match result { Ok(_) => println!(" Request accepted (validation might be lenient)"), Err(status) => { println!(" ✓ Error returned: {}", status.message()); assert!( status.code() == tonic::Code::InvalidArgument || status.code() == tonic::Code::FailedPrecondition ); }, } Ok(()) } #[tokio::test] async fn test_empty_account_id_error() -> Result<()> { println!("\n=== Test: Empty Account ID Error ==="); let service = setup_trading_service().await?; let request = Request::new(SubmitOrderRequest { account_id: "".to_string(), // Empty account ID symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 100.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); let result = service.submit_order(request).await; match result { Ok(_) => println!(" Request accepted (validation might be lenient)"), Err(status) => { println!(" ✓ Error returned: {}", status.message()); assert_eq!(status.code(), tonic::Code::InvalidArgument); }, } Ok(()) } #[tokio::test] async fn test_nonexistent_order_status() -> Result<()> { println!("\n=== Test: Nonexistent Order Status ==="); let service = setup_trading_service().await?; let request = Request::new(GetOrderStatusRequest { order_id: "nonexistent_order_12345".to_string(), }); let result = service.get_order_status(request).await; match result { Ok(response) => { let status = response.into_inner(); println!( " Response received, order present: {}", status.order.is_some() ); }, Err(status) => { println!(" ✓ Error returned: {}", status.message()); assert_eq!(status.code(), tonic::Code::NotFound); }, } Ok(()) } // ============================================================================ // Concurrent Request Tests // ============================================================================ #[tokio::test] async fn test_concurrent_endpoint_requests() -> Result<()> { println!("\n=== Test: Concurrent Endpoint Requests ==="); let service = Arc::new(setup_trading_service().await?); let mut handles = vec![]; // Submit multiple requests concurrently for i in 1..=10 { let svc = service.clone(); let handle = tokio::spawn(async move { let mut metadata = std::collections::HashMap::new(); metadata.insert("time_in_force".to_string(), "GTC".to_string()); metadata.insert("client_id".to_string(), format!("concurrent_{}", i)); let request = Request::new(SubmitOrderRequest { account_id: format!("grpc_test_{:03}", i), symbol: "SPY".to_string(), side: if i % 2 == 0 { OrderSide::Buy as i32 } else { OrderSide::Sell as i32 }, order_type: OrderType::Market as i32, quantity: 10.0 * (i as f64), price: None, stop_price: None, metadata, }); svc.submit_order(request).await }); handles.push(handle); } // Wait for all requests let mut success_count = 0; for handle in handles { if let Ok(Ok(_)) = handle.await { success_count += 1; } } println!(" ✓ {}/10 concurrent requests succeeded", success_count); assert!( success_count >= 8, "Most concurrent requests should succeed" ); Ok(()) } #[tokio::test] async fn test_mixed_endpoint_concurrent_access() -> Result<()> { println!("\n=== Test: Mixed Endpoint Concurrent Access ==="); let service = Arc::new(setup_trading_service().await?); let mut handles = vec![]; // Mix different endpoint calls for i in 1..=15 { let svc = service.clone(); let handle = tokio::spawn(async move { match i % 3 { 0 => { // Submit order let request = Request::new(SubmitOrderRequest { account_id: "mixed_test".to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 10.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); svc.submit_order(request).await.is_ok() }, 1 => { // Get positions let request = Request::new(GetPositionsRequest { account_id: Some("mixed_test".to_string()), symbol: None, }); svc.get_positions(request).await.is_ok() }, _ => { // Get portfolio summary let request = Request::new(GetPortfolioSummaryRequest { account_id: "mixed_test".to_string(), }); svc.get_portfolio_summary(request).await.is_ok() }, } }); handles.push(handle); } // Wait for all let mut success_count = 0; for handle in handles { if let Ok(true) = handle.await { success_count += 1; } } println!(" ✓ {}/15 mixed endpoint requests succeeded", success_count); assert!(success_count >= 12, "Most mixed requests should succeed"); Ok(()) }