fix: postgres and redis tests

This commit is contained in:
charlesgauthereau
2026-03-14 17:59:44 +01:00
parent d1d633fd00
commit 1fdea31430
17 changed files with 323 additions and 84 deletions
Generated
+11 -10
View File
@@ -1450,7 +1450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -2089,7 +2089,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.3",
"socket2 0.5.10",
"system-configuration",
"tokio",
"tower-service",
@@ -2680,7 +2680,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -3088,6 +3088,7 @@ dependencies = [
"tracing-appender",
"tracing-subscriber",
"typenum",
"url",
"uuid",
"wiremock",
]
@@ -3255,7 +3256,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls 0.23.37",
"socket2 0.6.3",
"socket2 0.5.10",
"thiserror 2.0.18",
"tokio",
"tracing",
@@ -3293,7 +3294,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.3",
"socket2 0.5.10",
"tracing",
"windows-sys 0.60.2",
]
@@ -3646,7 +3647,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -3717,7 +3718,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -4093,7 +4094,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -4255,7 +4256,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -5170,7 +5171,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.48.0",
]
[[package]]
+3 -1
View File
@@ -49,13 +49,15 @@ tokio-stream = "0.1.18"
aes = "0.9.0-rc.4"
typenum = "1.19.0"
testcontainers = "0.27.1"
testcontainers-modules = { version = "0.15.0", features = ["postgres"] }
testcontainers-modules = { version = "0.15.0", features = ["postgres", "redis"] }
postgres = "0.19.12"
url = "2.5.8"
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
mockall = "0.13"
testcontainers = "0.27.1"
testcontainers-modules = { version = "0.15.0", features = ["postgres", "redis"] }
wiremock = "0.6"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -e
echo "Detecting OS..."
OS_TYPE="$(uname -s)"
if [[ "$OS_TYPE" == "Linux" ]]; then
if command -v apt >/dev/null 2>&1; then
echo "Linux detected with apt. Installing tools..."
sudo apt update
echo "Installing redis-tools"
sudo apt install -y redis-tools
echo "Installing postgresql-client"
sudo apt install postgresql-client
else
echo "Unsupported Linux distribution. Only apt-based distros are supported."
exit 1
fi
elif [[ "$OS_TYPE" == "Darwin" ]]; then
if command -v brew >/dev/null 2>&1; then
echo "macOS detected. Installing redis via Homebrew..."
echo "Installing redis-tools"
brew install redis
echo "Installing postgresql-client"
brew install postgresql
else
echo "Homebrew not found. Please install Homebrew first: https://brew.sh/"
exit 1
fi
else
echo "Unsupported OS: $OS_TYPE"
exit 1
fi
echo "test tools installation completed."
+4 -4
View File
@@ -2,19 +2,19 @@ use crate::domain::mongodb::database::MongoDatabase;
use crate::domain::mysql::database::MySQLDatabase;
use crate::domain::postgres::database::PostgresDatabase;
use crate::domain::postgres::{detect_format_from_file, detect_format_from_size};
use crate::domain::redis::database::RedisDatabase;
use crate::domain::sqlite::database::SqliteDatabase;
use crate::services::config::{DatabaseConfig, DbType};
use anyhow::Result;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::domain::redis::database::RedisDatabase;
use crate::domain::sqlite::database::SqliteDatabase;
#[async_trait::async_trait]
pub trait Database: Send + Sync {
fn file_extension(&self) -> &'static str;
async fn ping(&self) -> Result<bool>;
async fn backup(&self, backup_dir: &Path) -> Result<PathBuf>;
async fn restore(&self, restore_file: &Path) -> Result<()>;
async fn backup(&self, backup_dir: &Path, is_test: Option<bool>) -> Result<PathBuf>;
async fn restore(&self, restore_file: &Path, is_test: Option<bool>) -> Result<()>;
}
pub struct DatabaseFactory;
+17 -12
View File
@@ -27,22 +27,27 @@ impl Database for MongoDatabase {
ping::run(self.cfg.clone()).await
}
async fn backup(&self, dir: &Path) -> Result<PathBuf> {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
let res = backup::run(
self.cfg.clone(),
dir.to_path_buf(),
self.file_extension(),
)
.await;
FileLock::release(&self.cfg.generated_id).await?;
async fn backup(&self, dir: &Path, is_test: Option<bool>) -> Result<PathBuf> {
let test_mode = is_test.unwrap_or(false);
if !test_mode {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
}
let res = backup::run(self.cfg.clone(), dir.to_path_buf(), self.file_extension()).await;
if !test_mode {
FileLock::release(&self.cfg.generated_id).await?;
}
res
}
async fn restore(&self, file: &Path) -> Result<()> {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Restore.as_str()).await?;
async fn restore(&self, file: &Path, is_test: Option<bool>) -> Result<()> {
let test_mode = is_test.unwrap_or(false);
if !test_mode {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Restore.as_str()).await?;
}
let res = restore::run(self.cfg.clone(), file.to_path_buf()).await;
FileLock::release(&self.cfg.generated_id).await?;
if !test_mode {
FileLock::release(&self.cfg.generated_id).await?;
}
res
}
}
+17 -6
View File
@@ -36,17 +36,28 @@ impl Database for MySQLDatabase {
ping::run(self.cfg.clone(), self.build_env().clone()).await
}
async fn backup(&self, dir: &Path) -> Result<PathBuf> {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
async fn backup(&self, dir: &Path, is_test: Option<bool>) -> Result<PathBuf> {
let test_mode = is_test.unwrap_or(false);
if !test_mode {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
}
let res = backup::run(self.cfg.clone(), dir.to_path_buf(), self.build_env().clone(), self.file_extension()).await;
FileLock::release(&self.cfg.generated_id).await?;
if !test_mode {
FileLock::release(&self.cfg.generated_id).await?;
}
res
}
async fn restore(&self, file: &Path) -> Result<()> {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Restore.as_str()).await?;
async fn restore(&self, file: &Path, is_test: Option<bool>) -> Result<()> {
let test_mode = is_test.unwrap_or(false);
if !test_mode {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Restore.as_str()).await?;
}
let res = restore::run(self.cfg.clone(), file.to_path_buf()).await;
FileLock::release(&self.cfg.generated_id).await?;
if !test_mode {
FileLock::release(&self.cfg.generated_id).await?;
}
res
}
}
+7 -1
View File
@@ -11,6 +11,7 @@ pub async fn run(
cfg: DatabaseConfig,
format: PostgresDumpFormat,
backup_dir: PathBuf,
is_test: Option<bool>
) -> Result<PathBuf> {
tokio::task::spawn_blocking(move || -> Result<PathBuf> {
debug!("Starting backup for database {}", cfg.name);
@@ -26,7 +27,12 @@ pub async fn run(
}
};
let pg_dump = select_pg_path(&version).join("pg_dump");
let pg_dump = if is_test == Option::from(false) {
select_pg_path(&version).join("pg_dump")
} else {
"pg_dump".to_string().parse()?
};
debug!("Using pg_dump at {:?}", pg_dump);
match format {
+19 -13
View File
@@ -2,11 +2,7 @@ use anyhow::Result;
use async_trait::async_trait;
use std::path::{Path, PathBuf};
use super::{
backup,
format::PostgresDumpFormat,
ping, restore,
};
use super::{backup, format::PostgresDumpFormat, ping, restore};
use crate::domain::factory::Database;
use crate::services::config::DatabaseConfig;
use crate::utils::locks::{DbOpLock, FileLock};
@@ -35,17 +31,27 @@ impl Database for PostgresDatabase {
ping::run(self.cfg.clone()).await
}
async fn backup(&self, dir: &Path) -> Result<PathBuf> {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
let res = backup::run(self.cfg.clone(), self.format, dir.to_path_buf()).await;
FileLock::release(&self.cfg.generated_id).await?;
async fn backup(&self, dir: &Path, is_test: Option<bool>) -> Result<PathBuf> {
let test_mode = is_test.unwrap_or(false);
if !test_mode {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
}
let res = backup::run(self.cfg.clone(), self.format, dir.to_path_buf(), is_test).await;
if !test_mode {
FileLock::release(&self.cfg.generated_id).await?;
}
res
}
async fn restore(&self, file: &Path) -> Result<()> {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Restore.as_str()).await?;
let res = restore::run(self.cfg.clone(), self.format, file.to_path_buf()).await;
FileLock::release(&self.cfg.generated_id).await?;
async fn restore(&self, file: &Path, is_test: Option<bool>) -> Result<()> {
let test_mode = is_test.unwrap_or(false);
if !test_mode {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Restore.as_str()).await?;
}
let res = restore::run(self.cfg.clone(), self.format, file.to_path_buf(), is_test).await;
if !test_mode {
FileLock::release(&self.cfg.generated_id).await?;
}
res
}
}
+7 -1
View File
@@ -11,6 +11,7 @@ pub async fn run(
cfg: DatabaseConfig,
format: PostgresDumpFormat,
restore_file: PathBuf,
is_test: Option<bool>,
) -> Result<()> {
tokio::task::spawn_blocking(move || -> Result<()> {
debug!("Starting restore for database {}", cfg.name);
@@ -26,7 +27,12 @@ pub async fn run(
}
};
let pg_restore = select_pg_path(&version).join("pg_restore");
let pg_restore = if is_test.unwrap_or(false) {
"pg_restore".to_string().parse()?
} else {
select_pg_path(&version).join("pg_restore")
};
debug!("Using pg_restore at {:?}", pg_restore);
if let Err(e) = futures::executor::block_on(terminate_connections(&cfg)) {
+11 -11
View File
@@ -1,4 +1,4 @@
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use async_trait::async_trait;
use std::path::{Path, PathBuf};
@@ -27,19 +27,19 @@ impl Database for RedisDatabase {
ping::run(self.cfg.clone()).await
}
async fn backup(&self, dir: &Path) -> Result<PathBuf> {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
let res = backup::run(
self.cfg.clone(),
dir.to_path_buf(),
self.file_extension(),
)
.await;
FileLock::release(&self.cfg.generated_id).await?;
async fn backup(&self, dir: &Path, is_test: Option<bool>) -> Result<PathBuf> {
let test_mode = is_test.unwrap_or(false);
if !test_mode {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
}
let res = backup::run(self.cfg.clone(), dir.to_path_buf(), self.file_extension()).await;
if !test_mode {
FileLock::release(&self.cfg.generated_id).await?;
}
res
}
async fn restore(&self, _file: &Path) -> Result<()> {
async fn restore(&self, _file: &Path, _is_test: Option<bool>) -> Result<()> {
bail!("Restore not supported for Redis databases")
}
}
+16 -7
View File
@@ -27,17 +27,26 @@ impl Database for SqliteDatabase {
ping::run(self.cfg.clone()).await
}
async fn backup(&self, dir: &Path) -> Result<PathBuf> {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
async fn backup(&self, dir: &Path, is_test: Option<bool>) -> Result<PathBuf> {
let test_mode = is_test.unwrap_or(false);
if !test_mode {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
}
let res = backup::run(self.cfg.clone(), dir.to_path_buf(), self.file_extension()).await;
FileLock::release(&self.cfg.generated_id).await?;
if !test_mode {
FileLock::release(&self.cfg.generated_id).await?;
}
res
}
async fn restore(&self, file: &Path) -> Result<()> {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Restore.as_str()).await?;
async fn restore(&self, file: &Path, is_test: Option<bool>) -> Result<()> {
let test_mode = is_test.unwrap_or(false);
if !test_mode {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Restore.as_str()).await?;
}
let res = restore::run(self.cfg.clone(), file.to_path_buf()).await;
FileLock::release(&self.cfg.generated_id).await?;
if !test_mode {
FileLock::release(&self.cfg.generated_id).await?;
}
res
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ impl BackupService {
});
}
match db.backup(tmp_path).await {
match db.backup(tmp_path, Some(false)).await {
Ok(file) => Ok(BackupResult {
generated_id,
+1 -1
View File
@@ -31,7 +31,7 @@ impl RestoreService {
});
}
match db.restore(&backup_file).await {
match db.restore(&backup_file, Some(false)).await {
Ok(_) => Ok(RestoreResult {
generated_id,
+2 -1
View File
@@ -1 +1,2 @@
mod postgres;
mod postgres;
mod redis;
+88 -14
View File
@@ -1,35 +1,109 @@
use oauth2::url;
use testcontainers::runners::AsyncRunner;
use testcontainers_modules::postgres::Postgres;
use crate::services::config::{DatabaseConfig, DbType};
use url::Host;
use crate::domain::factory::DatabaseFactory;
use crate::services::config::{DatabaseConfig, DbType};
use crate::tests::init_tracing_for_test;
use crate::utils::compress::{compress_to_tar_gz_large, decompress_large_tar_gz};
use oauth2::url;
use std::path::PathBuf;
use tempfile::TempDir;
use testcontainers::runners::AsyncRunner;
use testcontainers::{ContainerAsync, ImageExt};
use testcontainers_modules::postgres::Postgres;
use tracing::{error, info};
use url::Host;
#[tokio::test]
async fn postgres_ping_test() {
async fn create_config() -> (ContainerAsync<Postgres>, DatabaseConfig) {
let container = Postgres::default()
.with_env_var("POSTGRES_DB", "testdb")
.with_env_var("POSTGRES_USER", "testuser")
.with_env_var("POSTGRES_PASSWORD", "changeme")
.with_tag("17")
.start()
.await
.unwrap();
let host = container.get_host().await.unwrap_or(Host::parse("127.0.0.1").unwrap());
let port = container.get_host_port_ipv4(5432).await.unwrap_or(5432) ;
let host = container
.get_host()
.await
.unwrap_or(Host::parse("127.0.0.1").unwrap());
let port = container.get_host_port_ipv4(5432).await.unwrap_or(5432);
let config = DatabaseConfig {
name: "My test Postgres Database".to_string(),
database: "postgres".to_string(),
database: "testdb".to_string(),
db_type: DbType::Postgresql,
username: "postgres".to_string(),
password: "postgres".to_string(),
username: "testuser".to_string(),
password: "changeme".to_string(),
port,
host: host.to_string(),
generated_id: "40875631-e3d2-4dfe-a26b-2a347ecc64fd".to_string(),
path: "".to_string(),
};
(container, config)
}
#[tokio::test]
async fn postgres_ping_test() {
init_tracing_for_test();
let (_container, config) = create_config().await;
let db = DatabaseFactory::create_for_backup(config.clone()).await;
let reachable = db.ping().await.unwrap_or_else(|_| false);
assert_eq!(reachable, true);
}
}
#[tokio::test]
async fn postgres_backup_restore_test() {
init_tracing_for_test();
let (_container, config) = create_config().await;
let temp_dir = TempDir::new().unwrap();
let backup_path = temp_dir.path();
let db = DatabaseFactory::create_for_backup(config.clone()).await;
let file_path = db.backup(backup_path, Some(true)).await.unwrap();
assert!(file_path.is_file());
let compression = compress_to_tar_gz_large(&file_path).await.unwrap();
assert!(compression.compressed_path.is_file());
let files = decompress_large_tar_gz(compression.compressed_path.as_path(), temp_dir.path())
.await
.unwrap();
let backup_file: PathBuf;
if files.len() == 1 {
backup_file = files[0].clone()
} else {
backup_file = "".into()
}
let db = DatabaseFactory::create_for_restore(config.clone(), &backup_file).await;
let reachable = db.ping().await.unwrap_or(false);
info!("Reachable: {}", reachable);
assert_eq!(reachable, true);
info!("Running pg_restore: {:?}", backup_file);
match db.restore(&backup_file, Some(true)).await {
Ok(_) => {
info!("Restore succeeded for {}", config.generated_id);
assert!(true)
}
Err(e) => {
error!("Restore failed for {}: {:?}", config.generated_id, e);
assert!(false)
}
}
}
+65
View File
@@ -0,0 +1,65 @@
use tempfile::TempDir;
use testcontainers::runners::AsyncRunner;
use testcontainers::ContainerAsync;
use testcontainers_modules::redis::Redis;
use url::Host;
use crate::domain::factory::DatabaseFactory;
use crate::services::config::{DatabaseConfig, DbType};
use crate::tests::init_tracing_for_test;
async fn create_config() -> (ContainerAsync<Redis>, DatabaseConfig) {
let container = Redis::default().start().await.unwrap();
let host = container
.get_host()
.await
.unwrap_or(Host::parse("127.0.0.1").unwrap());
let port = container
.get_host_port_ipv4(6379)
.await
.unwrap_or(6379);
let config = DatabaseConfig {
name: "Test Redis".to_string(),
database: "redis".to_string(),
username: "".to_string(),
password: "".to_string(),
db_type: DbType::Redis,
port,
host: host.to_string(),
generated_id: "40875631-e3d2-4dfe-a26b-2a347ecc64fd".to_string(),
path: "".to_string(),
};
(container, config)
}
#[tokio::test]
async fn redis_ping_test() {
init_tracing_for_test();
let (_container, config) = create_config().await;
let db = DatabaseFactory::create_for_backup(config.clone()).await;
let reachable = db.ping().await.unwrap_or(false);
assert!(reachable);
}
#[tokio::test]
async fn redis_backup_test() {
init_tracing_for_test();
let (_container, config) = create_config().await;
let temp_dir = TempDir::new().unwrap();
let backup_path = temp_dir.path();
let db = DatabaseFactory::create_for_backup(config.clone()).await;
let file_path = db.backup(backup_path, Some(true)).await.unwrap();
assert!(file_path.is_file());
}
+17 -1
View File
@@ -1,2 +1,18 @@
mod utils;
mod domain;
mod domain;
use once_cell::sync::Lazy;
use tracing_subscriber;
static TRACING: Lazy<()> = Lazy::new(|| {
let _ = tracing_subscriber::fmt()
.with_test_writer()
.with_env_filter("debug")
.try_init();
});
fn init_tracing_for_test() -> () {
Lazy::force(&TRACING);
}