#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::sync::Arc;
use std::time::Duration;
use tokio::time::timeout;
use tokio_util::sync::CancellationToken;
use modkit::{
config::ConfigProvider,
runtime::{DbOptions, RunOptions, ShutdownOptions, run},
};
use uuid::Uuid;
#[derive(Clone)]
struct DbTestConfigProvider {
configs: std::collections::HashMap<String, serde_json::Value>,
}
impl DbTestConfigProvider {
fn new() -> Self {
Self {
configs: std::collections::HashMap::new(),
}
}
fn with_db_config(mut self, module_name: &str) -> Self {
self.configs.insert(
module_name.to_owned(),
serde_json::json!({
"database": {
"dsn": "sqlite::memory:",
"params": {
"journal_mode": "WAL"
}
}
}),
);
self
}
}
impl ConfigProvider for DbTestConfigProvider {
fn get_module_config(&self, module_name: &str) -> Option<&serde_json::Value> {
self.configs.get(module_name)
}
}
fn create_test_db_manager() -> Arc<modkit_db::DbManager> {
use figment::{Figment, providers::Serialized};
let figment = Figment::new().merge(Serialized::defaults(serde_json::json!({
"test_db_module": {
"database": {
"dsn": "sqlite::memory:",
"params": {
"journal_mode": "WAL"
}
}
}
})));
let home_dir = std::path::PathBuf::from("/tmp/modkit_db_test");
Arc::new(modkit_db::DbManager::from_figment(figment, home_dir).unwrap())
}
#[tokio::test]
async fn test_db_phase_with_manager_succeeds() {
let cancel = CancellationToken::new();
let cancel_clone = cancel.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
cancel_clone.cancel();
});
let opts = RunOptions {
modules_cfg: Arc::new(DbTestConfigProvider::new().with_db_config("test_db_module")),
db: DbOptions::Manager(create_test_db_manager()),
shutdown: ShutdownOptions::Token(cancel),
clients: Vec::new(),
instance_id: Uuid::new_v4(),
oop: None,
shutdown_deadline: None,
};
let result = timeout(Duration::from_millis(500), run(opts)).await;
assert!(result.is_ok(), "DB phase should complete");
let run_result = result.unwrap();
assert!(
run_result.is_ok(),
"DB phase should succeed with valid config"
);
}
#[tokio::test]
async fn test_db_phase_without_config_skips_migration() {
let cancel = CancellationToken::new();
let cancel_clone = cancel.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
cancel_clone.cancel();
});
let opts = RunOptions {
modules_cfg: Arc::new(DbTestConfigProvider::new()), db: DbOptions::Manager(create_test_db_manager()),
shutdown: ShutdownOptions::Token(cancel),
clients: Vec::new(),
instance_id: Uuid::new_v4(),
oop: None,
shutdown_deadline: None,
};
let result = timeout(Duration::from_millis(500), run(opts)).await;
assert!(result.is_ok(), "Should handle missing DB config gracefully");
let run_result = result.unwrap();
assert!(
run_result.is_ok(),
"Should succeed when modules lack DB config"
);
}
#[tokio::test]
async fn test_db_phase_with_none_option() {
let cancel = CancellationToken::new();
let cancel_clone = cancel.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
cancel_clone.cancel();
});
let opts = RunOptions {
modules_cfg: Arc::new(DbTestConfigProvider::new()),
db: DbOptions::None,
shutdown: ShutdownOptions::Token(cancel),
clients: Vec::new(),
instance_id: Uuid::new_v4(),
oop: None,
shutdown_deadline: None,
};
let result = timeout(Duration::from_millis(500), run(opts)).await;
assert!(result.is_ok(), "Should complete with DbOptions::None");
let run_result = result.unwrap();
assert!(run_result.is_ok(), "Should succeed without DB");
}
#[tokio::test]
async fn test_db_phase_error_propagation() {
let cancel = CancellationToken::new();
let cancel_clone = cancel.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
cancel_clone.cancel();
});
let bad_figment = figment::Figment::new().merge(figment::providers::Serialized::defaults(
serde_json::json!({
"test_module": {
"database": {
"dsn": "invalid://connection/string",
}
}
}),
));
let home_dir = std::path::PathBuf::from("/tmp/modkit_db_error_test");
let db_manager_result = modkit_db::DbManager::from_figment(bad_figment, home_dir);
if db_manager_result.is_err() {
return;
}
let opts = RunOptions {
modules_cfg: Arc::new(DbTestConfigProvider::new().with_db_config("test_module")),
db: DbOptions::Manager(Arc::new(db_manager_result.unwrap())),
shutdown: ShutdownOptions::Token(cancel),
clients: Vec::new(),
instance_id: Uuid::new_v4(),
oop: None,
shutdown_deadline: None,
};
let result = timeout(Duration::from_millis(500), run(opts)).await;
assert!(result.is_ok(), "Should not hang on DB errors");
}
#[tokio::test]
async fn test_db_phase_completes_before_init() {
let cancel = CancellationToken::new();
let cancel_clone = cancel.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
cancel_clone.cancel();
});
let opts = RunOptions {
modules_cfg: Arc::new(DbTestConfigProvider::new().with_db_config("test_db_module")),
db: DbOptions::Manager(create_test_db_manager()),
shutdown: ShutdownOptions::Token(cancel),
clients: Vec::new(),
instance_id: Uuid::new_v4(),
oop: None,
shutdown_deadline: None,
};
let start = std::time::Instant::now();
let result = timeout(Duration::from_millis(500), run(opts)).await;
let elapsed = start.elapsed();
assert!(result.is_ok(), "Should complete successfully");
assert!(
elapsed < Duration::from_millis(450),
"Should complete quickly when cancelled"
);
}