Files
foxhunt/bin/fxt/tests/integration/service_integration_tests.rs
jgrusewski 463b2cd130 refactor(fxt): complete TLI→FXT rename + fix token storage panic
- 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>
2026-03-04 14:43:07 +01:00

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");
}
}