//! Integration tests for Trading Service //! //! Comprehensive tests covering: //! - Order submission and validation //! - Order cancellation flows //! - Position management //! - Risk validation //! - Kill switch integration //! - gRPC error handling //! - Concurrent operations use anyhow::Result; use std::sync::Arc; use tonic::Request; use trading_service::proto::trading::{ trading_service_server::TradingService, CancelOrderRequest, GetOrderStatusRequest, GetPositionsRequest, OrderSide, OrderStatus, OrderType, SubmitOrderRequest, }; use trading_service::{services::trading::TradingServiceImpl, state::TradingServiceState}; /// Setup test trading service instance async fn setup_trading_service() -> Result { // Create test state let state = Arc::new(TradingServiceState::new_for_testing().await?); Ok(TradingServiceImpl::new(state)) } #[tokio::test] async fn test_submit_valid_market_order() -> Result<()> { println!("\n=== Test: Submit Valid Market Order ==="); let service = setup_trading_service().await?; let mut metadata = std::collections::HashMap::new(); metadata.insert("time_in_force".to_string(), "GTC".to_string()); metadata.insert( "client_order_id".to_string(), "client_order_123".to_string(), ); let request = Request::new(SubmitOrderRequest { account_id: "test_account_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!("✓ Order submitted: {}", order.order_id); assert_eq!(order.status, OrderStatus::Submitted as i32); assert!(!order.order_id.is_empty()); assert_eq!(order.message, "Order submitted successfully"); Ok(()) } #[tokio::test] async fn test_submit_valid_limit_order() -> Result<()> { println!("\n=== Test: Submit Valid Limit Order ==="); let service = setup_trading_service().await?; let mut metadata = std::collections::HashMap::new(); metadata.insert("time_in_force".to_string(), "DAY".to_string()); let request = Request::new(SubmitOrderRequest { account_id: "test_account_002".to_string(), symbol: "GOOGL".to_string(), side: OrderSide::Sell as i32, order_type: OrderType::Limit as i32, quantity: 50.0, price: Some(150.50), stop_price: None, metadata, }); let response = service.submit_order(request).await?; let order = response.into_inner(); println!("✓ Limit order submitted: {}", order.order_id); assert_eq!(order.status, OrderStatus::Submitted as i32); assert!(!order.order_id.is_empty()); Ok(()) } #[tokio::test] async fn test_submit_invalid_empty_symbol() -> Result<()> { println!("\n=== Test: Reject Empty Symbol ==="); 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: "test_account_003".to_string(), symbol: "".to_string(), // Invalid: empty symbol side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 100.0, price: None, stop_price: None, metadata, }); let result = service.submit_order(request).await; assert!(result.is_err(), "Empty symbol should be rejected"); if let Err(status) = result { assert_eq!(status.code(), tonic::Code::InvalidArgument); println!("✓ Rejected with: {}", status.message()); assert!(status.message().contains("Symbol cannot be empty")); } Ok(()) } #[tokio::test] async fn test_submit_invalid_negative_quantity() -> Result<()> { println!("\n=== Test: Reject Negative Quantity ==="); 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: "test_account_004".to_string(), symbol: "MSFT".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: -50.0, // Invalid: negative quantity price: None, stop_price: None, metadata, }); let result = service.submit_order(request).await; assert!(result.is_err(), "Negative quantity should be rejected"); if let Err(status) = result { assert_eq!(status.code(), tonic::Code::InvalidArgument); println!("✓ Rejected with: {}", status.message()); assert!(status.message().contains("Quantity must be positive")); } Ok(()) } #[tokio::test] async fn test_submit_invalid_zero_quantity() -> Result<()> { println!("\n=== Test: Reject Zero Quantity ==="); 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: "test_account_005".to_string(), symbol: "TSLA".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 0.0, // Invalid: zero quantity price: None, stop_price: None, metadata, }); let result = service.submit_order(request).await; assert!(result.is_err(), "Zero quantity should be rejected"); if let Err(status) = result { assert_eq!(status.code(), tonic::Code::InvalidArgument); println!("✓ Rejected with: {}", status.message()); } 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 mut metadata = std::collections::HashMap::new(); metadata.insert("time_in_force".to_string(), "GTC".to_string()); let submit_req = Request::new(SubmitOrderRequest { account_id: "test_account_006".to_string(), symbol: "NVDA".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 25.0, price: Some(500.0), stop_price: None, metadata, }); let submit_response = service.submit_order(submit_req).await?; let order_id = submit_response.into_inner().order_id; println!(" Order created: {}", order_id); // Now cancel it let cancel_req = Request::new(CancelOrderRequest { order_id: order_id.clone(), account_id: "test_account_006".to_string(), }); let cancel_response = service.cancel_order(cancel_req).await?; let cancel_result = cancel_response.into_inner(); println!("✓ Order cancelled: {}", order_id); assert!(cancel_result.success); Ok(()) } #[tokio::test] async fn test_cancel_nonexistent_order() -> Result<()> { println!("\n=== Test: Cancel Nonexistent Order ==="); let service = setup_trading_service().await?; let request = Request::new(CancelOrderRequest { order_id: "nonexistent_order_12345".to_string(), account_id: "test_account_007".to_string(), }); let result = service.cancel_order(request).await; // Should either return error or indicate failure in response match result { Ok(response) => { let cancel_result = response.into_inner(); assert!( !cancel_result.success, "Cancelling nonexistent order should fail" ); println!( "✓ Cancellation failed as expected: {}", cancel_result.message ); }, Err(status) => { println!("✓ Rejected with status: {}", status.code()); }, } Ok(()) } #[tokio::test] async fn test_get_order_status() -> Result<()> { println!("\n=== Test: Get Order Status ==="); let service = setup_trading_service().await?; // Submit an order first let mut metadata = std::collections::HashMap::new(); metadata.insert("time_in_force".to_string(), "GTC".to_string()); let submit_req = Request::new(SubmitOrderRequest { account_id: "test_account_008".to_string(), symbol: "AMD".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_req).await?; let order_id = submit_response.into_inner().order_id; // Get status let status_req = Request::new(GetOrderStatusRequest { order_id: order_id.clone(), }); let status_response = service.get_order_status(status_req).await?; let order_status = status_response.into_inner(); println!( "✓ Order status retrieved: {:?}", order_status.order.as_ref().map(|o| o.status) ); assert!(order_status.order.is_some()); assert!(!order_status.order.unwrap().order_id.is_empty()); Ok(()) } #[tokio::test] async fn test_get_positions() -> Result<()> { println!("\n=== Test: Get Positions ==="); let service = setup_trading_service().await?; let request = Request::new(GetPositionsRequest { account_id: Some("test_account_009".to_string()), symbol: None, // Get all positions }); let response = service.get_positions(request).await?; let positions = response.into_inner(); println!("✓ Retrieved {} positions", positions.positions.len()); Ok(()) } #[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![]; // Submit 10 orders 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_order_id".to_string(), format!("concurrent_order_{}", i), ); let request = Request::new(SubmitOrderRequest { account_id: format!("test_account_{:03}", i), symbol: "SPY".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 10.0, price: None, stop_price: None, metadata, }); svc.submit_order(request).await }); handles.push(handle); } // Wait for all to complete let mut success_count = 0; for handle in handles { if let Ok(Ok(_)) = handle.await { success_count += 1; } } println!( "✓ {}/10 concurrent orders submitted successfully", success_count ); assert_eq!(success_count, 10, "All concurrent orders should succeed"); Ok(()) } #[tokio::test] async fn test_risk_violation_rejection() -> Result<()> { println!("\n=== Test: Risk Violation Rejection ==="); let service = setup_trading_service().await?; // Attempt to submit an order with very large quantity that should trigger risk limits let mut metadata = std::collections::HashMap::new(); metadata.insert("time_in_force".to_string(), "GTC".to_string()); let request = Request::new(SubmitOrderRequest { account_id: "test_account_010".to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 1_000_000.0, // Very large quantity (exceeds 100k limit) price: None, stop_price: None, metadata, }); let result = service.submit_order(request).await; // Should be rejected due to risk limits (quantity > 100,000) assert!(result.is_err(), "Large quantity should be rejected"); if let Err(status) = result { assert_eq!(status.code(), tonic::Code::FailedPrecondition); println!("✓ Risk violation rejected: {}", status.message()); assert!( status.message().contains("Risk violation") || status.message().contains("exceeds maximum") ); } Ok(()) } #[tokio::test] async fn test_kill_switch_blocks_trading() -> Result<()> { println!("\n=== Test: Kill Switch Blocks Trading ==="); let service = setup_trading_service().await?; // Trigger kill switch (implementation depends on state configuration) // For now, this is a placeholder for when kill switch is active let mut metadata = std::collections::HashMap::new(); metadata.insert("time_in_force".to_string(), "GTC".to_string()); let request = Request::new(SubmitOrderRequest { account_id: "test_account_011".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 result = service.submit_order(request).await; // When kill switch is active, should be rejected // This test will evolve based on kill switch implementation println!(" Kill switch test result: {:?}", result.is_ok()); Ok(()) } #[tokio::test] async fn test_order_submission_latency() -> Result<()> { println!("\n=== Test: Order Submission Latency ==="); let service = setup_trading_service().await?; let mut latencies = vec![]; // Submit 100 orders and measure latency for i in 1..=100 { let mut metadata = std::collections::HashMap::new(); metadata.insert("time_in_force".to_string(), "GTC".to_string()); metadata.insert("client_order_id".to_string(), format!("perf_order_{}", i)); let request = Request::new(SubmitOrderRequest { account_id: "perf_test_account".to_string(), symbol: "SPY".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 1.0, price: None, stop_price: None, metadata, }); let start = std::time::Instant::now(); let _ = service.submit_order(request).await; let elapsed = start.elapsed(); latencies.push(elapsed); } // Calculate percentiles latencies.sort(); let p50 = latencies[49]; let p95 = latencies[94]; let p99 = latencies[98]; println!("\n Latency Metrics:"); println!(" ├─ P50: {:?}", p50); println!(" ├─ P95: {:?}", p95); println!(" └─ P99: {:?}", p99); // Warn if latencies are too high (thresholds depend on requirements) if p99 > std::time::Duration::from_millis(100) { println!(" ⚠ WARNING: P99 latency exceeds 100ms"); } Ok(()) }