- Rename TliConfig→FxtConfig, TliError→FxtError, TliResult→FxtResult - Migrate token storage path foxhunt-tli→foxhunt-fxt with auto-migration - Fix FileTokenStorage::Default panic (fallback to /tmp on missing HOME) - Update all doc comments, login prompt, config.toml.example, env vars - Update keyring service names foxhunt-tli-access→foxhunt-fxt-access - Add TOKEN_REFRESH_BUFFER_SECS named constant - Add clarifying comments for JWT dummy key and IBKR default client ID Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
836 lines
27 KiB
Rust
836 lines
27 KiB
Rust
//! Comprehensive service integration tests for TLI system
|
|
//!
|
|
//! This module tests the communication between TLI client and various services
|
|
//! including Trading Service, Risk Management, Market Data, and Backtesting Service.
|
|
|
|
use fake::faker::company::en::*;
|
|
use fake::faker::finance::en::*;
|
|
use fake::{Fake, Faker};
|
|
use std::collections::HashMap;
|
|
use std::time::Duration;
|
|
use tokio::time::{sleep, timeout};
|
|
use tonic::{Request, Response, Status};
|
|
use uuid::Uuid;
|
|
|
|
use crate::integration::{TestConfig, TestUtilities};
|
|
use crate::mocks::grpc_server::{MockBacktestingServer, MockRiskServer, MockTradingServer};
|
|
use fxt::client::{
|
|
BacktestingClient, BacktestingClientConfig, TliClientBuilder, TradingClient,
|
|
TradingClientConfig,
|
|
};
|
|
// Database imports removed - TLI is pure client
|
|
use fxt::prelude::*;
|
|
|
|
/// Service integration test suite
|
|
pub struct ServiceIntegrationTests {
|
|
config: TestConfig,
|
|
trading_client: Option<TradingClient>,
|
|
backtesting_client: Option<BacktestingClient>,
|
|
mock_servers: Vec<Box<dyn MockServer>>,
|
|
}
|
|
|
|
/// Mock server trait for test servers
|
|
pub trait MockServer: Send + Sync {
|
|
fn start(&mut self) -> FxtResult<u16>;
|
|
fn stop(&mut self) -> FxtResult<()>;
|
|
fn is_running(&self) -> bool;
|
|
fn get_port(&self) -> Option<u16>;
|
|
}
|
|
|
|
impl ServiceIntegrationTests {
|
|
pub fn new(config: TestConfig) -> Self {
|
|
Self {
|
|
config,
|
|
trading_client: None,
|
|
backtesting_client: None,
|
|
mock_servers: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Setup test environment with mock services
|
|
pub async fn setup(&mut self) -> FxtResult<()> {
|
|
tracing::info!("Setting up service integration test environment");
|
|
|
|
// Start mock trading service
|
|
let mut trading_server = MockTradingServer::new(self.config.mock_server_port)?;
|
|
let trading_port = trading_server.start()?;
|
|
self.mock_servers.push(Box::new(trading_server));
|
|
|
|
// Start mock backtesting service
|
|
let mut backtesting_server =
|
|
MockBacktestingServer::new(self.config.mock_server_port + 100)?;
|
|
let backtesting_port = backtesting_server.start()?;
|
|
self.mock_servers.push(Box::new(backtesting_server));
|
|
|
|
// Wait for services to be ready
|
|
sleep(Duration::from_millis(500)).await;
|
|
|
|
// Create TLI client suite
|
|
let client_suite = TliClientBuilder::new()
|
|
.with_service_endpoint(
|
|
"trading_service".to_string(),
|
|
format!("http://localhost:{}", trading_port),
|
|
)
|
|
.with_service_endpoint(
|
|
"backtesting_service".to_string(),
|
|
format!("http://localhost:{}", backtesting_port),
|
|
)
|
|
.with_trading_config(TradingClientConfig::default())
|
|
.with_backtesting_config(BacktestingClientConfig::default())
|
|
.build()
|
|
.await?;
|
|
|
|
self.trading_client = client_suite.trading_client;
|
|
self.backtesting_client = client_suite.backtesting_client;
|
|
|
|
tracing::info!("Service integration test environment setup complete");
|
|
Ok(())
|
|
}
|
|
|
|
/// Cleanup test environment
|
|
pub async fn teardown(&mut self) -> FxtResult<()> {
|
|
tracing::info!("Tearing down service integration test environment");
|
|
|
|
// Shutdown clients
|
|
if let Some(client) = self.trading_client.take() {
|
|
client.shutdown().await;
|
|
}
|
|
if let Some(client) = self.backtesting_client.take() {
|
|
client.shutdown().await;
|
|
}
|
|
|
|
// Stop mock servers
|
|
for server in &mut self.mock_servers {
|
|
let _ = server.stop();
|
|
}
|
|
self.mock_servers.clear();
|
|
|
|
tracing::info!("Service integration test environment teardown complete");
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Test TLI Client ↔ Trading Service communication
|
|
#[cfg(test)]
|
|
mod trading_service_tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_order_submission_flow() {
|
|
let mut test_env = ServiceIntegrationTests::new(TestConfig::default());
|
|
test_env
|
|
.setup()
|
|
.await
|
|
.expect("Failed to setup test environment");
|
|
|
|
let trading_client = test_env
|
|
.trading_client
|
|
.as_ref()
|
|
.expect("Trading client not available");
|
|
|
|
// Test order submission
|
|
let order_request = SubmitOrderRequest {
|
|
symbol: TestUtilities::generate_test_symbol("AAPL"),
|
|
side: OrderSide::Buy as i32,
|
|
order_type: OrderType::Market as i32,
|
|
quantity: 100.0,
|
|
price: Some(150.50),
|
|
stop_price: None,
|
|
time_in_force: "DAY".to_string(),
|
|
client_order_id: Uuid::new_v4().to_string(),
|
|
};
|
|
|
|
let result = timeout(
|
|
Duration::from_secs(10),
|
|
trading_client.submit_order(order_request),
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_ok(), "Order submission timed out");
|
|
let response = result.unwrap().expect("Order submission failed");
|
|
assert!(response.success, "Order submission was not successful");
|
|
assert!(
|
|
!response.order_id.is_empty(),
|
|
"Order ID should not be empty"
|
|
);
|
|
|
|
test_env
|
|
.teardown()
|
|
.await
|
|
.expect("Failed to teardown test environment");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_order_cancellation_flow() {
|
|
let mut test_env = ServiceIntegrationTests::new(TestConfig::default());
|
|
test_env
|
|
.setup()
|
|
.await
|
|
.expect("Failed to setup test environment");
|
|
|
|
let trading_client = test_env
|
|
.trading_client
|
|
.as_ref()
|
|
.expect("Trading client not available");
|
|
|
|
// First submit an order
|
|
let order_request = SubmitOrderRequest {
|
|
symbol: TestUtilities::generate_test_symbol("GOOGL"),
|
|
side: OrderSide::Buy as i32,
|
|
order_type: OrderType::Limit as i32,
|
|
quantity: 50.0,
|
|
price: Some(2800.00),
|
|
stop_price: None,
|
|
time_in_force: "GTC".to_string(),
|
|
client_order_id: Uuid::new_v4().to_string(),
|
|
};
|
|
|
|
let submit_response = trading_client
|
|
.submit_order(order_request)
|
|
.await
|
|
.expect("Failed to submit order");
|
|
assert!(submit_response.success, "Order submission failed");
|
|
|
|
// Then cancel the order
|
|
let cancel_request = CancelOrderRequest {
|
|
order_id: submit_response.order_id.clone(),
|
|
symbol: TestUtilities::generate_test_symbol("GOOGL"),
|
|
};
|
|
|
|
let cancel_response = trading_client
|
|
.cancel_order(cancel_request)
|
|
.await
|
|
.expect("Failed to cancel order");
|
|
assert!(cancel_response.success, "Order cancellation failed");
|
|
|
|
test_env
|
|
.teardown()
|
|
.await
|
|
.expect("Failed to teardown test environment");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_position_monitoring() {
|
|
let mut test_env = ServiceIntegrationTests::new(TestConfig::default());
|
|
test_env
|
|
.setup()
|
|
.await
|
|
.expect("Failed to setup test environment");
|
|
|
|
let trading_client = test_env
|
|
.trading_client
|
|
.as_ref()
|
|
.expect("Trading client not available");
|
|
|
|
// Test position query
|
|
let position_request = GetPositionsRequest {
|
|
account_id: Some("test_account".to_string()),
|
|
symbol_filter: None,
|
|
include_zero_positions: false,
|
|
};
|
|
|
|
let positions_response = trading_client
|
|
.get_positions(position_request)
|
|
.await
|
|
.expect("Failed to get positions");
|
|
|
|
// Validate response structure
|
|
assert!(
|
|
!positions_response.positions.is_empty(),
|
|
"Should have at least mock positions"
|
|
);
|
|
|
|
for position in &positions_response.positions {
|
|
assert!(
|
|
!position.symbol.is_empty(),
|
|
"Position symbol should not be empty"
|
|
);
|
|
assert!(
|
|
position.quantity != 0.0,
|
|
"Position quantity should not be zero"
|
|
);
|
|
}
|
|
|
|
test_env
|
|
.teardown()
|
|
.await
|
|
.expect("Failed to teardown test environment");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_market_data_subscription() {
|
|
let mut test_env = ServiceIntegrationTests::new(TestConfig::default());
|
|
test_env
|
|
.setup()
|
|
.await
|
|
.expect("Failed to setup test environment");
|
|
|
|
let trading_client = test_env
|
|
.trading_client
|
|
.as_ref()
|
|
.expect("Trading client not available");
|
|
|
|
// Test market data subscription
|
|
let subscription_request = MarketDataSubscriptionRequest {
|
|
symbols: vec!["AAPL".to_string(), "GOOGL".to_string(), "MSFT".to_string()],
|
|
data_types: vec![MarketDataType::Quote as i32, MarketDataType::Trade as i32],
|
|
include_level2: false,
|
|
};
|
|
|
|
let mut stream = trading_client
|
|
.subscribe_market_data(subscription_request)
|
|
.await
|
|
.expect("Failed to subscribe to market data");
|
|
|
|
// Collect a few market data updates
|
|
let mut updates_received = 0;
|
|
let timeout_duration = Duration::from_secs(5);
|
|
let start_time = std::time::Instant::now();
|
|
|
|
while updates_received < 3 && start_time.elapsed() < timeout_duration {
|
|
match timeout(Duration::from_millis(500), stream.recv()).await {
|
|
Ok(Some(update)) => {
|
|
assert!(
|
|
!update.symbol.is_empty(),
|
|
"Market data symbol should not be empty"
|
|
);
|
|
assert!(
|
|
update.timestamp > 0,
|
|
"Market data timestamp should be valid"
|
|
);
|
|
updates_received += 1;
|
|
tracing::info!("Received market data update for {}", update.symbol);
|
|
}
|
|
Ok(None) => break,
|
|
Err(_) => {
|
|
tracing::warn!("Market data update timed out");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
updates_received > 0,
|
|
"Should have received at least one market data update"
|
|
);
|
|
|
|
test_env
|
|
.teardown()
|
|
.await
|
|
.expect("Failed to teardown test environment");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_risk_management_validation() {
|
|
let mut test_env = ServiceIntegrationTests::new(TestConfig::default());
|
|
test_env
|
|
.setup()
|
|
.await
|
|
.expect("Failed to setup test environment");
|
|
|
|
let trading_client = test_env
|
|
.trading_client
|
|
.as_ref()
|
|
.expect("Trading client not available");
|
|
|
|
// Test pre-trade risk validation
|
|
let risk_check_request = PreTradeRiskCheckRequest {
|
|
symbol: TestUtilities::generate_test_symbol("TSLA"),
|
|
side: OrderSide::Buy as i32,
|
|
quantity: 1000.0, // Large quantity to trigger risk limits
|
|
estimated_price: Some(800.0),
|
|
order_type: OrderType::Market as i32,
|
|
account_id: "test_account".to_string(),
|
|
};
|
|
|
|
let risk_response = trading_client
|
|
.check_pre_trade_risk(risk_check_request)
|
|
.await
|
|
.expect("Failed to perform risk check");
|
|
|
|
// Validate risk check response
|
|
assert!(
|
|
risk_response.risk_score >= 0.0 && risk_response.risk_score <= 1.0,
|
|
"Risk score should be between 0 and 1"
|
|
);
|
|
|
|
if !risk_response.approved {
|
|
assert!(
|
|
!risk_response.rejection_reasons.is_empty(),
|
|
"Rejection reasons should be provided when order is not approved"
|
|
);
|
|
}
|
|
|
|
test_env
|
|
.teardown()
|
|
.await
|
|
.expect("Failed to teardown test environment");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_portfolio_analytics() {
|
|
let mut test_env = ServiceIntegrationTests::new(TestConfig::default());
|
|
test_env
|
|
.setup()
|
|
.await
|
|
.expect("Failed to setup test environment");
|
|
|
|
let trading_client = test_env
|
|
.trading_client
|
|
.as_ref()
|
|
.expect("Trading client not available");
|
|
|
|
// Test portfolio analytics
|
|
let analytics_request = PortfolioAnalyticsRequest {
|
|
account_id: "test_account".to_string(),
|
|
calculation_date: chrono::Utc::now().timestamp(),
|
|
include_realized_pnl: true,
|
|
include_unrealized_pnl: true,
|
|
include_risk_metrics: true,
|
|
};
|
|
|
|
let analytics_response = trading_client
|
|
.get_portfolio_analytics(analytics_request)
|
|
.await
|
|
.expect("Failed to get portfolio analytics");
|
|
|
|
// Validate analytics response
|
|
assert!(
|
|
analytics_response.total_portfolio_value.is_some(),
|
|
"Total portfolio value should be provided"
|
|
);
|
|
assert!(
|
|
analytics_response.daily_pnl.is_some(),
|
|
"Daily P&L should be provided"
|
|
);
|
|
assert!(
|
|
!analytics_response.position_analytics.is_empty(),
|
|
"Position analytics should not be empty"
|
|
);
|
|
|
|
test_env
|
|
.teardown()
|
|
.await
|
|
.expect("Failed to teardown test environment");
|
|
}
|
|
}
|
|
|
|
/// Test TLI Client ↔ Backtesting Service communication
|
|
#[cfg(test)]
|
|
mod backtesting_service_tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_backtest_execution_flow() {
|
|
let mut test_env = ServiceIntegrationTests::new(TestConfig::default());
|
|
test_env
|
|
.setup()
|
|
.await
|
|
.expect("Failed to setup test environment");
|
|
|
|
let backtesting_client = test_env
|
|
.backtesting_client
|
|
.as_ref()
|
|
.expect("Backtesting client not available");
|
|
|
|
// Create backtest configuration
|
|
let backtest_request = CreateBacktestRequest {
|
|
name: format!("Integration Test Backtest {}", Uuid::new_v4()),
|
|
strategy_id: "test_strategy".to_string(),
|
|
start_date: "2024-01-01".to_string(),
|
|
end_date: "2024-01-31".to_string(),
|
|
initial_capital: 100000.0,
|
|
symbols: vec!["AAPL".to_string(), "GOOGL".to_string()],
|
|
parameters: HashMap::new(),
|
|
};
|
|
|
|
// Start backtest
|
|
let create_response = backtesting_client
|
|
.create_backtest(backtest_request)
|
|
.await
|
|
.expect("Failed to create backtest");
|
|
assert!(create_response.success, "Backtest creation failed");
|
|
assert!(
|
|
!create_response.backtest_id.is_empty(),
|
|
"Backtest ID should not be empty"
|
|
);
|
|
|
|
let backtest_id = create_response.backtest_id;
|
|
|
|
// Start backtest execution
|
|
let start_request = StartBacktestRequest {
|
|
backtest_id: backtest_id.clone(),
|
|
async_execution: true,
|
|
};
|
|
|
|
let start_response = backtesting_client
|
|
.start_backtest(start_request)
|
|
.await
|
|
.expect("Failed to start backtest");
|
|
assert!(start_response.success, "Backtest start failed");
|
|
|
|
// Monitor backtest progress
|
|
let mut progress_checks = 0;
|
|
let max_progress_checks = 10;
|
|
|
|
while progress_checks < max_progress_checks {
|
|
let status_request = GetBacktestStatusRequest {
|
|
backtest_id: backtest_id.clone(),
|
|
};
|
|
|
|
let status_response = backtesting_client
|
|
.get_backtest_status(status_request)
|
|
.await
|
|
.expect("Failed to get backtest status");
|
|
|
|
tracing::info!(
|
|
"Backtest status: {:?}, Progress: {}%",
|
|
status_response.status,
|
|
status_response.progress_percentage
|
|
);
|
|
|
|
if status_response.status == BacktestStatus::Completed as i32 {
|
|
break;
|
|
}
|
|
|
|
progress_checks += 1;
|
|
sleep(Duration::from_millis(100)).await;
|
|
}
|
|
|
|
// Get backtest results
|
|
let results_request = GetBacktestResultsRequest {
|
|
backtest_id: backtest_id.clone(),
|
|
include_trades: true,
|
|
include_metrics: true,
|
|
};
|
|
|
|
let results_response = backtesting_client
|
|
.get_backtest_results(results_request)
|
|
.await
|
|
.expect("Failed to get backtest results");
|
|
|
|
// Validate results
|
|
assert!(
|
|
results_response.performance_summary.is_some(),
|
|
"Performance summary should be provided"
|
|
);
|
|
assert!(
|
|
!results_response.trades.is_empty(),
|
|
"Should have executed some trades"
|
|
);
|
|
|
|
test_env
|
|
.teardown()
|
|
.await
|
|
.expect("Failed to teardown test environment");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_strategy_optimization() {
|
|
let mut test_env = ServiceIntegrationTests::new(TestConfig::default());
|
|
test_env
|
|
.setup()
|
|
.await
|
|
.expect("Failed to setup test environment");
|
|
|
|
let backtesting_client = test_env
|
|
.backtesting_client
|
|
.as_ref()
|
|
.expect("Backtesting client not available");
|
|
|
|
// Create optimization request
|
|
let optimization_request = StrategyOptimizationRequest {
|
|
strategy_id: "test_strategy".to_string(),
|
|
start_date: "2024-01-01".to_string(),
|
|
end_date: "2024-03-31".to_string(),
|
|
symbols: vec!["AAPL".to_string()],
|
|
parameter_ranges: create_test_parameter_ranges(),
|
|
optimization_metric: OptimizationMetric::SharpeRatio as i32,
|
|
max_iterations: 50,
|
|
};
|
|
|
|
// Start optimization
|
|
let optimization_response = backtesting_client
|
|
.optimize_strategy(optimization_request)
|
|
.await
|
|
.expect("Failed to start strategy optimization");
|
|
|
|
assert!(
|
|
optimization_response.success,
|
|
"Strategy optimization start failed"
|
|
);
|
|
assert!(
|
|
!optimization_response.optimization_id.is_empty(),
|
|
"Optimization ID should not be empty"
|
|
);
|
|
|
|
test_env
|
|
.teardown()
|
|
.await
|
|
.expect("Failed to teardown test environment");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_backtest_data_management() {
|
|
let mut test_env = ServiceIntegrationTests::new(TestConfig::default());
|
|
test_env
|
|
.setup()
|
|
.await
|
|
.expect("Failed to setup test environment");
|
|
|
|
let backtesting_client = test_env
|
|
.backtesting_client
|
|
.as_ref()
|
|
.expect("Backtesting client not available");
|
|
|
|
// Test data availability check
|
|
let data_request = CheckDataAvailabilityRequest {
|
|
symbols: vec!["AAPL".to_string(), "GOOGL".to_string(), "MSFT".to_string()],
|
|
start_date: "2024-01-01".to_string(),
|
|
end_date: "2024-01-31".to_string(),
|
|
data_types: vec![DataType::OHLCV as i32, DataType::Trades as i32],
|
|
};
|
|
|
|
let data_response = backtesting_client
|
|
.check_data_availability(data_request)
|
|
.await
|
|
.expect("Failed to check data availability");
|
|
|
|
// Validate data availability response
|
|
assert!(
|
|
!data_response.symbol_availability.is_empty(),
|
|
"Symbol availability should not be empty"
|
|
);
|
|
|
|
for (symbol, availability) in &data_response.symbol_availability {
|
|
assert!(!symbol.is_empty(), "Symbol should not be empty");
|
|
assert!(
|
|
availability.coverage_percentage >= 0.0
|
|
&& availability.coverage_percentage <= 100.0,
|
|
"Coverage percentage should be between 0 and 100"
|
|
);
|
|
}
|
|
|
|
test_env
|
|
.teardown()
|
|
.await
|
|
.expect("Failed to teardown test environment");
|
|
}
|
|
|
|
fn create_test_parameter_ranges() -> HashMap<String, ParameterRange> {
|
|
let mut ranges = HashMap::new();
|
|
|
|
ranges.insert(
|
|
"lookback_period".to_string(),
|
|
ParameterRange {
|
|
min_value: 5.0,
|
|
max_value: 50.0,
|
|
step_size: 5.0,
|
|
parameter_type: ParameterType::Integer as i32,
|
|
},
|
|
);
|
|
|
|
ranges.insert(
|
|
"threshold".to_string(),
|
|
ParameterRange {
|
|
min_value: 0.01,
|
|
max_value: 0.1,
|
|
step_size: 0.01,
|
|
parameter_type: ParameterType::Float as i32,
|
|
},
|
|
);
|
|
|
|
ranges
|
|
}
|
|
}
|
|
|
|
/// Configuration management integration tests
|
|
#[cfg(test)]
|
|
mod config_integration_tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_configuration_lifecycle() {
|
|
let mut test_env = ServiceIntegrationTests::new(TestConfig::default());
|
|
test_env
|
|
.setup()
|
|
.await
|
|
.expect("Failed to setup test environment");
|
|
|
|
let trading_client = test_env
|
|
.trading_client
|
|
.as_ref()
|
|
.expect("Trading client not available");
|
|
|
|
// Test configuration retrieval
|
|
let get_config_request = GetConfigurationRequest {
|
|
config_type: ConfigurationType::TradingLimits as i32,
|
|
account_id: Some("test_account".to_string()),
|
|
};
|
|
|
|
let config_response = trading_client
|
|
.get_configuration(get_config_request)
|
|
.await
|
|
.expect("Failed to get configuration");
|
|
|
|
assert!(
|
|
!config_response.configurations.is_empty(),
|
|
"Should have configurations"
|
|
);
|
|
|
|
// Test configuration update
|
|
let updated_config = Configuration {
|
|
id: config_response.configurations[0].id.clone(),
|
|
config_type: ConfigurationType::TradingLimits as i32,
|
|
key: "max_position_size".to_string(),
|
|
value: "1000000".to_string(),
|
|
account_id: Some("test_account".to_string()),
|
|
last_updated: chrono::Utc::now().timestamp(),
|
|
};
|
|
|
|
let update_request = UpdateConfigurationRequest {
|
|
configurations: vec![updated_config],
|
|
validate_before_update: true,
|
|
};
|
|
|
|
let update_response = trading_client
|
|
.update_configuration(update_request)
|
|
.await
|
|
.expect("Failed to update configuration");
|
|
|
|
assert!(
|
|
update_response.success,
|
|
"Configuration update should succeed"
|
|
);
|
|
assert!(
|
|
update_response.validation_errors.is_empty(),
|
|
"Should have no validation errors"
|
|
);
|
|
|
|
test_env
|
|
.teardown()
|
|
.await
|
|
.expect("Failed to teardown test environment");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hot_reload_configuration() {
|
|
let mut test_env = ServiceIntegrationTests::new(TestConfig::default());
|
|
test_env
|
|
.setup()
|
|
.await
|
|
.expect("Failed to setup test environment");
|
|
|
|
let trading_client = test_env
|
|
.trading_client
|
|
.as_ref()
|
|
.expect("Trading client not available");
|
|
|
|
// Test hot reload trigger
|
|
let reload_request = TriggerConfigReloadRequest {
|
|
services: vec!["trading_service".to_string(), "risk_service".to_string()],
|
|
config_types: vec![ConfigurationType::TradingLimits as i32],
|
|
};
|
|
|
|
let reload_response = trading_client
|
|
.trigger_config_reload(reload_request)
|
|
.await
|
|
.expect("Failed to trigger config reload");
|
|
|
|
assert!(reload_response.success, "Config reload should succeed");
|
|
|
|
for result in &reload_response.service_results {
|
|
assert!(result.success, "Each service reload should succeed");
|
|
}
|
|
|
|
test_env
|
|
.teardown()
|
|
.await
|
|
.expect("Failed to teardown test environment");
|
|
}
|
|
}
|
|
|
|
/// Event streaming integration tests
|
|
#[cfg(test)]
|
|
mod event_streaming_tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_event_streaming_lifecycle() {
|
|
let mut test_env = ServiceIntegrationTests::new(TestConfig::default());
|
|
test_env
|
|
.setup()
|
|
.await
|
|
.expect("Failed to setup test environment");
|
|
|
|
let trading_client = test_env
|
|
.trading_client
|
|
.as_ref()
|
|
.expect("Trading client not available");
|
|
|
|
// Subscribe to events
|
|
let subscription_request = EventSubscriptionRequest {
|
|
event_types: vec![
|
|
EventType::OrderExecuted as i32,
|
|
EventType::PositionChanged as i32,
|
|
EventType::RiskLimitBreached as i32,
|
|
],
|
|
filters: HashMap::new(),
|
|
};
|
|
|
|
let mut event_stream = trading_client
|
|
.subscribe_to_events(subscription_request)
|
|
.await
|
|
.expect("Failed to subscribe to events");
|
|
|
|
// Generate some events by placing orders
|
|
let order_request = SubmitOrderRequest {
|
|
symbol: TestUtilities::generate_test_symbol("AAPL"),
|
|
side: OrderSide::Buy as i32,
|
|
order_type: OrderType::Market as i32,
|
|
quantity: 100.0,
|
|
price: None,
|
|
stop_price: None,
|
|
time_in_force: "DAY".to_string(),
|
|
client_order_id: Uuid::new_v4().to_string(),
|
|
};
|
|
|
|
// Submit order to generate events
|
|
let _order_response = trading_client
|
|
.submit_order(order_request)
|
|
.await
|
|
.expect("Failed to submit order");
|
|
|
|
// Collect events
|
|
let mut events_received = 0;
|
|
let timeout_duration = Duration::from_secs(5);
|
|
let start_time = std::time::Instant::now();
|
|
|
|
while events_received < 2 && start_time.elapsed() < timeout_duration {
|
|
match timeout(Duration::from_millis(500), event_stream.recv()).await {
|
|
Ok(Some(event)) => {
|
|
assert!(!event.event_id.is_empty(), "Event ID should not be empty");
|
|
assert!(event.timestamp > 0, "Event timestamp should be valid");
|
|
events_received += 1;
|
|
tracing::info!("Received event: {:?}", event.event_type);
|
|
}
|
|
Ok(None) => break,
|
|
Err(_) => {
|
|
tracing::warn!("Event stream timed out");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
events_received > 0,
|
|
"Should have received at least one event"
|
|
);
|
|
|
|
test_env
|
|
.teardown()
|
|
.await
|
|
.expect("Failed to teardown test environment");
|
|
}
|
|
}
|