Files
foxhunt/testing/test-common/src/fixtures/database.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

169 lines
4.6 KiB
Rust

//! 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<Arc<Mutex<PgPool>>> = 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::postgres::PgQueryResult> {
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<sqlx::Transaction<'_, sqlx::Postgres>> {
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);
}
}