diff --git a/crates/common/src/database.rs b/crates/common/src/database.rs index ded6452..1c1ce8c 100644 --- a/crates/common/src/database.rs +++ b/crates/common/src/database.rs @@ -30,7 +30,8 @@ impl Database { Ok(Self { pool }) } - #[must_use] pub const fn pool(&self) -> &PgPool { + #[must_use] + pub const fn pool(&self) -> &PgPool { &self.pool } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 86a1ec6..287ab4f 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -7,9 +7,6 @@ pub mod migrate; pub mod migrate_cli; pub mod models; -#[cfg(test)] -mod tests; - pub use config::*; pub use database::*; pub use error::*; diff --git a/crates/common/tests/database_tests.rs b/crates/common/tests/database_tests.rs new file mode 100644 index 0000000..3faa130 --- /dev/null +++ b/crates/common/tests/database_tests.rs @@ -0,0 +1,179 @@ +//! Database integration tests + +use fc_common::config::DatabaseConfig; +use fc_common::*; +use sqlx::PgPool; + +#[tokio::test] +async fn test_database_connection() -> anyhow::Result<()> { + let config = DatabaseConfig { + url: "postgresql://postgres:password@localhost/test".to_string(), + max_connections: 5, + min_connections: 1, + connect_timeout: 5, // Short timeout for test + idle_timeout: 600, + max_lifetime: 1800, + }; + + // Try to connect, skip test if database is not available + let db = match Database::new(config).await { + Ok(db) => db, + Err(e) => { + println!("Skipping test_database_connection: no PostgreSQL instance available - {}", e); + return Ok(()); + } + }; + + // Test health check + Database::health_check(db.pool()).await?; + + // Test connection info + let info = db.get_connection_info().await?; + assert!(!info.database.is_empty()); + assert!(!info.user.is_empty()); + assert!(!info.version.is_empty()); + + // Test pool stats + let stats = db.get_pool_stats().await; + assert!(stats.size >= 1); + + db.close().await; + + Ok(()) +} + +#[tokio::test] +async fn test_database_health_check() -> anyhow::Result<()> { + // Try to connect, skip test if database is not available + let pool = match PgPool::connect("postgresql://postgres:password@localhost/test").await { + Ok(pool) => pool, + Err(e) => { + println!("Skipping test_database_health_check: no PostgreSQL instance available - {}", e); + return Ok(()); + } + }; + + // Should succeed + Database::health_check(&pool).await?; + + pool.close().await; + Ok(()) +} + +#[tokio::test] +async fn test_connection_info() -> anyhow::Result<()> { + // Try to connect, skip test if database is not available + let pool = match PgPool::connect("postgresql://postgres:password@localhost/test").await { + Ok(pool) => pool, + Err(e) => { + println!("Skipping test_connection_info: no PostgreSQL instance available - {}", e); + return Ok(()); + } + }; + + let db = match Database::new(DatabaseConfig { + url: "postgresql://postgres:password@localhost/test".to_string(), + max_connections: 5, + min_connections: 1, + connect_timeout: 5, // Short timeout for test + idle_timeout: 600, + max_lifetime: 1800, + }) + .await { + Ok(db) => db, + Err(e) => { + println!("Skipping test_connection_info: database connection failed - {}", e); + pool.close().await; + return Ok(()); + } + }; + + let info = db.get_connection_info().await?; + + assert!(!info.database.is_empty()); + assert!(!info.user.is_empty()); + assert!(!info.version.is_empty()); + assert!(info.version.contains("PostgreSQL")); + + db.close().await; + pool.close().await; + + Ok(()) +} + +#[tokio::test] +async fn test_pool_stats() -> anyhow::Result<()> { + let db = match Database::new(DatabaseConfig { + url: "postgresql://postgres:password@localhost/test".to_string(), + max_connections: 5, + min_connections: 1, + connect_timeout: 5, // Short timeout for test + idle_timeout: 600, + max_lifetime: 1800, + }) + .await { + Ok(db) => db, + Err(e) => { + println!("Skipping test_pool_stats: no PostgreSQL instance available - {}", e); + return Ok(()); + } + }; + + let stats = db.get_pool_stats().await; + + assert!(stats.size >= 1); + assert!(stats.idle >= 1); + assert_eq!(stats.size, stats.idle + stats.active); + + db.close().await; + + Ok(()) +} + +#[sqlx::test] +async fn test_database_config_validation() -> anyhow::Result<()> { + // Valid config + let config = DatabaseConfig { + url: "postgresql://user:pass@localhost/db".to_string(), + max_connections: 10, + min_connections: 2, + connect_timeout: 30, + idle_timeout: 600, + max_lifetime: 1800, + }; + assert!(config.validate().is_ok()); + + // Invalid URL + let mut config = config.clone(); + config.url = "invalid://url".to_string(); + assert!(config.validate().is_err()); + + // Empty URL + config.url = "".to_string(); + assert!(config.validate().is_err()); + + // Zero max connections + config = DatabaseConfig { + url: "postgresql://user:pass@localhost/db".to_string(), + max_connections: 0, + min_connections: 1, + connect_timeout: 30, + idle_timeout: 600, + max_lifetime: 1800, + }; + assert!(config.validate().is_err()); + + // Min > max + config = DatabaseConfig { + url: "postgresql://user:pass@localhost/db".to_string(), + max_connections: 5, + min_connections: 10, + connect_timeout: 30, + idle_timeout: 600, + max_lifetime: 1800, + }; + assert!(config.validate().is_err()); + + Ok(()) +} + diff --git a/crates/common/tests/mod.rs b/crates/common/tests/mod.rs new file mode 100644 index 0000000..dd8a689 --- /dev/null +++ b/crates/common/tests/mod.rs @@ -0,0 +1,148 @@ +//! Integration tests for database and configuration + +use fc_common::Database; +use fc_common::config::{Config, DatabaseConfig}; + +#[tokio::test] +async fn test_database_connection_full() -> anyhow::Result<()> { + // This test requires a running PostgreSQL instance + // Skip if no database is available + let config = DatabaseConfig { + url: "postgresql://postgres:password@localhost/fc_ci_test".to_string(), + max_connections: 5, + min_connections: 1, + connect_timeout: 5, // Short timeout for test + idle_timeout: 600, + max_lifetime: 1800, + }; + + // Try to connect, skip test if database is not available + let db = match Database::new(config).await { + Ok(db) => db, + Err(_) => { + println!("Skipping database test: no PostgreSQL instance available"); + return Ok(()); + } + }; + + // Test health check + Database::health_check(db.pool()).await?; + + // Test connection info + let info = db.get_connection_info().await?; + assert!(!info.database.is_empty()); + assert!(!info.user.is_empty()); + assert!(!info.version.is_empty()); + + // Test pool stats + let stats = db.get_pool_stats().await; + assert!(stats.size >= 1); + assert!(stats.idle >= 1); + assert_eq!(stats.size, stats.idle + stats.active); + + db.close().await; + + Ok(()) +} + +#[test] +fn test_config_loading() -> anyhow::Result<()> { + // Test default config loading + let config = Config::load()?; + assert!(config.validate().is_ok()); + + // Test that defaults are reasonable + assert_eq!(config.database.max_connections, 20); + assert_eq!(config.database.min_connections, 5); + assert_eq!(config.server.port, 3000); + assert_eq!(config.evaluator.poll_interval, 60); + assert_eq!(config.queue_runner.workers, 4); + + Ok(()) +} + +#[test] +fn test_config_validation() -> anyhow::Result<()> { + // Test valid config + let config = Config::default(); + assert!(config.validate().is_ok()); + + // Test invalid database URL + let mut config = config.clone(); + config.database.url = "invalid://url".to_string(); + assert!(config.validate().is_err()); + + // Test invalid port + let mut config = config.clone(); + config.server.port = 0; + assert!(config.validate().is_err()); + + // Test invalid connections + let mut config = config.clone(); + config.database.max_connections = 0; + assert!(config.validate().is_err()); + + config.database.max_connections = 10; + config.database.min_connections = 15; + assert!(config.validate().is_err()); + + // Test invalid evaluator settings + let mut config = config.clone(); + config.evaluator.poll_interval = 0; + assert!(config.validate().is_err()); + + // Test invalid queue runner settings + let mut config = config.clone(); + config.queue_runner.workers = 0; + assert!(config.validate().is_err()); + + Ok(()) +} + +#[test] +fn test_database_config_validation() -> anyhow::Result<()> { + // Test valid config + let config = DatabaseConfig::default(); + assert!(config.validate().is_ok()); + + // Test invalid URL + let mut config = config.clone(); + config.url = "invalid://url".to_string(); + assert!(config.validate().is_err()); + + // Test empty URL + config.url = "".to_string(); + assert!(config.validate().is_err()); + + // Test zero max connections + config = DatabaseConfig::default(); + config.max_connections = 0; + assert!(config.validate().is_err()); + + // Test min > max + config = DatabaseConfig::default(); + config.max_connections = 5; + config.min_connections = 10; + assert!(config.validate().is_err()); + + Ok(()) +} + +#[test] +fn test_config_serialization() -> anyhow::Result<()> { + let config = Config::default(); + + // Test TOML serialization + let toml_str = toml::to_string_pretty(&config)?; + let parsed: Config = toml::from_str(&toml_str)?; + assert_eq!(config.database.url, parsed.database.url); + assert_eq!(config.server.port, parsed.server.port); + + // Test JSON serialization + let json_str = serde_json::to_string_pretty(&config)?; + let parsed: Config = serde_json::from_str(&json_str)?; + assert_eq!(config.database.url, parsed.database.url); + assert_eq!(config.server.port, parsed.server.port); + + Ok(()) +}