#![allow(clippy::unwrap_used, clippy::expect_used)]
mod common;
use figment::{Figment, providers::Serialized};
use modkit_db::{DbError, config::*, manager::DbManager};
use std::collections::HashMap;
use tempfile::TempDir;
#[tokio::test]
async fn test_precedence_module_fields_override_server() {
let global_config = GlobalDatabaseConfig {
servers: {
let mut servers = HashMap::new();
servers.insert(
"sqlite_server".to_owned(),
DbConnConfig {
engine: Some(DbEngineCfg::Sqlite),
params: Some({
let mut params = HashMap::new();
params.insert("synchronous".to_owned(), "FULL".to_owned());
params.insert("journal_mode".to_owned(), "DELETE".to_owned());
params
}),
..Default::default()
},
);
servers
},
auto_provision: Some(false),
};
let figment = Figment::new().merge(Serialized::defaults(serde_json::json!({
"database": global_config,
"modules": {
"test_module": {
"database": {
"server": "sqlite_server",
"engine": "sqlite",
"file": format!("precedence_test_{}.db", std::process::id()),
"params": {
"synchronous": "NORMAL", "busy_timeout": "5000" }
}
}
}
})));
let temp_dir = TempDir::new().unwrap();
let manager = DbManager::from_figment(figment, temp_dir.path().to_path_buf()).unwrap();
let result = manager.get("test_module").await;
match result {
Ok(Some(_handle)) => {
}
Ok(None) => {
panic!("Expected database handle for module");
}
Err(err) => {
let error_msg = err.to_string();
assert!(
!error_msg.contains("Unknown SQLite"),
"Config merging failed: {error_msg}"
);
}
}
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_precedence_module_dsn_override_server() {
let test_data = common::test_data_dir();
let server_db = test_data.join(format!("server_{}.db", std::process::id()));
let module_db = test_data.join(format!("module_{}.db", std::process::id()));
let global_config = GlobalDatabaseConfig {
servers: {
let mut servers = HashMap::new();
servers.insert(
"sqlite_server".to_owned(),
DbConnConfig {
engine: Some(DbEngineCfg::Sqlite),
dsn: Some(format!("sqlite://{}?synchronous=FULL", server_db.display())),
..Default::default()
},
);
servers
},
auto_provision: Some(false),
};
let figment = Figment::new().merge(Serialized::defaults(serde_json::json!({
"database": global_config,
"modules": {
"test_module": {
"database": {
"server": "sqlite_server",
"engine": "sqlite",
"dsn": format!("sqlite://{}?synchronous=NORMAL", module_db.display()) }
}
}
})));
let temp_dir = TempDir::new().unwrap();
let manager = DbManager::from_figment(figment, temp_dir.path().to_path_buf()).unwrap();
let result = manager.get("test_module").await;
match result {
Ok(Some(_db)) => {
assert!(
module_db.exists(),
"Expected module DB file to exist at {}",
module_db.display()
);
assert!(
!server_db.exists(),
"Expected server DB file to NOT exist at {}",
server_db.display()
);
}
Ok(None) => {
panic!("Expected database handle for module");
}
Err(err) => {
panic!("Expected successful connection with module DSN override, got: {err:?}");
}
}
}
#[tokio::test]
async fn test_precedence_params_merging() {
let global_config = GlobalDatabaseConfig {
servers: {
let mut servers = HashMap::new();
servers.insert(
"sqlite_server".to_owned(),
DbConnConfig {
params: Some({
let mut params = HashMap::new();
params.insert("synchronous".to_owned(), "FULL".to_owned());
params.insert("journal_mode".to_owned(), "DELETE".to_owned());
params
}),
..Default::default()
},
);
servers
},
auto_provision: Some(false),
};
let figment = Figment::new().merge(Serialized::defaults(serde_json::json!({
"database": global_config,
"modules": {
"test_module": {
"database": {
"server": "sqlite_server",
"file": format!("params_test_{}.db", std::process::id()),
"params": {
"synchronous": "NORMAL", "busy_timeout": "1000" }
}
}
}
})));
let temp_dir = TempDir::new().unwrap();
let manager = DbManager::from_figment(figment, temp_dir.path().to_path_buf()).unwrap();
let result = manager.get("test_module").await;
match result {
Ok(_handle) => {
}
Err(err) => {
let error_msg = err.to_string();
assert!(!error_msg.contains("PRAGMA"));
assert!(!error_msg.contains("Unknown SQLite"));
}
}
}
#[tokio::test]
async fn test_conflict_detection_sqlite_dsn_with_server_fields() {
let figment = Figment::new().merge(Serialized::defaults(serde_json::json!({
"modules": {
"test_module": {
"database": {
"dsn": format!("sqlite:file:conflict_test_{}.db", std::process::id()),
"host": "localhost", "port": 5432 }
}
}
})));
let temp_dir = TempDir::new().unwrap();
let manager = DbManager::from_figment(figment, temp_dir.path().to_path_buf()).unwrap();
let result = manager.get("test_module").await;
assert!(result.is_err());
if let Err(DbError::ConfigConflict(msg)) = result {
assert!(msg.contains("SQLite DSN cannot be used with host/port fields"));
} else {
panic!("Expected ConfigConflict error, got: {result:?}");
}
}
#[tokio::test]
async fn test_conflict_detection_nonsqlite_dsn_with_sqlite_fields() {
let figment = Figment::new().merge(Serialized::defaults(serde_json::json!({
"modules": {
"test_module": {
"database": {
"dsn": "postgres://user:pass@localhost:5432/db",
"file": format!("pg_conflict_{}.db", std::process::id()) }
}
}
})));
let temp_dir = TempDir::new().unwrap();
let manager = DbManager::from_figment(figment, temp_dir.path().to_path_buf()).unwrap();
let result = manager.get("test_module").await;
assert!(result.is_err());
if let Err(DbError::ConfigConflict(msg)) = result {
assert!(msg.contains("Non-SQLite DSN cannot be used with file/path fields"));
} else {
panic!("Expected ConfigConflict error, got: {result:?}");
}
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_file_and_path_handling() {
let temp_dir = TempDir::new().unwrap();
let absolute_path = temp_dir
.path()
.join(format!("ignored_{}.db", std::process::id()));
let file = format!("file_path_test_{}.db", std::process::id());
let figment = Figment::new().merge(Serialized::defaults(serde_json::json!({
"modules": {
"test_module": {
"database": {
"engine": "sqlite",
"file": file, "path": absolute_path }
}
}
})));
let manager = DbManager::from_figment(figment, temp_dir.path().to_path_buf()).unwrap();
let result = manager.get("test_module").await;
match result {
Ok(Some(_db)) => {
let expected_path = temp_dir.path().join("test_module").join(&file);
assert!(
expected_path.exists(),
"Expected DB file to exist at {}",
expected_path.display()
);
assert!(
!absolute_path.exists(),
"Expected ignored path to NOT exist at {}",
absolute_path.display()
);
}
Ok(None) => {
panic!("Expected database handle");
}
Err(err) => {
panic!("Expected successful connection, got: {err:?}");
}
}
}
#[tokio::test]
async fn test_unknown_server_reference() {
let figment = Figment::new().merge(Serialized::defaults(serde_json::json!({
"modules": {
"test_module": {
"database": {
"server": "nonexistent_server"
}
}
}
})));
let temp_dir = TempDir::new().unwrap();
let manager = DbManager::from_figment(figment, temp_dir.path().to_path_buf()).unwrap();
let result = manager.get("test_module").await;
assert!(result.is_err());
if let Err(DbError::InvalidConfig(msg)) = result {
assert!(msg.contains("Referenced server 'nonexistent_server' not found"));
} else {
panic!("Expected InvalidConfig error, got: {result:?}");
}
}
#[tokio::test]
#[cfg(not(feature = "sqlite"))]
async fn test_feature_disabled_error() {
let figment = Figment::new().merge(Serialized::defaults(serde_json::json!({
"modules": {
"test_module": {
"database": {
"engine": "sqlite",
"file": format!("feature_test_{}.db", std::process::id())
}
}
}
})));
let temp_dir = TempDir::new().unwrap();
let manager = DbManager::from_figment(figment, temp_dir.path().to_path_buf()).unwrap();
let result = manager.get("test_module").await;
assert!(result.is_err());
if let Err(DbError::FeatureDisabled(msg)) = result {
assert!(msg.contains("SQLite feature not enabled"));
} else {
panic!("Expected FeatureDisabled error, got: {result:?}");
}
}
#[tokio::test]
async fn test_redacted_dsn_in_logs() {
use modkit_db::options::redact_credentials_in_dsn;
let dsn_with_password = "postgres://user:secret123@localhost:5432/db";
let redacted = redact_credentials_in_dsn(Some(dsn_with_password));
assert!(redacted.contains("user:***@localhost"));
assert!(!redacted.contains("secret123"));
let dsn_no_password = "sqlite:file:test_no_password.db";
let redacted = redact_credentials_in_dsn(Some(dsn_no_password));
assert_eq!(redacted, "sqlite:file:test_no_password.db");
let redacted = redact_credentials_in_dsn(None);
assert_eq!(redacted, "none");
}