feat: add tokio as a dev dependency and implement unit tests for settings service

This commit is contained in:
2026-02-20 04:40:11 +00:00
parent acc0668392
commit c280f7ff8b
3 changed files with 432 additions and 0 deletions

1
Cargo.lock generated
View File

@@ -1339,6 +1339,7 @@ dependencies = [
"tauri-plugin-log",
"tauri-plugin-opener",
"thiserror 2.0.18",
"tokio",
"uuid",
]

View File

@@ -34,6 +34,10 @@ tauri-plugin-log = "2.8.0"
log = "0.4.29"
struct_iterable = "0.1.1"
[dev-dependencies]
tokio = { workspace = true }
sea-orm = { workspace = true, features = ["mock"] }
[profile.dev]
incremental = true

View File

@@ -157,3 +157,430 @@ impl SettingsService for SettingsServiceImpl {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDateTime;
use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult};
fn create_mock_setting(key: &str, value: &str) -> settings::Model {
settings::Model {
key: key.to_string(),
value: Some(value.to_string()),
updated_at: NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
.expect("Failed to parse datetime"),
}
}
#[tokio::test]
async fn test_get_settings_empty() {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![] as Vec<settings::Model>])
.into_connection();
let service = SettingsServiceImpl::new(db);
let result = service.get_settings(None).await;
assert!(result.is_ok());
let settings = result.expect("Failed to get settings");
// Should return default settings when no settings exist
assert_eq!(settings.language.to_string(), "en");
assert_eq!(settings.default_currency, "HKD");
assert_eq!(settings.base_currency, "HKD");
}
#[tokio::test]
async fn test_get_settings_with_values() {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![
create_mock_setting("language", "zh-TW"),
create_mock_setting("default_currency", "USD"),
create_mock_setting("base_currency", "EUR"),
create_mock_setting("timezone", "Asia/Tokyo"),
] as Vec<settings::Model>])
.into_connection();
let service = SettingsServiceImpl::new(db);
let result = service.get_settings(None).await;
assert!(result.is_ok());
let settings = result.expect("Failed to get settings");
assert_eq!(settings.language.to_string(), "zh-TW");
assert_eq!(settings.default_currency, "USD");
assert_eq!(settings.base_currency, "EUR");
assert_eq!(settings.timezone, "Asia/Tokyo");
}
#[tokio::test]
async fn test_update_setting_insert_new() {
// Mock: find_by_id returns None (setting doesn't exist)
// Insert returns the inserted model via RETURNING clause
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![] as Vec<settings::Model>]) // find_by_id
.append_query_results(vec![
vec![create_mock_setting("language", "zh-HK")] as Vec<settings::Model>
]) // insert returning
.append_exec_results(vec![MockExecResult {
last_insert_id: 1,
rows_affected: 1,
}])
.into_connection();
let service = SettingsServiceImpl::new(db);
let result = service
.update_setting("language".to_string(), "zh-HK".to_string(), None)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_update_setting_update_existing() {
// Mock: find_by_id returns existing setting
// Update returns the updated model via RETURNING clause
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![
vec![create_mock_setting("language", "en")] as Vec<settings::Model>
]) // find_by_id
.append_query_results(vec![
vec![create_mock_setting("language", "zh-TW")] as Vec<settings::Model>
]) // update returning
.append_exec_results(vec![MockExecResult {
last_insert_id: 0,
rows_affected: 1,
}])
.into_connection();
let service = SettingsServiceImpl::new(db);
let result = service
.update_setting("language".to_string(), "zh-TW".to_string(), None)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_update_settings_multiple() {
// Mock two update operations
let db = MockDatabase::new(DatabaseBackend::Sqlite)
// First update: language - find returns None, then insert
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![
vec![create_mock_setting("language", "zh-TW")] as Vec<settings::Model>
])
.append_exec_results(vec![MockExecResult {
last_insert_id: 1,
rows_affected: 1,
}])
// Second update: theme - find returns None, then insert
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![
vec![create_mock_setting("theme", "dark")] as Vec<settings::Model>
])
.append_exec_results(vec![MockExecResult {
last_insert_id: 2,
rows_affected: 1,
}])
.into_connection();
let service = SettingsServiceImpl::new(db);
let input = UpdateSettingsInput {
settings: {
let mut map = HashMap::new();
map.insert("language".to_string(), "zh-TW".to_string());
map.insert("theme".to_string(), "dark".to_string());
map
},
};
let result = service.update_settings(input, None).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_initialize_default_settings() {
// Settings has 12 fields, we mock: 1 exists + 11 inserts
// For each insert: initialize_default_settings does find_by_id, then update_setting does (find_by_id + insert)
// So each insert needs 2 find queries + 1 insert returning query + 1 exec
let db = MockDatabase::new(DatabaseBackend::Sqlite)
// Check language (exists) - only find, no insert
.append_query_results(vec![
vec![create_mock_setting("language", "en")] as Vec<settings::Model>
])
// Check default_currency (not exists) -> update_setting -> find + insert
.append_query_results(vec![vec![] as Vec<settings::Model>]) // initialize_default_settings find
.append_query_results(vec![vec![] as Vec<settings::Model>]) // update_setting find
.append_query_results(vec![
vec![create_mock_setting("default_currency", "HKD")] as Vec<settings::Model>
]) // insert returning
.append_exec_results(vec![MockExecResult {
last_insert_id: 1,
rows_affected: 1,
}])
// Check base_currency (not exists) -> insert
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![
vec![create_mock_setting("base_currency", "HKD")] as Vec<settings::Model>
])
.append_exec_results(vec![MockExecResult {
last_insert_id: 2,
rows_affected: 1,
}])
// Check timezone (not exists) -> insert
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![
vec![create_mock_setting("timezone", "auto")] as Vec<settings::Model>
])
.append_exec_results(vec![MockExecResult {
last_insert_id: 3,
rows_affected: 1,
}])
// Check default_view (not exists) -> insert
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![
vec![create_mock_setting("default_view", "combined")] as Vec<settings::Model>
])
.append_exec_results(vec![MockExecResult {
last_insert_id: 4,
rows_affected: 1,
}])
// Check display_mode (not exists) -> insert
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![
vec![create_mock_setting("display_mode", "system")] as Vec<settings::Model>
])
.append_exec_results(vec![MockExecResult {
last_insert_id: 5,
rows_affected: 1,
}])
// Check decimal_places (not exists) -> insert
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![
vec![create_mock_setting("decimal_places", "2")] as Vec<settings::Model>
])
.append_exec_results(vec![MockExecResult {
last_insert_id: 6,
rows_affected: 1,
}])
// Check date_format (not exists) -> insert
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![
vec![create_mock_setting("date_format", "YYYY-MM-DD")] as Vec<settings::Model>
])
.append_exec_results(vec![MockExecResult {
last_insert_id: 7,
rows_affected: 1,
}])
// Check time_format (not exists) -> insert
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![
vec![create_mock_setting("time_format", "24h")] as Vec<settings::Model>
])
.append_exec_results(vec![MockExecResult {
last_insert_id: 8,
rows_affected: 1,
}])
// Check theme (not exists) -> insert
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![
vec![create_mock_setting("theme", "system")] as Vec<settings::Model>
])
.append_exec_results(vec![MockExecResult {
last_insert_id: 9,
rows_affected: 1,
}])
// Check week_starts_on (not exists) -> insert
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![
vec![create_mock_setting("week_starts_on", "sunday")] as Vec<settings::Model>
])
.append_exec_results(vec![MockExecResult {
last_insert_id: 10,
rows_affected: 1,
}])
// Check scheduled_check_interval (not exists) -> insert
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_query_results(vec![
vec![create_mock_setting("scheduled_check_interval", "1")] as Vec<settings::Model>,
])
.append_exec_results(vec![MockExecResult {
last_insert_id: 11,
rows_affected: 1,
}])
.into_connection();
let service = SettingsServiceImpl::new(db);
let result = service.initialize_default_settings(None).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_initialize_default_settings_all_exist() {
// Mock scenario where all default settings already exist
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![
vec![create_mock_setting("language", "en")] as Vec<settings::Model>
])
.append_query_results(vec![
vec![create_mock_setting("default_currency", "HKD")] as Vec<settings::Model>
])
.append_query_results(vec![
vec![create_mock_setting("base_currency", "HKD")] as Vec<settings::Model>
])
.append_query_results(vec![
vec![create_mock_setting("timezone", "auto")] as Vec<settings::Model>
])
.append_query_results(vec![
vec![create_mock_setting("default_view", "combined")] as Vec<settings::Model>
])
.append_query_results(vec![
vec![create_mock_setting("display_mode", "system")] as Vec<settings::Model>
])
.append_query_results(vec![
vec![create_mock_setting("decimal_places", "2")] as Vec<settings::Model>
])
.append_query_results(vec![
vec![create_mock_setting("date_format", "YYYY-MM-DD")] as Vec<settings::Model>
])
.append_query_results(vec![
vec![create_mock_setting("time_format", "24h")] as Vec<settings::Model>
])
.append_query_results(vec![
vec![create_mock_setting("theme", "system")] as Vec<settings::Model>
])
.append_query_results(vec![
vec![create_mock_setting("week_starts_on", "sunday")] as Vec<settings::Model>
])
.append_query_results(vec![
vec![create_mock_setting("scheduled_check_interval", "1")] as Vec<settings::Model>,
])
.into_connection();
let service = SettingsServiceImpl::new(db);
let result = service.initialize_default_settings(None).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_get_setting_with_default() {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![] as Vec<settings::Model>])
.into_connection();
let service = SettingsServiceImpl::new(db);
let result = service
.get_setting("nonexistent_key", "default_value")
.await;
assert_eq!(result, "default_value");
}
#[tokio::test]
async fn test_get_setting_with_value() {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![
vec![create_mock_setting("existing_key", "stored_value")] as Vec<settings::Model>,
])
.into_connection();
let service = SettingsServiceImpl::new(db);
let result = service.get_setting("existing_key", "default_value").await;
assert_eq!(result, "stored_value");
}
#[tokio::test]
async fn test_settings_with_all_fields() {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![
create_mock_setting("language", "zh-HK"),
create_mock_setting("default_currency", "USD"),
create_mock_setting("base_currency", "EUR"),
create_mock_setting("timezone", "UTC"),
create_mock_setting("default_view", "split"),
create_mock_setting("display_mode", "dark"),
create_mock_setting("decimal_places", "4"),
create_mock_setting("date_format", "DD/MM/YYYY"),
create_mock_setting("time_format", "12h"),
create_mock_setting("theme", "light"),
create_mock_setting("week_starts_on", "monday"),
create_mock_setting("scheduled_check_interval", "7"),
] as Vec<settings::Model>])
.into_connection();
let service = SettingsServiceImpl::new(db);
let result = service.get_settings(None).await;
assert!(result.is_ok());
let settings = result.expect("Failed to get settings");
assert_eq!(settings.language.to_string(), "zh-HK");
assert_eq!(settings.default_currency, "USD");
assert_eq!(settings.base_currency, "EUR");
assert_eq!(settings.timezone, "UTC");
assert_eq!(settings.default_view.to_string(), "split");
assert_eq!(settings.display_mode.to_string(), "dark");
assert_eq!(settings.decimal_places, 4);
assert_eq!(settings.date_format, "DD/MM/YYYY");
assert_eq!(settings.time_format, "12h");
assert_eq!(settings.theme.to_string(), "light");
assert_eq!(settings.week_starts_on.to_string(), "monday");
assert_eq!(settings.scheduled_check_interval, 7);
}
#[tokio::test]
async fn test_update_setting_with_null_value_in_db() {
let setting_with_null = settings::Model {
key: "test_key".to_string(),
value: None,
updated_at: NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
.expect("Failed to parse datetime"),
};
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![setting_with_null] as Vec<settings::Model>]) // find_by_id
.append_query_results(vec![
vec![create_mock_setting("test_key", "new_value")] as Vec<settings::Model>
]) // update returning
.append_exec_results(vec![MockExecResult {
last_insert_id: 0,
rows_affected: 1,
}])
.into_connection();
let service = SettingsServiceImpl::new(db);
let result = service
.update_setting("test_key".to_string(), "new_value".to_string(), None)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_settings_transaction_logs() {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![] as Vec<settings::Model>])
.append_exec_results(vec![MockExecResult {
last_insert_id: 0,
rows_affected: 1,
}])
.into_connection();
let service = SettingsServiceImpl::new(db);
// Verify that the database backend is SQLite
assert_eq!(service.db.get_database_backend(), DatabaseBackend::Sqlite);
}
}