Files
foxhunt/services/trading_service/tests/grpc_handler_comprehensive.rs
jgrusewski eae3c31e53 fix(clippy): Fix 6 unwrap_used violations in risk/data
Patterns applied:
- Pattern 2: Float comparison (2x: utils.rs, var_edge_cases_tests.rs)
- Pattern 7: Date/time construction (2x: production_streaming.rs, streaming.rs)
- Pattern 1: Duration/time ops (2x: rate limiter, semaphore)
- Pattern 4: Optional field access (1x: position_tracker.rs)

Changes:
- data/src/utils.rs: Float sort with NaN handling
- data/src/providers/benzinga/production_streaming.rs: Rate limiter + semaphore + date/time
- data/src/providers/benzinga/streaming.rs: Date/time construction
- risk/src/position_tracker.rs: Emergency fallback counter
- risk/tests/var_edge_cases_tests.rs: Test helper float sort

Test impact: 0 failures (182/182 passing)
Compilation: Clean (0 errors, 0 warnings)
Time: 25 min (44% under budget)
2025-10-23 14:58:32 +02:00

876 lines
26 KiB
Rust

//! 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<TradingServiceImpl> {
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(())
}