//! Integration tests for the database package //! //! These tests require a running PostgreSQL database. //! Use docker-compose to start the test database: //! ```bash //! docker-compose up -d postgres //! ``` use config::database::{DatabaseConfig, PoolConfig, TransactionConfig}; use database::{Database, DatabaseError, DatabasePool, OrderDirection, QueryBuilder}; use sqlx::FromRow; use std::time::Duration; /// Helper to get test database configuration fn test_db_config() -> DatabaseConfig { let mut config = DatabaseConfig::new(); // Use a known good connection string let db_url = "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string(); config.url = db_url.clone(); config.pool.database_url = db_url; config.application_name = Some("database_integration_tests".to_string()); config.enable_query_logging = true; config } /// Test row structure for fetch operations #[derive(Debug, Clone, FromRow)] struct TestRow { id: i32, name: String, } mod database_basic_operations { use super::*; #[tokio::test] async fn test_database_new() { let config = test_db_config(); let db = Database::new(config).await; assert!(db.is_ok(), "Database creation should succeed"); } #[tokio::test] async fn test_database_from_env() { std::env::set_var( "DATABASE_URL", "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt", ); let db = Database::from_env().await; assert!(db.is_ok(), "Database creation from env should succeed"); } #[tokio::test] async fn test_database_ping() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); let result = db.ping().await; assert!(result.is_ok(), "Database ping should succeed"); } #[tokio::test] async fn test_database_health_check() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); let result = db.health_check().await; assert!(result.is_ok(), "Health check should succeed"); assert!(result.unwrap(), "Database should be healthy"); } #[tokio::test] async fn test_database_health_info() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); let health_info = db.health_info().await; assert!(health_info.is_ok(), "Health info should be available"); let info = health_info.unwrap(); assert!(info.healthy, "Database should be healthy"); assert!(!info.version.is_empty(), "Version should be populated"); assert!( info.database_size >= 0, "Database size should be non-negative" ); } #[tokio::test] async fn test_database_version() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); let version = db.version().await; assert!(version.is_ok(), "Version query should succeed"); assert!( version.unwrap().contains("PostgreSQL"), "Version should contain PostgreSQL" ); } #[tokio::test] async fn test_database_size() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); let size = db.database_size().await; assert!(size.is_ok(), "Database size query should succeed"); assert!(size.unwrap() > 0, "Database size should be positive"); } #[tokio::test] async fn test_database_table_exists() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Test with a table that likely exists let exists = db.table_exists("migrations").await; assert!(exists.is_ok(), "Table exists check should succeed"); // Test with a table that doesn't exist let not_exists = db.table_exists("nonexistent_table_xyz").await; assert!(not_exists.is_ok(), "Table exists check should succeed"); assert!( !not_exists.unwrap(), "Nonexistent table should return false" ); } } mod database_query_operations { use super::*; #[tokio::test] async fn test_database_execute() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Create a temp table let result = db .execute("CREATE TEMP TABLE test_execute (id SERIAL PRIMARY KEY, name TEXT)") .await; assert!(result.is_ok(), "Execute should succeed"); } #[tokio::test] async fn test_database_execute_with_param() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Create temp table db.execute("CREATE TEMP TABLE test_param (id SERIAL PRIMARY KEY, name TEXT)") .await .expect("Table creation failed"); // Insert with parameter let result = db .execute_with_param("INSERT INTO test_param (name) VALUES ($1)", "test_name") .await; assert!(result.is_ok(), "Execute with param should succeed"); assert_eq!(result.unwrap(), 1, "Should insert 1 row"); } #[tokio::test] async fn test_database_execute_raw() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Create temp table db.execute("CREATE TEMP TABLE test_raw (id SERIAL PRIMARY KEY, value INT)") .await .expect("Table creation failed"); // Insert with raw SQL let result = db .execute_raw("INSERT INTO test_raw (value) VALUES ($1)", 42) .await; assert!(result.is_ok(), "Execute raw should succeed"); assert_eq!(result.unwrap(), 1, "Should insert 1 row"); } #[tokio::test] async fn test_database_query_builder_integration() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Test query builder factory methods let select_builder = Database::select(&["*"]); assert!(format!("{:?}", select_builder).contains("SelectBuilder")); let insert_builder = Database::insert("test_table"); assert!(format!("{:?}", insert_builder).contains("InsertBuilder")); let update_builder = Database::update("test_table"); assert!(format!("{:?}", update_builder).contains("UpdateBuilder")); let delete_builder = Database::delete("test_table"); assert!(format!("{:?}", delete_builder).contains("DeleteBuilder")); } } mod database_pool_tests { use super::*; #[tokio::test] async fn test_pool_acquire() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); let conn = db.acquire().await; assert!(conn.is_ok(), "Pool acquire should succeed"); } #[tokio::test] async fn test_pool_statistics() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); let stats = db.pool_stats().await; assert!( stats.active_connections >= 0, "Active connections should be valid" ); assert!( stats.idle_connections >= 0, "Idle connections should be valid" ); } #[tokio::test] async fn test_pool_size_metrics() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); let size = db.pool_size(); assert!(size > 0, "Pool size should be positive"); let idle = db.idle_connections(); assert!(idle >= 0, "Idle connections should be non-negative"); } #[tokio::test] async fn test_pool_close() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); assert!(!db.is_closed(), "Pool should not be closed initially"); db.close().await; assert!(db.is_closed(), "Pool should be closed after close()"); } #[tokio::test] async fn test_pool_reset_stats() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Generate some activity let _ = db.ping().await; db.reset_stats().await; let stats = db.pool_stats().await; // Note: total_acquisitions might not be 0 due to health checks assert!( stats.failed_acquisitions >= 0, "Failed acquisitions should be reset" ); } } mod database_transaction_tests { use super::*; #[tokio::test] async fn test_begin_transaction() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); let tx = db.begin_transaction().await; assert!(tx.is_ok(), "Begin transaction should succeed"); let tx = tx.unwrap(); assert!( tx.elapsed().as_secs() < 1, "Transaction should start immediately" ); } #[tokio::test] async fn test_begin_transaction_with_timeout() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); let tx = db .begin_transaction_with_timeout(Duration::from_secs(60)) .await; assert!(tx.is_ok(), "Begin transaction with timeout should succeed"); } #[tokio::test] async fn test_transaction_commit() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Create temp table db.execute("CREATE TEMP TABLE test_tx_commit (id SERIAL PRIMARY KEY, value INT)") .await .expect("Table creation failed"); let mut tx = db .begin_transaction() .await .expect("Begin transaction failed"); // Execute within transaction let result = tx .execute("INSERT INTO test_tx_commit (value) VALUES (100)") .await; assert!(result.is_ok(), "Transaction execute should succeed"); // Commit let commit_result = tx.commit().await; assert!(commit_result.is_ok(), "Transaction commit should succeed"); } #[tokio::test] async fn test_transaction_rollback() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Create temp table db.execute("CREATE TEMP TABLE test_tx_rollback (id SERIAL PRIMARY KEY, value INT)") .await .expect("Table creation failed"); let mut tx = db .begin_transaction() .await .expect("Begin transaction failed"); // Execute within transaction let result = tx .execute("INSERT INTO test_tx_rollback (value) VALUES (200)") .await; assert!(result.is_ok(), "Transaction execute should succeed"); // Rollback let rollback_result = tx.rollback().await; assert!( rollback_result.is_ok(), "Transaction rollback should succeed" ); } #[tokio::test] async fn test_with_transaction() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Create temp table db.execute("CREATE TEMP TABLE test_with_tx (id SERIAL PRIMARY KEY, value INT)") .await .expect("Table creation failed"); let result = db .with_transaction(|mut tx| async move { tx.execute("INSERT INTO test_with_tx (value) VALUES (300)") .await?; Ok((42, tx)) }) .await; assert!(result.is_ok(), "with_transaction should succeed"); assert_eq!(result.unwrap(), 42, "Should return the closure result"); } #[tokio::test] async fn test_with_transaction_timeout() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); let result = db .with_transaction_timeout( |mut tx| async move { tx.execute("SELECT 1").await?; Ok(("success", tx)) }, Duration::from_secs(10), ) .await; assert!(result.is_ok(), "with_transaction_timeout should succeed"); assert_eq!(result.unwrap(), "success"); } #[tokio::test] async fn test_transaction_statistics() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Perform some transactions let _ = db.with_transaction(|tx| async move { Ok(((), tx)) }).await; let stats = db.transaction_stats(); assert!( stats.total_transactions > 0, "Should have transaction count" ); } #[tokio::test] async fn test_transaction_savepoint() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Create temp table db.execute("CREATE TEMP TABLE test_savepoint (id SERIAL PRIMARY KEY, value INT)") .await .expect("Table creation failed"); let mut tx = db .begin_transaction() .await .expect("Begin transaction failed"); // Create savepoint let sp_result = tx.savepoint("sp1").await; assert!(sp_result.is_ok(), "Savepoint creation should succeed"); // Execute after savepoint tx.execute("INSERT INTO test_savepoint (value) VALUES (400)") .await .unwrap(); // Rollback to savepoint let rollback_result = tx.rollback_to_savepoint("sp1").await; assert!( rollback_result.is_ok(), "Rollback to savepoint should succeed" ); tx.commit().await.unwrap(); } #[tokio::test] async fn test_transaction_release_savepoint() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); let mut tx = db .begin_transaction() .await .expect("Begin transaction failed"); // Create savepoint tx.savepoint("sp2") .await .expect("Savepoint creation failed"); // Release savepoint let release_result = tx.release_savepoint("sp2").await; assert!(release_result.is_ok(), "Release savepoint should succeed"); tx.commit().await.unwrap(); } } mod database_config_tests { use super::*; #[test] fn test_database_config_getter() { let config = test_db_config(); let runtime = tokio::runtime::Runtime::new().unwrap(); runtime.block_on(async { let db = Database::new(config.clone()) .await .expect("Database creation failed"); let retrieved_config = db.config(); assert_eq!(retrieved_config.application_name, config.application_name); assert_eq!( retrieved_config.enable_query_logging, config.enable_query_logging ); }); } #[tokio::test] async fn test_invalid_database_url() { let mut config = DatabaseConfig::new(); config.url = "invalid://not-a-database".to_string(); let result = Database::new(config).await; assert!(result.is_err(), "Invalid URL should fail"); if let Err(e) = result { assert!(matches!(e, DatabaseError::Configuration { .. })); } } #[tokio::test] async fn test_database_config_validation() { let mut config = DatabaseConfig::new(); config.url = String::new(); // Invalid empty URL let result = Database::new(config).await; assert!(result.is_err(), "Empty URL should fail validation"); } } mod error_handling_tests { use super::*; #[tokio::test] async fn test_query_error() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Execute invalid SQL let result = db.execute("INVALID SQL STATEMENT").await; assert!(result.is_err(), "Invalid SQL should fail"); if let Err(e) = result { assert!(matches!(e, DatabaseError::Query { .. })); } } #[tokio::test] async fn test_table_not_found_error() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Query non-existent table let result = db.execute("SELECT * FROM nonexistent_table_xyz").await; assert!(result.is_err(), "Querying non-existent table should fail"); } #[tokio::test] async fn test_transaction_timeout() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Begin transaction with very short timeout let tx = db .begin_transaction_with_timeout(Duration::from_nanos(1)) .await; if let Ok(tx) = tx { // Should timeout immediately assert!(tx.is_timed_out(), "Transaction should be timed out"); // Attempting to commit should fail with timeout error let commit_result = tx.commit().await; assert!(commit_result.is_err(), "Commit should fail due to timeout"); } } #[tokio::test] async fn test_constraint_violation_handling() { let config = test_db_config(); let db = Database::new(config) .await .expect("Database creation failed"); // Create temp table with unique constraint db.execute("CREATE TEMP TABLE test_constraint (id SERIAL PRIMARY KEY, email TEXT UNIQUE)") .await .expect("Table creation failed"); // Insert first row db.execute("INSERT INTO test_constraint (email) VALUES ('test@example.com')") .await .expect("First insert should succeed"); // Try to insert duplicate let result = db .execute("INSERT INTO test_constraint (email) VALUES ('test@example.com')") .await; assert!(result.is_err(), "Duplicate insert should fail"); if let Err(e) = result { // Should be constraint violation error assert!( matches!(e, DatabaseError::ConstraintViolation { .. }) || matches!(e, DatabaseError::Query { .. }) ); } } } mod pool_configuration_tests { use super::*; #[tokio::test] async fn test_custom_pool_config() { let mut config = test_db_config(); config.pool.max_connections = 5; config.pool.min_connections = 1; let db = Database::new(config).await; assert!( db.is_ok(), "Database with custom pool config should succeed" ); } #[tokio::test] async fn test_pool_config_validation() { let mut pool_config = PoolConfig::default(); pool_config.min_connections = 10; pool_config.max_connections = 5; // Invalid: min > max let mut config = test_db_config(); config.pool = pool_config; let result = Database::new(config).await; assert!(result.is_err(), "Invalid pool config should fail"); } } mod transaction_configuration_tests { use super::*; #[tokio::test] async fn test_custom_transaction_config() { let mut config = test_db_config(); config.transaction.default_timeout_secs = 60; config.transaction.max_retries = 5; config.transaction.enable_retry = true; let db = Database::new(config).await; assert!( db.is_ok(), "Database with custom transaction config should succeed" ); } #[tokio::test] async fn test_transaction_retry_disabled() { let mut config = test_db_config(); config.transaction.enable_retry = false; let db = Database::new(config) .await .expect("Database creation failed"); // Transaction should not retry on failure let result = db .with_transaction(|mut tx| async move { tx.execute("INVALID SQL").await?; Ok(((), tx)) }) .await; assert!(result.is_err(), "Transaction should fail without retry"); } }