//! Database test fixtures //! //! Provides shared database setup/teardown utilities used across 39+ test files. use anyhow::Result; use once_cell::sync::OnceCell; use sqlx::{postgres::PgPoolOptions, PgPool}; use std::sync::Arc; use tokio::sync::Mutex; static DB_POOL: OnceCell>> = OnceCell::new(); /// Test database wrapper with automatic setup and cleanup pub struct TestDb { pool: PgPool, schema_name: String, } impl TestDb { /// Create a new test database instance /// /// Uses the DATABASE_URL environment variable or falls back to default. /// Creates an isolated schema for test isolation. pub async fn new() -> Self { let pool = get_test_pool().await; let schema_name = format!("test_{}", uuid::Uuid::new_v4().to_string().replace('-', "_")); // Create isolated schema sqlx::query(&format!("CREATE SCHEMA {}", schema_name)) .execute(&pool) .await .expect("Failed to create test schema"); Self { pool, schema_name, } } /// Create a new test database with migrations applied pub async fn with_migrations() -> Self { let db = Self::new().await; // Set search path to test schema sqlx::query(&format!("SET search_path TO {}", db.schema_name)) .execute(&db.pool) .await .expect("Failed to set search path"); // Run migrations sqlx::migrate!("../../migrations") .run(&db.pool) .await .expect("Failed to run migrations"); db } /// Get a reference to the connection pool pub fn pool(&self) -> &PgPool { &self.pool } /// Execute a query in the test schema pub async fn execute(&self, sql: &str) -> Result { sqlx::query(&format!("SET search_path TO {}; {}", self.schema_name, sql)) .execute(&self.pool) .await .map_err(Into::into) } /// Begin a transaction for test isolation pub async fn begin(&self) -> Result> { self.pool.begin().await.map_err(Into::into) } } impl Drop for TestDb { fn drop(&mut self) { // Clean up test schema let pool = self.pool.clone(); let schema_name = self.schema_name.clone(); tokio::spawn(async move { let _ = sqlx::query(&format!("DROP SCHEMA IF EXISTS {} CASCADE", schema_name)) .execute(&pool) .await; }); } } /// Get or create the shared test database pool /// /// Pattern found in 39 test files: /// - services/ml_training_service/tests/checkpoint_manager_tests.rs /// - services/ml_training_service/tests/job_tracker_test.rs /// - database/src/test_helpers.rs /// - And 36+ more async fn get_test_pool() -> PgPool { let pool = DB_POOL.get_or_init(|| { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); Arc::new(Mutex::new( tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(async { PgPoolOptions::new() .max_connections(5) .connect(&database_url) .await .expect("Failed to create test pool") }) }) )) }); pool.lock().await.clone() } /// Setup test data in the database /// /// Pattern found in data_loader_integration.rs pub async fn setup_test_data(pool: &PgPool, symbol_prefix: &str) -> Result<()> { // Clean existing test data sqlx::query(&format!( "DELETE FROM market_events WHERE symbol LIKE '{}%'", symbol_prefix )) .execute(pool) .await?; sqlx::query(&format!( "DELETE FROM trade_executions WHERE symbol LIKE '{}%'", symbol_prefix )) .execute(pool) .await?; Ok(()) } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_db_creation() { let db = TestDb::new().await; assert!(db.pool().is_closed() == false); } #[tokio::test] async fn test_db_with_migrations() { let db = TestDb::with_migrations().await; let result = db.execute("SELECT 1").await; assert!(result.is_ok()); } #[tokio::test] async fn test_schema_isolation() { let db1 = TestDb::new().await; let db2 = TestDb::new().await; // Different schemas should be isolated assert_ne!(db1.schema_name, db2.schema_name); } }