#![cfg(feature = "sqlite")]
use dbnexus::{DbConfig, DbPool, DbPoolBuilder};
#[path = "../../common/mod.rs"]
mod common;
#[tokio::test]
async fn test_db_pool_new_sqlite_memory() {
let pool = DbPool::new("sqlite::memory:").await;
assert!(pool.is_ok(), "DbPool::new should succeed for sqlite::memory:");
let pool = pool.unwrap();
let status = pool.status();
assert!(
status.total >= 1,
"pool should have at least 1 connection after warmup, got {}",
status.total
);
}
#[tokio::test]
async fn test_db_pool_with_config() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
max_connections: 10,
min_connections: 1,
..Default::default()
};
let pool = DbPool::with_config(config).await;
assert!(pool.is_ok(), "with_config should succeed");
}
#[tokio::test]
async fn test_db_pool_try_from_config() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
..Default::default()
};
let pool = DbPool::try_from_config(config).await;
assert!(pool.is_ok(), "try_from_config should succeed");
}
#[tokio::test]
async fn test_db_pool_new_invalid_url_fails() {
let result = DbPool::new("invalid://nonexistent").await;
assert!(result.is_err(), "invalid URL should fail");
}
#[tokio::test]
async fn test_db_pool_builder_with_url() {
let pool = DbPoolBuilder::new().url("sqlite::memory:").build().await;
assert!(pool.is_ok(), "builder with url should succeed");
}
#[tokio::test]
async fn test_db_pool_builder_with_config() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
max_connections: 5,
..Default::default()
};
let pool = DbPoolBuilder::new().config(config).build().await;
assert!(pool.is_ok(), "builder with config should succeed");
let pool = pool.unwrap();
assert_eq!(pool.config().max_connections, 5);
}
#[tokio::test]
async fn test_db_pool_builder_max_min_connections() {
let pool = DbPoolBuilder::new()
.url("sqlite::memory:")
.max_connections(15)
.min_connections(2)
.build()
.await;
assert!(pool.is_ok());
let pool = pool.unwrap();
assert_eq!(pool.config().max_connections, 15);
assert_eq!(pool.config().min_connections, 2);
}
#[tokio::test]
async fn test_db_pool_builder_admin_role() {
let pool = DbPoolBuilder::new()
.url("sqlite::memory:")
.admin_role("root")
.build()
.await;
assert!(pool.is_ok());
let pool = pool.unwrap();
assert_eq!(pool.config().admin_role, "root");
}
#[tokio::test]
async fn test_db_pool_status_initial() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
min_connections: 0,
max_connections: 20,
..Default::default()
};
let pool = DbPool::with_config(config).await.unwrap();
let status = pool.status();
assert_eq!(status.total, 0);
assert_eq!(status.active, 0);
assert_eq!(status.idle, 0);
}
#[tokio::test]
async fn test_db_pool_status_invariant_total_equals_active_plus_idle() {
let pool = common::make_sqlite_memory_pool().await;
let status = pool.status();
assert_eq!(
status.total,
status.active + status.idle,
"invariant: total == active + idle"
);
}
#[tokio::test]
async fn test_db_pool_config_returns_built_config() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
max_connections: 7,
admin_role: "sa".to_string(),
..Default::default()
};
let pool = DbPool::with_config(config).await.unwrap();
assert_eq!(pool.config().max_connections, 7);
assert_eq!(pool.config().admin_role, "sa");
assert_eq!(pool.config().url, "sqlite::memory:");
}
#[tokio::test]
async fn test_db_pool_get_actual_config() {
let pool = common::make_sqlite_memory_pool().await;
let cfg1 = pool.config();
let cfg2 = pool.get_actual_config();
assert_eq!(cfg1.url, cfg2.url);
assert_eq!(cfg1.max_connections, cfg2.max_connections);
}
#[tokio::test]
async fn test_db_pool_get_session_admin() {
let pool = common::make_sqlite_memory_pool().await;
let session = pool.get_session("admin").await;
assert!(session.is_ok(), "admin role should always be allowed");
let session = session.unwrap();
assert_eq!(session.role(), "admin");
}
#[tokio::test]
async fn test_db_pool_get_session_system() {
let pool = common::make_sqlite_memory_pool().await;
let session = pool.get_session("system").await;
assert!(
session.is_ok(),
"system role should be allowed without permission config"
);
}
#[cfg(feature = "permission")]
#[tokio::test]
async fn test_db_pool_get_session_unauthorized_role_fails() {
let pool = common::make_sqlite_memory_pool().await;
let result = pool.get_session("guest").await;
assert!(
result.is_err(),
"non-safe role should be rejected without permission config"
);
match result {
Err(dbnexus::DbError::Permission(_)) => { }
Err(other) => panic!("expected Permission error, got {:?}", other),
Ok(_) => panic!("expected error, got Ok"),
}
}
#[tokio::test]
async fn test_db_pool_get_session_role_matches() {
let pool = common::make_sqlite_memory_pool().await;
let session = pool.get_session("admin").await.unwrap();
assert_eq!(session.role(), "admin");
drop(session);
}
#[tokio::test]
async fn test_db_pool_clone_usable() {
let pool = common::make_sqlite_memory_pool().await;
let cloned = pool.clone();
let session = cloned.get_session("admin").await;
assert!(session.is_ok(), "cloned pool should be usable");
}
#[tokio::test]
async fn test_db_pool_clone_config_equal() {
let pool = common::make_sqlite_memory_pool().await;
let cloned = pool.clone();
assert_eq!(pool.config().url, cloned.config().url);
assert_eq!(pool.config().max_connections, cloned.config().max_connections);
}
#[tokio::test]
async fn test_db_pool_max_connections_one() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
max_connections: 1,
min_connections: 0,
..Default::default()
};
let pool = DbPool::with_config(config).await.unwrap();
let _session = pool.get_session("admin").await.expect("first session should succeed");
}
#[tokio::test]
async fn test_db_pool_serial_get_session_reuses_connection() {
let pool = common::make_sqlite_memory_pool().await;
for i in 0..5 {
let session = pool.get_session("admin").await;
assert!(session.is_ok(), "iteration {} should succeed", i);
drop(session);
}
}
#[tokio::test]
async fn test_db_pool_concurrent_get_session_preserves_invariants() {
use std::sync::Arc;
let pool = Arc::new(common::make_sqlite_memory_pool().await);
let max = pool.config().max_connections;
let mut handles = Vec::new();
for _ in 0..max {
let pool_clone = Arc::clone(&pool);
handles.push(tokio::spawn(async move {
let _session = pool_clone.get_session("admin").await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}));
}
for handle in handles {
handle.await.unwrap();
}
let status = pool.status();
assert!(status.total >= status.active, "invariant violated: total >= active");
}
#[tokio::test]
async fn test_db_pool_drop_no_panic() {
let pool = common::make_sqlite_memory_pool().await;
drop(pool);
}
#[tokio::test]
async fn test_db_pool_drop_with_active_session_no_panic() {
let pool = common::make_sqlite_memory_pool().await;
let _session = pool.get_session("admin").await.unwrap();
drop(pool);
drop(_session);
}
#[cfg(feature = "permission")]
#[test]
fn test_db_pool_try_from_with_permission_returns_err() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
max_connections: 5,
..Default::default()
};
let result = DbPool::try_from(&config);
assert!(
result.is_err(),
"try_from should return Err when permission feature is enabled (sync constructor cannot init cache)"
);
}
#[tokio::test]
async fn test_db_pool_builder_no_url_no_config_fails() {
let result = DbPoolBuilder::new().build().await;
assert!(result.is_err(), "build() without url or config should fail");
}
#[tokio::test]
async fn test_db_pool_builder_admin_role_with_config() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
admin_role: "initial".to_string(),
..Default::default()
};
let pool = DbPoolBuilder::new()
.config(config)
.admin_role("root")
.build()
.await
.unwrap();
assert_eq!(pool.config().admin_role, "root");
}
#[tokio::test]
async fn test_db_pool_builder_max_connections_with_url_only() {
let pool = DbPoolBuilder::new()
.url("sqlite::memory:")
.max_connections(13)
.build()
.await
.unwrap();
assert_eq!(pool.config().max_connections, 13);
}
#[tokio::test]
async fn test_db_pool_builder_min_connections_with_url_only() {
let pool = DbPoolBuilder::new()
.url("sqlite::memory:")
.min_connections(2)
.build()
.await
.unwrap();
assert_eq!(pool.config().min_connections, 2);
}
#[allow(deprecated)]
#[tokio::test]
async fn test_db_pool_builder_deprecated_setters_are_noop() {
#[cfg(feature = "metrics")]
{
use dbnexus::MetricsCollector;
let collector = std::sync::Arc::new(MetricsCollector::new());
let _ = DbPoolBuilder::new()
.url("sqlite::memory:")
.metrics_collector(collector)
.build()
.await;
}
#[cfg(feature = "permission")]
{
use dbnexus::access::permission::PermissionConfig;
let _ = DbPoolBuilder::new()
.url("sqlite::memory:")
.permission_config(PermissionConfig::default())
.build()
.await;
}
#[cfg(feature = "cache")]
{
use oxcache::Cache;
let cache: std::sync::Arc<Cache<String, serde_json::Value>> =
std::sync::Arc::new(Cache::builder().capacity(10).build().await.unwrap());
let _ = DbPoolBuilder::new()
.url("sqlite::memory:")
.with_oxcache(cache)
.build()
.await;
}
}
#[cfg(feature = "pool-health-check")]
#[test]
fn test_parse_health_check_interval_boundaries() {
use dbnexus::DbPool;
assert_eq!(DbPool::parse_health_check_interval(""), 30);
assert_eq!(DbPool::parse_health_check_interval("60"), 60);
assert_eq!(DbPool::parse_health_check_interval("5"), 5);
assert_eq!(DbPool::parse_health_check_interval("300"), 300);
assert_eq!(DbPool::parse_health_check_interval("1"), 5);
assert_eq!(DbPool::parse_health_check_interval("4"), 5);
assert_eq!(DbPool::parse_health_check_interval("301"), 300);
assert_eq!(DbPool::parse_health_check_interval("1000"), 300);
assert_eq!(DbPool::parse_health_check_interval("abc"), 30);
assert_eq!(DbPool::parse_health_check_interval("12.5"), 30);
assert_eq!(DbPool::parse_health_check_interval("-5"), 30);
}
#[tokio::test]
async fn test_check_connection_health_healthy_seaorm() {
use dbnexus::DbConnection;
let pool = common::make_sqlite_memory_pool().await;
let sea_conn = sea_orm::Database::connect("sqlite::memory:").await.unwrap();
let conn = DbConnection::SeaOrm(sea_conn);
let is_healthy = pool.check_connection_health(&conn).await;
assert!(is_healthy, "healthy SeaORM connection should return true");
}
#[cfg(feature = "pool-health-check")]
#[tokio::test]
async fn test_clean_invalid_connections_healthy_pool() {
let pool = common::make_sqlite_memory_pool().await;
let removed = pool.clean_invalid_connections().await;
assert_eq!(removed, 0, "no invalid connections should be removed from healthy pool");
}
#[cfg(feature = "pool-health-check")]
#[tokio::test]
async fn test_validate_and_recreate_connections_healthy_pool() {
let pool = common::make_sqlite_memory_pool().await;
let recreated = pool.validate_and_recreate_connections().await;
assert!(recreated.is_ok(), "should succeed on healthy pool");
assert_eq!(
recreated.unwrap(),
0,
"no connections should be recreated on healthy pool"
);
}
#[cfg(feature = "metrics")]
#[tokio::test]
async fn test_pool_metrics_no_collector_returns_zeros() {
let pool = common::make_sqlite_memory_pool().await;
let metrics = pool.pool_metrics();
assert_eq!(metrics.slow_acquires, 0);
assert_eq!(metrics.timeout_errors, 0);
assert_eq!(metrics.critical_timeouts, 0);
}
#[cfg(feature = "auto-migrate")]
#[tokio::test]
async fn test_run_auto_migrate_no_dir_returns_zero() {
let config = DbConfig {
url: "sqlite::memory:".to_string(),
auto_migrate: true,
migrations_dir: None,
..Default::default()
};
let pool = DbPool::with_config(config).await.unwrap();
let result = pool.run_auto_migrate().await;
assert!(result.is_ok(), "should succeed when no migrations_dir set");
assert_eq!(result.unwrap(), 0, "should apply 0 migrations when no dir");
}
#[tokio::test]
async fn test_db_connection_as_sea_orm_ok() {
use dbnexus::DbConnection;
let sea_conn = sea_orm::Database::connect("sqlite::memory:").await.unwrap();
let conn = DbConnection::SeaOrm(sea_conn);
assert!(
conn.as_sea_orm().is_ok(),
"as_sea_orm on SeaOrm variant should return Ok"
);
}
#[tokio::test]
async fn test_db_connection_is_duckdb_seaorm() {
use dbnexus::DbConnection;
let sea_conn = sea_orm::Database::connect("sqlite::memory:").await.unwrap();
let conn = DbConnection::SeaOrm(sea_conn);
assert!(!conn.is_duckdb(), "SeaOrm connection should not be duckdb");
}
#[tokio::test]
async fn test_db_connection_debug_format() {
use dbnexus::DbConnection;
let sea_conn = sea_orm::Database::connect("sqlite::memory:").await.unwrap();
let conn = DbConnection::SeaOrm(sea_conn);
let debug_str = format!("{:?}", conn);
assert!(
debug_str.contains("SeaOrm"),
"debug output should contain SeaOrm: {}",
debug_str
);
}