common: add basic database tests; skip when DB unavailable

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I10be55f54495c07de19ed26a03c9596c6a6a6964
This commit is contained in:
raf 2025-11-02 23:04:20 +03:00
commit cbf16a7e63
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 329 additions and 4 deletions

View file

@ -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
}

View file

@ -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::*;

View file

@ -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(())
}

148
crates/common/tests/mod.rs Normal file
View file

@ -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(())
}