Files
foxhunt/testing/integration/mocks/mod.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

737 lines
23 KiB
Rust

//! Mock Services for Integration Testing
//!
//! This module provides comprehensive mock implementations of all trading system
//! services for integration testing, including realistic latency simulation,
//! configurable failure rates, and comprehensive response patterns.
use std::collections::HashMap;
use std::sync::{Arc, atomic::{AtomicU16, AtomicU64, Ordering}};
use std::time::{Duration, Instant};
use tokio::sync::{mpsc, RwLock, Mutex};
use uuid::Uuid;
use serde_json::json;
use chrono::{DateTime, Utc};
use fxt::prelude::*;
use crate::fixtures::*;
pub mod mock_trading_service;
pub mod mock_backtesting_service;
pub mod mock_database;
pub mod mock_ml_infrastructure;
/// Mock trading service implementation for comprehensive integration testing
///
/// Provides realistic simulation of the trading service including configurable
/// latency, failure rates, order lifecycle management, and circuit breaker behavior.
pub struct MockTradingService {
/// TCP port the mock service is listening on
port: u16,
/// Handle to the background server task
server_handle: Option<tokio::task::JoinHandle<()>>,
/// Configured responses for specific order IDs
order_responses: Arc<RwLock<HashMap<String, SubmitOrderResponse>>>,
/// Configured risk rejections for testing risk management
risk_rejections: Arc<RwLock<HashMap<String, String>>>,
/// Counter for testing failure sequences
failure_sequence_count: Arc<AtomicU64>,
/// Simulated order lifecycle progressions
lifecycle_simulations: Arc<RwLock<HashMap<String, Vec<OrderStatus>>>>,
/// Circuit breaker state for testing resilience
circuit_breaker_status: Arc<RwLock<CircuitBreakerStatus>>,
/// Overall trading system status
trading_status: Arc<RwLock<TradingStatus>>,
/// Service configuration including latency and failure rates
config: MockServiceConfig,
}
/// Configuration for mock service behavior
///
/// Controls latency simulation, failure injection, chaos testing,
/// and connection limits for realistic testing scenarios.
#[derive(Debug, Clone)]
pub struct MockServiceConfig {
/// Simulated service latency in milliseconds
pub latency_ms: u64,
/// Failure injection rate (0.0 to 1.0)
pub failure_rate: f64,
/// Whether to enable chaos testing features
pub enable_chaos: bool,
/// Maximum number of concurrent connections
pub max_connections: usize,
}
impl Default for MockServiceConfig {
fn default() -> Self {
Self {
latency_ms: 10,
failure_rate: 0.01, // 1%
enable_chaos: false,
max_connections: 100,
}
}
}
// OrderStatus now imported from canonical source
use common::OrderStatus;
/// Circuit breaker state for testing resilience patterns
///
/// Tracks failure counts and circuit breaker state to validate
/// proper handling of service degradation scenarios.
#[derive(Debug, Clone)]
pub struct CircuitBreakerStatus {
/// Whether the circuit breaker is currently open
pub is_open: bool,
/// Number of consecutive failures
pub failure_count: u32,
/// Timestamp of the most recent failure
pub last_failure_time: Option<DateTime<Utc>>,
}
impl Default for CircuitBreakerStatus {
fn default() -> Self {
Self {
is_open: false,
failure_count: 0,
last_failure_time: None,
}
}
}
/// Overall trading system operational status
///
/// Tracks system health, emergency stop state, heartbeat,
/// and key operational metrics for monitoring and testing.
#[derive(Debug, Clone)]
pub struct TradingStatus {
/// Whether the trading system is currently active
pub is_active: bool,
/// Whether emergency stop has been triggered
pub emergency_stop_active: bool,
/// Timestamp of the last system heartbeat
pub last_heartbeat: DateTime<Utc>,
/// Number of currently active orders
pub active_orders: u64,
/// Total trading volume processed
pub total_volume: Decimal,
}
impl Default for TradingStatus {
fn default() -> Self {
Self {
is_active: true,
emergency_stop_active: false,
last_heartbeat: Utc::now(),
active_orders: 0,
total_volume: Decimal::ZERO,
}
}
}
/// Order submission request structure for testing
///
/// Represents the structure of order submission requests
/// used to test the trading service API.
#[derive(Debug, Clone)]
pub struct SubmitOrderRequest {
/// Trading symbol (e.g., "AAPL", "BTCUSD")
pub symbol: String,
/// Order side as integer (Buy/Sell enum value)
pub side: i32,
/// Order type as integer (Market/Limit enum value)
pub order_type: i32,
/// Order quantity
pub quantity: f64,
/// Limit price (None for market orders)
pub price: Option<f64>,
/// Client-provided order identifier
pub client_order_id: String,
/// Additional order metadata
pub metadata: HashMap<String, serde_json::Value>,
}
/// Order submission response structure for testing
///
/// Contains the result of an order submission including
/// success status, assigned order ID, and execution timing.
#[derive(Debug, Clone)]
pub struct SubmitOrderResponse {
/// Whether the order was successfully submitted
pub success: bool,
/// System-assigned order identifier
pub order_id: String,
/// Response message or error description
pub message: String,
/// Order processing time in nanoseconds
pub execution_time_ns: u64,
}
/// Start backtest request structure
#[derive(Debug, Clone)]
pub struct StartBacktestRequest {
pub backtest_id: String,
pub enable_monitoring: bool,
}
/// Start backtest response structure
#[derive(Debug, Clone)]
pub struct StartBacktestResponse {
pub success: bool,
pub message: String,
}
impl MockTradingService {
/// Create a new mock trading service with default configuration
///
/// # Returns
/// * `Ok(MockTradingService)` - Configured mock service ready to start
///
/// * `Err(TliError)` - If port allocation or initialization failed
pub async fn new() -> TliResult<Self> {
Self::new_with_config(MockServiceConfig::default()).await
}
/// Create a new mock trading service with custom configuration
///
/// # Arguments
/// * `config` - Custom service configuration including latency and failure rates
///
/// # Returns
/// * `Ok(MockTradingService)` - Configured mock service
///
/// * `Err(TliError)` - If port allocation failed
pub async fn new_with_config(config: MockServiceConfig) -> TliResult<Self> {
let port = TEST_PORT_MANAGER.allocate_port().await;
Ok(Self {
port,
server_handle: None,
order_responses: Arc::new(RwLock::new(HashMap::new())),
risk_rejections: Arc::new(RwLock::new(HashMap::new())),
failure_sequence_count: Arc::new(AtomicU64::new(0)),
lifecycle_simulations: Arc::new(RwLock::new(HashMap::new())),
circuit_breaker_status: Arc::new(RwLock::new(CircuitBreakerStatus::default())),
trading_status: Arc::new(RwLock::new(TradingStatus::default())),
config,
})
}
/// Get the TCP port number the mock service is bound to
///
/// # Returns
///
/// The port number allocated for this mock service
pub fn port(&self) -> u16 {
self.port
}
/// Configure a specific response for an order submission
///
/// # Arguments
/// * `client_order_id` - Client order ID to configure response for
///
/// * `response` - The response to return for this order
pub async fn configure_order_response(&self, client_order_id: &str, response: SubmitOrderResponse) {
let mut responses = self.order_responses.write().await;
responses.insert(client_order_id.to_string(), response);
}
/// Configure a risk rejection for testing risk management
///
/// # Arguments
/// * `client_order_id` - Order ID to reject
///
/// * `reason` - Rejection reason message
pub async fn configure_risk_rejection(&self, client_order_id: &str, reason: String) {
let mut rejections = self.risk_rejections.write().await;
rejections.insert(client_order_id.to_string(), reason);
}
/// Configure failure sequence for circuit breaker testing
pub async fn configure_failure_sequence(&self, count: u64) {
self.failure_sequence_count.store(count, Ordering::Relaxed);
}
/// Configure order lifecycle simulation
pub async fn configure_lifecycle_simulation(&self, order_id: &str, statuses: Vec<OrderStatus>) {
let mut simulations = self.lifecycle_simulations.write().await;
simulations.insert(order_id.to_string(), statuses);
}
/// Start the mock trading service and begin accepting connections
///
/// # Returns
/// * `Ok(())` - Service started successfully
///
/// * `Err(TliError)` - If service failed to start
pub async fn start(&mut self) -> TliResult<()> {
let port = self.port;
let config = self.config.clone();
let order_responses = Arc::clone(&self.order_responses);
let risk_rejections = Arc::clone(&self.risk_rejections);
let failure_sequence = Arc::clone(&self.failure_sequence_count);
let circuit_breaker = Arc::clone(&self.circuit_breaker_status);
let trading_status = Arc::clone(&self.trading_status);
let handle = tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.expect("Failed to bind mock trading service");
while let Ok((stream, _)) = listener.accept().await {
let responses = Arc::clone(&order_responses);
let rejections = Arc::clone(&risk_rejections);
let failure_seq = Arc::clone(&failure_sequence);
let cb_status = Arc::clone(&circuit_breaker);
let trade_status = Arc::clone(&trading_status);
let service_config = config.clone();
tokio::spawn(async move {
Self::handle_connection(stream, responses, rejections, failure_seq, cb_status, trade_status, service_config).await;
});
}
});
self.server_handle = Some(handle);
Ok(())
}
/// Handle individual client connection
async fn handle_connection(
stream: tokio::net::TcpStream,
order_responses: Arc<RwLock<HashMap<String, SubmitOrderResponse>>>,
risk_rejections: Arc<RwLock<HashMap<String, String>>>,
failure_sequence: Arc<AtomicU64>,
circuit_breaker: Arc<RwLock<CircuitBreakerStatus>>,
trading_status: Arc<RwLock<TradingStatus>>,
config: MockServiceConfig,
) {
// Simulate service latency
if config.latency_ms > 0 {
tokio::time::sleep(Duration::from_millis(config.latency_ms)).await;
}
// Simulate random failures if configured
if config.enable_chaos && fastrand::f64() < config.failure_rate {
return; // Drop connection to simulate failure
}
// Handle gRPC-style protocol simulation
// In a real implementation, this would use tonic/gRPC
// For testing, we'll simulate the essential behavior
}
/// Stop the mock service and release resources
pub async fn stop(&mut self) {
if let Some(handle) = self.server_handle.take() {
handle.abort();
}
TEST_PORT_MANAGER.release_port(self.port).await;
}
}
impl Drop for MockTradingService {
fn drop(&mut self) {
if let Some(handle) = self.server_handle.take() {
handle.abort();
}
}
}
/// Mock backtesting service for integration testing
pub struct MockBacktestingService {
port: u16,
server_handle: Option<tokio::task::JoinHandle<()>>,
backtest_responses: Arc<RwLock<HashMap<String, BacktestResult>>>,
ml_responses: Arc<RwLock<HashMap<String, MLBacktestConfig>>>,
ensemble_responses: Arc<RwLock<HashMap<String, EnsembleResults>>>,
config: MockServiceConfig,
}
/// Backtest result structure
#[derive(Debug, Clone)]
pub struct BacktestResult {
pub backtest_id: String,
pub strategy_name: String,
pub total_return: f64,
pub annualized_return: f64,
pub max_drawdown: f64,
pub sharpe_ratio: f64,
pub total_trades: u64,
pub win_rate: f64,
pub avg_trade_return: f64,
pub final_value: f64,
pub execution_time_ms: u64,
pub events_processed: u64,
}
/// ML backtest configuration
#[derive(Debug, Clone)]
pub struct MLBacktestConfig {
pub model_name: String,
pub expected_return: f64,
pub expected_sharpe: f64,
pub expected_trades: u64,
}
/// Ensemble results structure
#[derive(Debug, Clone)]
pub struct EnsembleResults {
pub ensemble_return: f64,
pub ensemble_sharpe: f64,
pub individual_returns: Vec<f64>,
pub individual_sharpes: Vec<f64>,
pub diversification_benefit: f64,
pub model_weights_final: Vec<f64>,
pub rebalance_count: u32,
}
/// Create backtest request structure
#[derive(Debug, Clone)]
pub struct CreateBacktestRequest {
pub name: String,
pub strategy_type: String,
pub symbol: String,
pub start_date: i64,
pub end_date: i64,
pub initial_capital: f64,
pub parameters: serde_json::Value,
pub enable_real_time_monitoring: bool,
}
/// Create backtest response structure
#[derive(Debug, Clone)]
pub struct CreateBacktestResponse {
pub success: bool,
pub backtest_id: String,
pub message: String,
}
impl MockBacktestingService {
/// Create new mock backtesting service
pub async fn new() -> TliResult<Self> {
let port = TEST_PORT_MANAGER.allocate_port().await;
Ok(Self {
port,
server_handle: None,
backtest_responses: Arc::new(RwLock::new(HashMap::new())),
ml_responses: Arc::new(RwLock::new(HashMap::new())),
ensemble_responses: Arc::new(RwLock::new(HashMap::new())),
config: MockServiceConfig::default(),
})
}
/// Get the port the mock service is running on
pub fn port(&self) -> u16 {
self.port
}
/// Configure backtest response
pub async fn configure_backtest_response(&self, name: &str, result: BacktestResult) {
let mut responses = self.backtest_responses.write().await;
responses.insert(name.to_string(), result);
}
/// Configure ML backtest response
pub async fn configure_ml_backtest_response(
&self,
name: &str,
model_name: &str,
expected_return: f64,
expected_sharpe: f64,
expected_trades: u64,
) {
let mut responses = self.ml_responses.write().await;
responses.insert(name.to_string(), MLBacktestConfig {
model_name: model_name.to_string(),
expected_return,
expected_sharpe,
expected_trades,
});
}
/// Configure ensemble response
pub async fn configure_ensemble_response(&self, name: &str, results: EnsembleResults) {
let mut responses = self.ensemble_responses.write().await;
responses.insert(name.to_string(), results);
}
/// Stop the mock service
pub async fn stop(&mut self) {
if let Some(handle) = self.server_handle.take() {
handle.abort();
}
TEST_PORT_MANAGER.release_port(self.port).await;
}
}
/// Test database manager for PostgreSQL integration testing
pub struct TestDatabaseManager {
pool: sqlx::PgPool,
config: TestDatabaseConfig,
}
/// Test database configuration
#[derive(Debug, Clone)]
pub struct TestDatabaseConfig {
pub database_url: String,
pub max_connections: u32,
pub enable_cleanup: bool,
pub test_schema: String,
}
impl Default for TestDatabaseConfig {
fn default() -> Self {
Self {
database_url: std::env::var("TEST_DATABASE_URL")
.unwrap_or_else(|_| "postgresql://foxhunt_test:test_password@localhost:5432/foxhunt_test".to_string()),
max_connections: 20,
enable_cleanup: true,
test_schema: "test_".to_string(),
}
}
}
impl TestDatabaseManager {
/// Create new test database manager
pub async fn new() -> TliResult<Self> {
Self::new_with_config(TestDatabaseConfig::default()).await
}
/// Create new test database manager with custom configuration
pub async fn new_with_config(config: TestDatabaseConfig) -> TliResult<Self> {
let pool = sqlx::PgPool::connect(&config.database_url)
.await
.map_err(|e| TliError::DatabaseError(format!("Failed to connect to test database: {}", e)))?;
Ok(Self { pool, config })
}
/// Get connection pool
pub fn get_pool(&self) -> &sqlx::PgPool {
&self.pool
}
/// Verify order was persisted
pub async fn verify_order_persisted(&self, order_id: &str) -> TliResult<bool> {
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM orders WHERE order_id = $1")
.bind(order_id)
.fetch_one(&self.pool)
.await
.map_err(|e| TliError::DatabaseError(format!("Failed to verify order persistence: {}", e)))?;
Ok(count > 0)
}
/// Clean up test data
pub async fn cleanup(&self) -> TliResult<()> {
if self.config.enable_cleanup {
let cleanup_queries = vec![
"DELETE FROM trading_events WHERE source LIKE '%test%'",
"DELETE FROM orders WHERE client_order_id LIKE '%test%'",
"DELETE FROM positions WHERE account_id LIKE '%test%'",
"DELETE FROM risk_events WHERE account_id LIKE '%test%'",
"DELETE FROM market_data WHERE symbol LIKE '%TEST%'",
"DELETE FROM performance_metrics WHERE metric_name LIKE '%test%'",
];
for query in cleanup_queries {
sqlx::query(query)
.execute(&self.pool)
.await
.map_err(|e| TliError::DatabaseError(format!("Cleanup failed: {}", e)))?;
}
}
Ok(())
}
}
/// Test database structure
pub struct TestDatabase {
manager: TestDatabaseManager,
}
impl TestDatabase {
/// Create new test database
pub async fn new() -> TliResult<Self> {
let manager = TestDatabaseManager::new().await?;
Ok(Self { manager })
}
/// Verify order was persisted
pub async fn verify_order_persisted(&self, order_id: &str) -> TliResult<bool> {
self.manager.verify_order_persisted(order_id).await
}
}
/// Test data provider for market data simulation
pub struct TestDataProvider {
historical_data: HashMap<String, Vec<MarketDataPoint>>,
}
/// Market data point structure
#[derive(Debug, Clone)]
pub struct MarketDataPoint {
pub timestamp: DateTime<Utc>,
pub symbol: String,
pub price: f64,
pub volume: u64,
pub bid: Option<f64>,
pub ask: Option<f64>,
}
impl TestDataProvider {
/// Create new test data provider
pub async fn new() -> TliResult<Self> {
let mut provider = Self {
historical_data: HashMap::new(),
};
// Generate sample data for common symbols
provider.generate_sample_data("AAPL", 1000).await;
provider.generate_sample_data("MSFT", 1000).await;
provider.generate_sample_data("GOOGL", 1000).await;
Ok(provider)
}
/// Generate sample market data
async fn generate_sample_data(&mut self, symbol: &str, count: usize) {
let mut data = Vec::new();
let mut price = 150.0;
let start_time = Utc::now() - chrono::Duration::days(30);
for i in 0..count {
let timestamp = start_time + chrono::Duration::seconds(i as i64 * 60);
// Simple random walk
price += (fastrand::f64() - 0.5) * 2.0;
price = price.max(100.0).min(200.0); // Keep price in reasonable range
data.push(MarketDataPoint {
timestamp,
symbol: symbol.to_string(),
price,
volume: 1000 + fastrand::u64(0..10000),
bid: Some(price - 0.01),
ask: Some(price + 0.01),
});
}
self.historical_data.insert(symbol.to_string(), data);
}
/// Get historical data for symbol
pub fn get_historical_data(&self, symbol: &str) -> Option<&Vec<MarketDataPoint>> {
self.historical_data.get(symbol)
}
}
/// ML testing infrastructure
pub struct MLTestInfrastructure {
mock_models: HashMap<String, MockMLModel>,
}
/// Mock ML model
#[derive(Debug, Clone)]
pub struct MockMLModel {
pub name: String,
pub inference_latency_ns: u64,
pub accuracy: f64,
pub memory_usage_mb: u64,
}
impl MLTestInfrastructure {
/// Create new ML testing infrastructure
pub async fn new() -> TliResult<Self> {
let mut models = HashMap::new();
// Add mock models with realistic characteristics
models.insert("TLOB".to_string(), MockMLModel {
name: "TLOB".to_string(),
inference_latency_ns: 15_000, // 15µs
accuracy: 0.89,
memory_usage_mb: 256,
});
models.insert("MAMBA".to_string(), MockMLModel {
name: "MAMBA".to_string(),
inference_latency_ns: 25_000, // 25µs
accuracy: 0.91,
memory_usage_mb: 512,
});
models.insert("TFT".to_string(), MockMLModel {
name: "TFT".to_string(),
inference_latency_ns: 35_000, // 35µs
accuracy: 0.87,
memory_usage_mb: 384,
});
models.insert("DQN".to_string(), MockMLModel {
name: "DQN".to_string(),
inference_latency_ns: 20_000, // 20µs
accuracy: 0.84,
memory_usage_mb: 128,
});
Ok(Self {
mock_models: models,
})
}
/// Get mock model
pub fn get_model(&self, name: &str) -> Option<&MockMLModel> {
self.mock_models.get(name)
}
/// List available models
pub fn list_models(&self) -> Vec<&str> {
self.mock_models.keys().map(|s| s.as_str()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_mock_trading_service() {
let mut service = MockTradingService::new().await.unwrap();
assert!(service.port() > 0);
service.configure_order_response("test_order", SubmitOrderResponse {
success: true,
order_id: "order_123".to_string(),
message: "Success".to_string(),
execution_time_ns: 15_000,
}).await;
service.stop().await;
}
#[tokio::test]
async fn test_test_data_provider() {
let provider = TestDataProvider::new().await.unwrap();
let aapl_data = provider.get_historical_data("AAPL").unwrap();
assert_eq!(aapl_data.len(), 1000);
assert!(aapl_data[0].price > 0.0);
}
#[tokio::test]
async fn test_ml_infrastructure() {
let ml_infra = MLTestInfrastructure::new().await.unwrap();
let models = ml_infra.list_models();
assert!(models.contains(&"TLOB"));
assert!(models.contains(&"MAMBA"));
let tlob = ml_infra.get_model("TLOB").unwrap();
assert_eq!(tlob.name, "TLOB");
assert!(tlob.inference_latency_ns > 0);
}
}