//! Per-test database isolation. //! //! Each test gets its own PostgreSQL database created from scratch with all //! migrations applied. Dropped automatically when `TestDb` goes out of scope. use sqlx::postgres::PgPoolOptions; use sqlx::{Connection, Executor, PgConnection, PgPool}; use std::time::Duration; use uuid::Uuid; pub struct TestDb { pub pool: PgPool, db_name: String, admin_url: String, } impl TestDb { pub async fn new() -> Self { let admin_url = std::env::var("TEST_DATABASE_URL") .unwrap_or_else(|_| "postgres://localhost/postgres".to_string()); let db_name = format!("mt_test_{}", Uuid::new_v4().simple()); let mut admin_conn = PgConnection::connect(&admin_url) .await .expect("Failed to connect to admin database"); admin_conn .execute(format!("CREATE DATABASE \"{}\"", db_name).as_str()) .await .expect("Failed to create test database"); let test_url = Self::replace_db_name(&admin_url, &db_name); let pool = PgPoolOptions::new() .max_connections(5) .acquire_timeout(Duration::from_secs(5)) .connect(&test_url) .await .expect("Failed to connect to test database"); sqlx::migrate!("./migrations") .run(&pool) .await .expect("Failed to run migrations on test database"); TestDb { pool, db_name, admin_url, } } fn replace_db_name(url: &str, new_db: &str) -> String { if let Some(pos) = url.rfind('/') { let base = &url[..pos]; let query = url[pos + 1..] .find('?') .map(|q| &url[pos + 1 + q..]) .unwrap_or(""); if query.is_empty() { format!("{}/{}", base, new_db) } else { format!("{}/{}{}", base, new_db, query) } } else { panic!("Invalid database URL: no '/' found"); } } } impl Drop for TestDb { fn drop(&mut self) { let admin_url = self.admin_url.clone(); let db_name = self.db_name.clone(); self.pool.close_event(); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("Failed to build cleanup runtime"); rt.block_on(async { if let Ok(mut conn) = PgConnection::connect(&admin_url).await { let _ = conn .execute( format!( "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{}'", db_name ) .as_str(), ) .await; let _ = conn .execute(format!("DROP DATABASE IF EXISTS \"{}\"", db_name).as_str()) .await; } }); }) .join() .ok(); } }