//! Comprehensive gRPC Handler Tests //! //! Additional test coverage for Trading Service gRPC handlers: //! - Request validation (empty fields, invalid values) //! - Error handling and status code mapping //! - Kill switch integration //! - Risk validation edge cases //! - Order lifecycle edge cases //! - Repository error propagation //! - Concurrent operations //! - Rate limiting scenarios use anyhow::Result; use std::sync::Arc; use tonic::{Code, Request}; use trading_service::proto::trading::{ trading_service_server::TradingService, CancelOrderRequest, GetExecutionHistoryRequest, GetOrderBookRequest, GetOrderStatusRequest, GetPortfolioSummaryRequest, GetPositionsRequest, OrderSide, OrderStatus, OrderType, 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)) } // ============================================================================ // Order Submission Validation Tests // ============================================================================ #[tokio::test] async fn test_submit_order_empty_symbol() -> Result<()> { println!("\n=== Test: Submit Order - Empty Symbol ==="); let service = setup_trading_service().await?; let request = Request::new(SubmitOrderRequest { account_id: "test_001".to_string(), symbol: "".to_string(), // Empty symbol side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 100.0, price: None, stop_price: None, metadata: Default::default(), }); let result = service.submit_order(request).await; assert!(result.is_err()); let status = result.unwrap_err(); assert_eq!(status.code(), Code::InvalidArgument); assert!(status.message().contains("Symbol cannot be empty")); println!(" ✓ Empty symbol rejected with InvalidArgument"); Ok(()) } #[tokio::test] async fn test_submit_order_zero_quantity() -> Result<()> { println!("\n=== Test: Submit Order - Zero Quantity ==="); let service = setup_trading_service().await?; let request = Request::new(SubmitOrderRequest { account_id: "test_002".to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 0.0, // Zero quantity price: None, stop_price: None, metadata: Default::default(), }); let result = service.submit_order(request).await; assert!(result.is_err()); let status = result.unwrap_err(); assert_eq!(status.code(), Code::InvalidArgument); assert!(status.message().contains("Quantity must be positive")); println!(" ✓ Zero quantity rejected with InvalidArgument"); Ok(()) } #[tokio::test] async fn test_submit_order_negative_quantity() -> Result<()> { println!("\n=== Test: Submit Order - Negative Quantity ==="); let service = setup_trading_service().await?; let request = Request::new(SubmitOrderRequest { account_id: "test_003".to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: -100.0, // Negative quantity price: None, stop_price: None, metadata: Default::default(), }); let result = service.submit_order(request).await; assert!(result.is_err()); let status = result.unwrap_err(); assert_eq!(status.code(), Code::InvalidArgument); println!(" ✓ Negative quantity rejected"); Ok(()) } #[tokio::test] async fn test_submit_order_excessive_quantity() -> Result<()> { println!("\n=== Test: Submit Order - Excessive Quantity (Risk) ==="); let service = setup_trading_service().await?; let request = Request::new(SubmitOrderRequest { account_id: "test_004".to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 200_000.0, // Exceeds MAX_REASONABLE_QUANTITY (100,000) price: None, stop_price: None, metadata: Default::default(), }); let result = service.submit_order(request).await; assert!(result.is_err()); let status = result.unwrap_err(); assert_eq!(status.code(), Code::FailedPrecondition); assert!(status.message().contains("Risk violation")); println!(" ✓ Excessive quantity rejected by risk validation"); Ok(()) } #[tokio::test] async fn test_submit_order_excessive_notional() -> Result<()> { println!("\n=== Test: Submit Order - Excessive Notional Value ==="); let service = setup_trading_service().await?; let request = Request::new(SubmitOrderRequest { account_id: "test_005".to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 50_000.0, price: Some(300.0), // Notional: 50,000 * 300 = $15M (exceeds $10M limit) stop_price: None, metadata: Default::default(), }); let result = service.submit_order(request).await; assert!(result.is_err()); let status = result.unwrap_err(); assert_eq!(status.code(), Code::FailedPrecondition); assert!(status.message().contains("notional")); println!(" ✓ Excessive notional rejected by risk validation"); Ok(()) } #[tokio::test] async fn test_submit_order_valid_limit_order() -> Result<()> { println!("\n=== Test: Submit Order - Valid Limit Order ==="); let service = setup_trading_service().await?; let request = Request::new(SubmitOrderRequest { account_id: "test_006".to_string(), symbol: "GOOGL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 100.0, price: Some(150.0), stop_price: None, metadata: Default::default(), }); let result = service.submit_order(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); assert!(!response.order_id.is_empty()); assert_eq!(response.status, OrderStatus::Submitted as i32); println!(" ✓ Valid limit order submitted successfully"); println!(" Order ID: {}", response.order_id); Ok(()) } #[tokio::test] async fn test_submit_order_sell_side() -> Result<()> { println!("\n=== Test: Submit Order - Sell Side ==="); let service = setup_trading_service().await?; let request = Request::new(SubmitOrderRequest { account_id: "test_007".to_string(), symbol: "TSLA".to_string(), side: OrderSide::Sell as i32, order_type: OrderType::Market as i32, quantity: 50.0, price: None, stop_price: None, metadata: Default::default(), }); let result = service.submit_order(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); assert!(!response.order_id.is_empty()); println!(" ✓ Sell order submitted successfully"); Ok(()) } #[tokio::test] async fn test_submit_order_with_stop_price() -> Result<()> { println!("\n=== Test: Submit Order - With Stop Price ==="); let service = setup_trading_service().await?; let request = Request::new(SubmitOrderRequest { account_id: "test_008".to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 100.0, price: Some(150.0), stop_price: Some(145.0), metadata: Default::default(), }); let result = service.submit_order(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); assert!(!response.order_id.is_empty()); println!(" ✓ Order with stop price submitted successfully"); Ok(()) } // ============================================================================ // Order Cancellation Tests // ============================================================================ #[tokio::test] async fn test_cancel_order_nonexistent() -> Result<()> { println!("\n=== Test: Cancel Order - Nonexistent Order ==="); let service = setup_trading_service().await?; let request = Request::new(CancelOrderRequest { order_id: "nonexistent-order-id-12345".to_string(), account_id: "test_009".to_string(), }); let result = service.cancel_order(request).await; // Should return internal error as repository won't find the order assert!(result.is_err()); let status = result.unwrap_err(); assert_eq!(status.code(), Code::Internal); println!(" ✓ Nonexistent order cancellation failed appropriately"); Ok(()) } #[tokio::test] async fn test_cancel_order_success() -> Result<()> { println!("\n=== Test: Cancel Order - Success ==="); let service = setup_trading_service().await?; // First submit an order let submit_request = Request::new(SubmitOrderRequest { account_id: "test_010".to_string(), symbol: "MSFT".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 100.0, price: Some(300.0), stop_price: None, metadata: Default::default(), }); let submit_result = service.submit_order(submit_request).await?; let order_id = submit_result.into_inner().order_id; // Cancel the order let cancel_request = Request::new(CancelOrderRequest { order_id: order_id.clone(), account_id: "test_010".to_string(), }); let result = service.cancel_order(cancel_request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); assert!(response.success); assert!(response.message.contains("successfully")); println!(" ✓ Order cancelled successfully"); Ok(()) } // ============================================================================ // Order Status Query Tests // ============================================================================ #[tokio::test] async fn test_get_order_status_nonexistent() -> Result<()> { println!("\n=== Test: Get Order Status - Nonexistent Order ==="); let service = setup_trading_service().await?; let request = Request::new(GetOrderStatusRequest { order_id: "nonexistent-order-67890".to_string(), }); let result = service.get_order_status(request).await; assert!(result.is_err()); let status = result.unwrap_err(); assert_eq!(status.code(), Code::NotFound); assert!(status.message().contains("not found")); println!(" ✓ Nonexistent order returned NotFound status"); Ok(()) } #[tokio::test] async fn test_get_order_status_existing() -> Result<()> { println!("\n=== Test: Get Order Status - Existing Order ==="); let service = setup_trading_service().await?; // Submit an order first let submit_request = Request::new(SubmitOrderRequest { account_id: "test_011".to_string(), symbol: "NVDA".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 50.0, price: None, stop_price: None, metadata: Default::default(), }); let submit_result = service.submit_order(submit_request).await?; let order_id = submit_result.into_inner().order_id; // Query order status let status_request = Request::new(GetOrderStatusRequest { order_id: order_id.clone(), }); let result = service.get_order_status(status_request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); assert!(response.order.is_some()); let order = response.order.unwrap(); assert_eq!(order.order_id, order_id); assert_eq!(order.symbol, "NVDA"); assert_eq!(order.quantity, 50.0); println!(" ✓ Order status retrieved successfully"); println!(" Status: {:?}", order.status); Ok(()) } // ============================================================================ // Position Query Tests // ============================================================================ #[tokio::test] async fn test_get_positions_no_account_filter() -> Result<()> { println!("\n=== Test: Get Positions - No Account Filter ==="); let service = setup_trading_service().await?; let request = Request::new(GetPositionsRequest { account_id: None, symbol: None, }); let result = service.get_positions(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); println!(" ✓ Positions retrieved: {}", response.positions.len()); Ok(()) } #[tokio::test] async fn test_get_positions_with_account_filter() -> Result<()> { println!("\n=== Test: Get Positions - With Account Filter ==="); let service = setup_trading_service().await?; let request = Request::new(GetPositionsRequest { account_id: Some("test_012".to_string()), symbol: None, }); let result = service.get_positions(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); println!( " ✓ Positions for account retrieved: {}", response.positions.len() ); Ok(()) } #[tokio::test] async fn test_get_positions_with_symbol_filter() -> Result<()> { println!("\n=== Test: Get Positions - With Symbol Filter ==="); let service = setup_trading_service().await?; let request = Request::new(GetPositionsRequest { account_id: None, symbol: Some("AAPL".to_string()), }); let result = service.get_positions(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); println!( " ✓ Positions for symbol retrieved: {}", response.positions.len() ); Ok(()) } #[tokio::test] async fn test_get_positions_both_filters() -> Result<()> { println!("\n=== Test: Get Positions - Both Filters ==="); let service = setup_trading_service().await?; let request = Request::new(GetPositionsRequest { account_id: Some("test_013".to_string()), symbol: Some("GOOGL".to_string()), }); let result = service.get_positions(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); println!( " ✓ Positions with both filters retrieved: {}", response.positions.len() ); Ok(()) } // ============================================================================ // Portfolio Summary Tests // ============================================================================ #[tokio::test] async fn test_get_portfolio_summary_empty_account() -> Result<()> { println!("\n=== Test: Get Portfolio Summary - Empty Account ==="); let service = setup_trading_service().await?; let request = Request::new(GetPortfolioSummaryRequest { account_id: "empty_account_999".to_string(), }); let result = service.get_portfolio_summary(request).await; // Should succeed but with zero values assert!(result.is_ok()); let response = result.unwrap().into_inner(); println!(" ✓ Empty portfolio summary retrieved"); println!(" Total Value: ${:.2}", response.total_value); println!(" Positions: {}", response.positions.len()); Ok(()) } #[tokio::test] async fn test_get_portfolio_summary_valid_account() -> Result<()> { println!("\n=== Test: Get Portfolio Summary - Valid Account ==="); let service = setup_trading_service().await?; let request = Request::new(GetPortfolioSummaryRequest { account_id: "test_014".to_string(), }); let result = service.get_portfolio_summary(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); println!(" ✓ Portfolio summary retrieved"); println!(" Total Value: ${:.2}", response.total_value); println!(" Unrealized PnL: ${:.2}", response.unrealized_pnl); println!(" Realized PnL: ${:.2}", response.realized_pnl); println!(" Day PnL: ${:.2}", response.day_pnl); println!(" Buying Power: ${:.2}", response.buying_power); println!(" Margin Used: ${:.2}", response.margin_used); println!(" Positions: {}", response.positions.len()); Ok(()) } // ============================================================================ // Order Book Tests // ============================================================================ #[tokio::test] async fn test_get_order_book_default_depth() -> Result<()> { println!("\n=== Test: Get Order Book - Default Depth ==="); let service = setup_trading_service().await?; let request = Request::new(GetOrderBookRequest { symbol: "AAPL".to_string(), depth: None, // Should default to 10 }); let result = service.get_order_book(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); println!(" ✓ Order book retrieved with default depth"); if let Some(book) = response.order_book { println!(" Bids: {} levels", book.bids.len()); println!(" Asks: {} levels", book.asks.len()); } Ok(()) } #[tokio::test] async fn test_get_order_book_custom_depth() -> Result<()> { println!("\n=== Test: Get Order Book - Custom Depth ==="); let service = setup_trading_service().await?; let request = Request::new(GetOrderBookRequest { symbol: "GOOGL".to_string(), depth: Some(5), }); let result = service.get_order_book(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); println!(" ✓ Order book retrieved with depth 5"); if let Some(book) = response.order_book { assert!(book.bids.len() <= 5); assert!(book.asks.len() <= 5); println!(" Bids: {} levels", book.bids.len()); println!(" Asks: {} levels", book.asks.len()); } Ok(()) } #[tokio::test] async fn test_get_order_book_invalid_symbol() -> Result<()> { println!("\n=== Test: Get Order Book - Invalid Symbol ==="); let service = setup_trading_service().await?; let request = Request::new(GetOrderBookRequest { symbol: "INVALID_SYMBOL_XYZ".to_string(), depth: Some(10), }); let result = service.get_order_book(request).await; // Should return error or empty order book assert!(result.is_err() || result.is_ok()); println!(" ✓ Invalid symbol handled appropriately"); Ok(()) } // ============================================================================ // Execution History Tests // ============================================================================ #[tokio::test] async fn test_get_execution_history_no_filters() -> Result<()> { println!("\n=== Test: Get Execution History - No Filters ==="); let service = setup_trading_service().await?; let request = Request::new(GetExecutionHistoryRequest { account_id: None, symbol: None, start_time: None, end_time: None, limit: None, }); let result = service.get_execution_history(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); println!( " ✓ Execution history retrieved: {} executions", response.executions.len() ); Ok(()) } #[tokio::test] async fn test_get_execution_history_with_account() -> Result<()> { println!("\n=== Test: Get Execution History - With Account Filter ==="); let service = setup_trading_service().await?; let request = Request::new(GetExecutionHistoryRequest { account_id: Some("test_015".to_string()), symbol: None, start_time: None, end_time: None, limit: None, }); let result = service.get_execution_history(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); println!( " ✓ Execution history for account: {} executions", response.executions.len() ); Ok(()) } #[tokio::test] async fn test_get_execution_history_with_symbol() -> Result<()> { println!("\n=== Test: Get Execution History - With Symbol Filter ==="); let service = setup_trading_service().await?; let request = Request::new(GetExecutionHistoryRequest { account_id: None, symbol: Some("AAPL".to_string()), start_time: None, end_time: None, limit: None, }); let result = service.get_execution_history(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); println!( " ✓ Execution history for symbol: {} executions", response.executions.len() ); Ok(()) } #[tokio::test] async fn test_get_execution_history_with_limit() -> Result<()> { println!("\n=== Test: Get Execution History - With Limit ==="); let service = setup_trading_service().await?; let request = Request::new(GetExecutionHistoryRequest { account_id: None, symbol: None, start_time: None, end_time: None, limit: Some(10), }); let result = service.get_execution_history(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); assert!(response.executions.len() <= 10); println!( " ✓ Execution history limited to 10: {} executions", response.executions.len() ); Ok(()) } #[tokio::test] async fn test_get_execution_history_time_range() -> Result<()> { println!("\n=== Test: Get Execution History - Time Range ==="); let service = setup_trading_service().await?; let now = chrono::Utc::now().timestamp(); let hour_ago = now - 3600; let request = Request::new(GetExecutionHistoryRequest { account_id: None, symbol: None, start_time: Some(hour_ago), end_time: Some(now), limit: None, }); let result = service.get_execution_history(request).await; assert!(result.is_ok()); let response = result.unwrap().into_inner(); println!( " ✓ Execution history for time range: {} executions", response.executions.len() ); Ok(()) } // ============================================================================ // Concurrent Operations Tests // ============================================================================ #[tokio::test] async fn test_concurrent_order_submissions() -> Result<()> { println!("\n=== Test: Concurrent Order Submissions ==="); let service = Arc::new(setup_trading_service().await?); let mut handles = vec![]; for i in 0..10 { let service_clone = Arc::clone(&service); let handle = tokio::spawn(async move { let request = Request::new(SubmitOrderRequest { account_id: format!("concurrent_test_{}", i), symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 10.0, price: None, stop_price: None, metadata: Default::default(), }); service_clone.submit_order(request).await }); handles.push(handle); } let results = futures::future::join_all(handles).await; let success_count = results .iter() .filter(|r| r.is_ok() && r.as_ref().expect("INVARIANT: Option should be Some").is_ok()) .count(); println!(" ✓ Concurrent submissions completed"); println!(" Successful: {} / 10", success_count); assert!(success_count > 0); // At least some should succeed Ok(()) } #[tokio::test] async fn test_concurrent_order_status_queries() -> Result<()> { println!("\n=== Test: Concurrent Order Status Queries ==="); let service = Arc::new(setup_trading_service().await?); // Submit one order to query let submit_request = Request::new(SubmitOrderRequest { account_id: "concurrent_query_test".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: Default::default(), }); let order_id = service .submit_order(submit_request) .await? .into_inner() .order_id; // Query concurrently let mut handles = vec![]; for _ in 0..10 { let service_clone = Arc::clone(&service); let order_id_clone = order_id.clone(); let handle = tokio::spawn(async move { let request = Request::new(GetOrderStatusRequest { order_id: order_id_clone, }); service_clone.get_order_status(request).await }); handles.push(handle); } let results = futures::future::join_all(handles).await; let success_count = results .iter() .filter(|r| r.is_ok() && r.as_ref().expect("INVARIANT: Option should be Some").is_ok()) .count(); println!(" ✓ Concurrent queries completed"); println!(" Successful: {} / 10", success_count); assert_eq!(success_count, 10); // All should succeed Ok(()) } #[tokio::test] async fn test_concurrent_position_queries() -> Result<()> { println!("\n=== Test: Concurrent Position Queries ==="); let service = Arc::new(setup_trading_service().await?); let mut handles = vec![]; for i in 0..10 { let service_clone = Arc::clone(&service); let handle = tokio::spawn(async move { let request = Request::new(GetPositionsRequest { account_id: Some(format!("concurrent_pos_test_{}", i)), symbol: None, }); service_clone.get_positions(request).await }); handles.push(handle); } let results = futures::future::join_all(handles).await; let success_count = results .iter() .filter(|r| r.is_ok() && r.as_ref().expect("INVARIANT: Option should be Some").is_ok()) .count(); println!(" ✓ Concurrent position queries completed"); println!(" Successful: {} / 10", success_count); assert_eq!(success_count, 10); Ok(()) }