//! Database Testing Harness //! //! Provides database testing utilities including transaction management, //! test data setup, and database health checking for E2E tests. use anyhow::{Context, Result}; use sqlx::postgres::{PgConnection, PgPool, PgPoolOptions}; use std::sync::Arc; use tracing::{debug, info, warn}; /// Database test harness for E2E testing #[derive(Debug)] pub struct DatabaseTestHarness { pool: PgPool, test_database_url: String, } impl DatabaseTestHarness { /// Create a new database test harness pub async fn new() -> Result { info!("๐Ÿ—„๏ธ Initializing database test harness"); let test_database_url = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgresql://localhost/foxhunt_test".to_string()); debug!("Connecting to test database: {}", test_database_url); let pool = PgPoolOptions::new() .max_connections(10) .connect(&test_database_url) .await .context("Failed to connect to test database")?; // Ensure test database schema is up to date sqlx::migrate!("../../migrations") .run(&pool) .await .context("Failed to run database migrations")?; info!("โœ… Database test harness initialized"); Ok(Self { pool, test_database_url, }) } /// Begin a test transaction that will be automatically rolled back pub async fn begin_test_transaction(&self) -> Result { debug!("Starting test transaction"); let mut conn = self .pool .acquire() .await .context("Failed to acquire database connection")?; sqlx::query("BEGIN") .execute(&mut *conn) .await .context("Failed to begin transaction")?; Ok(conn.detach()) } /// Check database health pub async fn check_health(&self) -> Result<()> { debug!("Checking database health"); sqlx::query("SELECT 1") .execute(&self.pool) .await .context("Database health check failed")?; Ok(()) } /// Get database pool for direct access pub fn get_pool(&self) -> &PgPool { &self.pool } /// Setup test configuration data pub async fn setup_test_config(&self) -> Result<()> { info!("๐Ÿ”ง Setting up test configuration data"); let test_configs = vec![ ("risk_limit", "0.02"), ("max_position_size", "100000"), ("trading_enabled", "true"), ("ml_inference_enabled", "true"), ("circuit_breaker_threshold", "0.1"), ("emergency_stop_enabled", "true"), ("max_daily_loss", "50000"), ("position_concentration_limit", "0.2"), ("var_confidence_level", "0.95"), ("sharpe_ratio_target", "1.5"), ]; for (key, value) in test_configs { sqlx::query!( "INSERT INTO configuration (key, value, description, created_at, updated_at) VALUES ($1, $2, $3, NOW(), NOW()) ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()", key, value, format!("Test configuration for {}", key) ) .execute(&self.pool) .await .with_context(|| format!("Failed to set test config: {}", key))?; } info!("โœ… Test configuration data setup complete"); Ok(()) } /// Clean up test data pub async fn cleanup_test_data(&self) -> Result<()> { info!("๐Ÿงน Cleaning up test data"); // Clean up test orders sqlx::query!("DELETE FROM orders WHERE client_order_id LIKE 'TEST_%'") .execute(&self.pool) .await .context("Failed to cleanup test orders")?; // Clean up test configurations sqlx::query!( "DELETE FROM configuration WHERE key LIKE '%_test_%' OR description LIKE 'Test configuration%'" ) .execute(&self.pool) .await .context("Failed to cleanup test configurations")?; // Clean up test events sqlx::query!("DELETE FROM events WHERE event_type = 'test_event'") .execute(&self.pool) .await .context("Failed to cleanup test events")?; info!("โœ… Test data cleanup complete"); Ok(()) } /// Create test market data pub async fn create_test_market_data(&self, symbol: &str, count: i32) -> Result<()> { info!("๐Ÿ“Š Creating test market data for {}", symbol); for i in 0..count { let price = 150.0 + (i as f64 * 0.1); let timestamp = chrono::Utc::now() - chrono::Duration::seconds((count - i) as i64); sqlx::query!( "INSERT INTO market_data (symbol, price, volume, timestamp, exchange) VALUES ($1, $2, $3, $4, 'TEST')", symbol, price, 1000, timestamp ) .execute(&self.pool) .await .with_context(|| format!("Failed to insert test market data for {}", symbol))?; } info!( "โœ… Created {} test market data points for {}", count, symbol ); Ok(()) } /// Verify database schema pub async fn verify_schema(&self) -> Result { info!("๐Ÿ” Verifying database schema"); let mut verification = SchemaVerification { tables_found: Vec::new(), missing_tables: Vec::new(), schema_valid: true, }; let required_tables = vec![ "configuration", "orders", "positions", "market_data", "events", "risk_metrics", ]; for table_name in &required_tables { let exists = sqlx::query!( "SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1 )", table_name ) .fetch_one(&self.pool) .await .context("Failed to check table existence")?; if exists.exists.unwrap_or(false) { verification.tables_found.push(table_name.to_string()); } else { verification.missing_tables.push(table_name.to_string()); verification.schema_valid = false; warn!("Missing required table: {}", table_name); } } if verification.schema_valid { info!("โœ… Database schema verification passed"); } else { warn!( "โš ๏ธ Database schema verification failed: {} missing tables", verification.missing_tables.len() ); } Ok(verification) } /// Get database connection statistics pub async fn get_connection_stats(&self) -> Result { let pool_state = self.pool.size(); Ok(ConnectionStats { total_connections: pool_state, active_connections: pool_state, // Approximate idle_connections: 0, // Not directly available }) } } /// Schema verification result #[derive(Debug)] pub struct SchemaVerification { pub tables_found: Vec, pub missing_tables: Vec, pub schema_valid: bool, } /// Database connection statistics #[derive(Debug)] pub struct ConnectionStats { pub total_connections: u32, pub active_connections: u32, pub idle_connections: u32, } /// Test transaction guard that auto-rolls back pub struct TestTransaction { conn: Option, } impl TestTransaction { pub fn new(conn: PgConnection) -> Self { Self { conn: Some(conn) } } /// Get mutable reference to connection pub fn connection(&mut self) -> &mut PgConnection { self.conn.as_mut().expect("Connection already consumed") } /// Commit the transaction (consumes the guard) pub async fn commit(mut self) -> Result<()> { if let Some(mut conn) = self.conn.take() { sqlx::query("COMMIT") .execute(&mut conn) .await .context("Failed to commit test transaction")?; } Ok(()) } } impl Drop for TestTransaction { fn drop(&mut self) { if let Some(mut conn) = self.conn.take() { // Rollback transaction on drop let _ = futures::executor::block_on(async { sqlx::query("ROLLBACK").execute(&mut conn).await }); } } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_connection_stats() { let stats = ConnectionStats { total_connections: 10, active_connections: 5, idle_connections: 5, }; assert_eq!(stats.total_connections, 10); assert_eq!(stats.active_connections, 5); assert_eq!(stats.idle_connections, 5); } #[test] fn test_schema_verification() { let verification = SchemaVerification { tables_found: vec!["configuration".to_string(), "orders".to_string()], missing_tables: vec!["positions".to_string()], schema_valid: false, }; assert_eq!(verification.tables_found.len(), 2); assert_eq!(verification.missing_tables.len(), 1); assert!(!verification.schema_valid); } }