#![cfg(feature = "sqlx-postgres")]
use std::fs;
use std::path::PathBuf;
use sunbeam_g2v::config::DatabaseConfig;
use sunbeam_g2v::db::{Database, Migrator, connect, connect_eager};
use sunbeam_g2v::error::ServiceError;
mod support;
async fn db_config() -> DatabaseConfig {
let url = support::containers::postgres_url().await;
DatabaseConfig {
url,
max_connections: 5,
connect_timeout: 5,
}
}
async fn with_isolated_schema<F, Fut>(db: &Database, f: F)
where
F: FnOnce(sqlx::PgPool) -> Fut,
Fut: std::future::Future<Output = ()>,
{
let schema = format!("g2v_test_{}", uuid::Uuid::new_v4().simple());
sqlx::query(sqlx::AssertSqlSafe(format!("CREATE SCHEMA \"{schema}\"")))
.execute(db.pool())
.await
.expect("create test schema");
let url = format!(
"{}?options=-csearch_path%3D{schema}",
support::containers::postgres_url().await
);
let test_pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(3)
.connect(&url)
.await
.expect("connect to test schema pool");
f(test_pool).await;
sqlx::query(sqlx::AssertSqlSafe(format!(
"DROP SCHEMA IF EXISTS \"{schema}\" CASCADE"
)))
.execute(db.pool())
.await
.expect("drop test schema");
}
#[tokio::test]
async fn test_connect_lazy_succeeds_with_real_db() {
let db = connect(&db_config().await).expect("connect_lazy should not fail for valid URL");
db.ping()
.await
.expect("ping should succeed against real Postgres");
}
#[tokio::test]
async fn test_ping_fails_when_db_unreachable() {
let config = DatabaseConfig {
url: "postgres://user:pw@localhost:9/db".to_string(),
max_connections: 2,
connect_timeout: 2,
};
let db = connect(&config).expect("connect_lazy must succeed for valid URL");
let result = db.ping().await;
assert!(result.is_err(), "ping to unreachable host must fail");
match result.err().unwrap() {
ServiceError::Database(_) | ServiceError::DeadlineExceeded(_) => {}
other => panic!("expected Database or DeadlineExceeded, got {other:?}"),
}
}
#[tokio::test]
async fn test_migrator_runs_against_real_db() {
let db = connect_eager(&db_config().await)
.await
.expect("connect_eager should succeed");
let table = format!("tbl_{}", uuid::Uuid::new_v4().simple());
with_isolated_schema(&db, |pool| async move {
let tmpdir = tempdir_with_migration(&table);
let migrations_path = tmpdir.join("migrations");
let migrator = Migrator::new(&migrations_path)
.await
.expect("Migrator::new should succeed for valid directory");
migrator
.run(&pool)
.await
.expect("migration should run without error");
let row: (bool,) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
"SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = '{table}'
)"
)))
.fetch_one(&pool)
.await
.expect("existence query should succeed");
assert!(row.0, "table {table} should exist after migration");
})
.await;
}
#[tokio::test]
async fn test_migrator_idempotent() {
let db = connect_eager(&db_config().await)
.await
.expect("connect_eager should succeed");
let table = format!("tbl_{}", uuid::Uuid::new_v4().simple());
with_isolated_schema(&db, |pool| async move {
let tmpdir = tempdir_with_migration(&table);
let migrations_path = tmpdir.join("migrations");
let migrator = Migrator::new(&migrations_path)
.await
.expect("Migrator::new should succeed");
migrator
.run(&pool)
.await
.expect("first migration run should succeed");
migrator
.run(&pool)
.await
.expect("second migration run should be idempotent");
})
.await;
}
fn tempdir_with_migration(table_name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"sunbeam_g2v_migtest_{}",
uuid::Uuid::new_v4().simple()
));
let migrations = dir.join("migrations");
fs::create_dir_all(&migrations).expect("create temp migrations dir");
let sql_path = migrations.join("20260101000000_test.sql");
fs::write(
&sql_path,
format!(
"CREATE TABLE IF NOT EXISTS {table_name} (\
id SERIAL PRIMARY KEY, \
name TEXT\
);\n"
),
)
.expect("write migration SQL file");
dir
}