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)
876 lines
26 KiB
Rust
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(())
|
|
}
|