mod common;
use common::TestFixture;
use rcman::{SettingsConfig, SettingsManager};
use serde_json::json;
use std::fs;
use std::sync::Arc;
use std::thread;
use tempfile::TempDir;
#[test]
fn test_save_invalid_top_level_key() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let result = fixture
.manager
.save_setting("invalid_section", "key", &json!("value"));
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("not found") || err_msg.contains("invalid_section"));
}
#[test]
fn test_save_invalid_nested_key() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let result = fixture
.manager
.save_setting("ui", "invalid_key", &json!("value"));
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("not found") || err_msg.contains("invalid_key"));
}
#[test]
fn test_deeply_nested_invalid_path() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let result = fixture
.manager
.save_setting("ui.nested.deeply.invalid", "key", &json!("value"));
assert!(result.is_err());
}
#[test]
fn test_save_wrong_type_for_number() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let result = fixture
.manager
.save_setting("ui", "font_size", &json!("not_a_number"));
assert!(result.is_err());
}
#[test]
fn test_save_wrong_type_for_boolean() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let result = fixture
.manager
.save_setting("general", "tray_enabled", &json!(123));
let _ = result;
}
#[test]
fn test_number_out_of_range() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let result = fixture
.manager
.save_setting("ui", "font_size", &json!(100.0));
let _ = result;
}
#[test]
fn test_select_invalid_option() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let result = fixture
.manager
.save_setting("ui", "theme", &json!("invalid_theme"));
let _ = result;
}
#[test]
fn test_concurrent_reads() {
let fixture = Arc::new(TestFixture::new());
let _ = fixture.manager.get_all().unwrap();
fixture
.manager
.save_setting("ui", "theme", &json!("light"))
.unwrap();
let mut handles = vec![];
for _ in 0..10 {
let fixture_clone = Arc::clone(&fixture);
let handle = thread::spawn(move || {
let metadata = fixture_clone.manager.metadata().unwrap();
let theme = metadata.get("ui.theme").unwrap();
assert_eq!(theme.value, Some(json!("light")));
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
#[test]
fn test_concurrent_writes_same_key() {
let fixture = Arc::new(TestFixture::new());
let _ = fixture.manager.get_all().unwrap();
let mut handles = vec![];
for i in 0..5 {
let fixture_clone = Arc::clone(&fixture);
let value = if i % 2 == 0 { "light" } else { "dark" };
let handle = thread::spawn(move || {
fixture_clone
.manager
.save_setting("ui", "theme", &json!(value))
.unwrap();
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
let metadata = fixture.manager.metadata().unwrap();
let theme = metadata.get("ui.theme").unwrap();
let value = theme.value.as_ref().unwrap().as_str().unwrap();
assert!(value == "light" || value == "dark");
}
#[test]
fn test_concurrent_writes_different_keys() {
let fixture = Arc::new(TestFixture::new());
let _ = fixture.manager.get_all().unwrap();
let mut handles = vec![];
let fixture_clone = Arc::clone(&fixture);
handles.push(thread::spawn(move || {
for _ in 0..10 {
fixture_clone
.manager
.save_setting("ui", "theme", &json!("light"))
.unwrap();
}
}));
let fixture_clone = Arc::clone(&fixture);
handles.push(thread::spawn(move || {
for _ in 0..10 {
fixture_clone
.manager
.save_setting("ui", "font_size", &json!(16.0))
.unwrap();
}
}));
let fixture_clone = Arc::clone(&fixture);
handles.push(thread::spawn(move || {
for _ in 0..10 {
fixture_clone
.manager
.save_setting("general", "language", &json!("en"))
.unwrap();
}
}));
for handle in handles {
handle.join().unwrap();
}
let metadata = fixture.manager.metadata().unwrap();
assert_eq!(
metadata.get("ui.theme").unwrap().value,
Some(json!("light"))
);
assert_eq!(
metadata.get("ui.font_size").unwrap().value,
Some(json!(16.0))
);
assert_eq!(
metadata.get("general.language").unwrap().value,
Some(json!("en"))
);
}
#[test]
fn test_concurrent_save_and_reset_all_interactions() {
let fixture = Arc::new(TestFixture::new());
let _ = fixture.manager.get_all().unwrap();
let mut handles = vec![];
let fixture_clone = Arc::clone(&fixture);
handles.push(thread::spawn(move || {
for _ in 0..50 {
fixture_clone
.manager
.save_setting("ui", "theme", &json!("light"))
.unwrap();
fixture_clone
.manager
.save_setting("ui", "font_size", &json!(16.0))
.unwrap();
fixture_clone
.manager
.save_setting("general", "language", &json!("tr"))
.unwrap();
}
}));
let fixture_clone = Arc::clone(&fixture);
handles.push(thread::spawn(move || {
for _ in 0..50 {
fixture_clone.manager.reset_all().unwrap();
}
}));
for handle in handles {
handle.join().unwrap();
}
let metadata = fixture.manager.metadata().unwrap();
let theme = metadata
.get("ui.theme")
.and_then(|m| m.value.as_ref())
.and_then(serde_json::Value::as_str)
.unwrap();
assert!(theme == "dark" || theme == "light");
let font_size = metadata
.get("ui.font_size")
.and_then(|m| m.value.as_ref())
.and_then(serde_json::Value::as_f64)
.unwrap();
assert!((font_size - 14.0).abs() < f64::EPSILON || (font_size - 16.0).abs() < f64::EPSILON);
let language = metadata
.get("general.language")
.and_then(|m| m.value.as_ref())
.and_then(serde_json::Value::as_str)
.unwrap();
assert!(language == "en" || language == "tr");
let settings_path = fixture.manager.config().settings_path();
let persisted: serde_json::Value =
serde_json::from_str(&fs::read_to_string(settings_path).unwrap()).unwrap();
assert!(persisted.is_object());
}
#[test]
fn test_load_corrupted_json() {
let temp_dir = TempDir::new().unwrap();
let config = SettingsConfig::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_schema::<common::TestSettings>()
.build();
let manager = SettingsManager::new(config).unwrap();
manager.get_all().unwrap();
let settings_file = temp_dir.path().join("settings.json");
fs::write(&settings_file, b"{invalid json content").unwrap();
let _ = manager.metadata();
}
#[test]
fn test_load_truncated_json() {
let temp_dir = TempDir::new().unwrap();
let config = SettingsConfig::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_schema::<common::TestSettings>()
.build();
let manager = SettingsManager::new(config).unwrap();
manager.get_all().unwrap();
let settings_file = temp_dir.path().join("settings.json");
fs::write(&settings_file, b"{\"ui\": {\"theme\":").unwrap();
let _ = manager.metadata();
}
#[test]
fn test_save_to_readonly_directory() {
let temp_dir = TempDir::new().unwrap();
let config = SettingsConfig::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_schema::<common::TestSettings>()
.build();
let manager = SettingsManager::new(config).unwrap();
let _ = manager.get_all().unwrap();
manager
.save_setting("ui", "theme", &json!("light"))
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let settings_path = temp_dir.path().join("settings.json");
assert!(
settings_path.exists(),
"Settings file should exist after save"
);
let perms = fs::metadata(&settings_path).unwrap().permissions();
assert_eq!(
perms.mode() & 0o777,
0o600,
"Settings file should have 0o600 permissions"
);
let mut dir_perms = fs::metadata(temp_dir.path()).unwrap().permissions();
dir_perms.set_mode(0o555); fs::set_permissions(temp_dir.path(), dir_perms).unwrap();
let result = manager.save_setting("ui", "theme", &json!("dark"));
assert!(
result.is_err(),
"Save should fail when directory is readonly"
);
let mut dir_perms = fs::metadata(temp_dir.path()).unwrap().permissions();
dir_perms.set_mode(0o755);
fs::set_permissions(temp_dir.path(), dir_perms).unwrap();
}
}
#[test]
fn test_env_override_basic() {
let fixture = TestFixture::with_env_prefix("RCMAN_TEST_BASIC");
fixture
.env_source
.set("RCMAN_TEST_BASIC_UI__THEME", "light");
let settings = fixture.manager.get_all().unwrap();
let _ = settings.ui.theme;
}
#[test]
fn test_env_override_precedence_over_saved() {
let fixture = TestFixture::with_env_prefix("RCMAN_TEST_PRECEDENCE");
let _ = fixture.manager.get_all().unwrap();
fixture
.manager
.save_setting("ui", "theme", &json!("dark"))
.unwrap();
fixture
.env_source
.set("RCMAN_TEST_PRECEDENCE_UI__THEME", "light");
let metadata = fixture.manager.metadata().unwrap();
let theme = metadata.get("ui.theme").unwrap();
let _ = theme.value;
}
#[test]
fn test_env_override_invalid_value() {
let fixture = TestFixture::new();
fixture
.env_source
.set("RCMAN_TEST_UI__THEME", "invalid_theme");
let result = fixture.manager.get_all();
let _ = result;
}
#[test]
fn test_env_override_type_mismatch() {
let fixture = TestFixture::new();
fixture
.env_source
.set("RCMAN_TEST_UI__FONT_SIZE", "not_a_number");
let result = fixture.manager.get_all();
let _ = result;
}
#[test]
fn test_reset_nonexistent_key() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let result = fixture.manager.reset_setting("ui", "theme");
assert!(result.is_ok());
}
#[test]
fn test_reset_all_empty_settings() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let result = fixture.manager.reset_all();
assert!(result.is_ok());
}
#[test]
fn test_sub_settings_invalid_key_with_dots() {
let fixture = TestFixture::with_sub_settings();
let remotes = fixture.manager.sub_settings("remotes").unwrap();
let result = remotes.set("remote.with.dots", &json!({"type": "test"}));
let _ = result;
}
#[test]
fn test_sub_settings_empty_key() {
let fixture = TestFixture::with_sub_settings();
let remotes = fixture.manager.sub_settings("remotes").unwrap();
let result = remotes.set("", &json!({"type": "test"}));
let _ = result;
}
#[test]
fn test_sub_settings_concurrent_access() {
let fixture = Arc::new(TestFixture::with_sub_settings());
let mut handles = vec![];
for i in 0..5 {
let fixture_clone = Arc::clone(&fixture);
let handle = thread::spawn(move || {
let remotes = fixture_clone.manager.sub_settings("remotes").unwrap();
remotes
.set(&format!("remote{i}"), &json!({"id": i}))
.unwrap();
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
let remotes = fixture.manager.sub_settings("remotes").unwrap();
for i in 0..5 {
let remote: serde_json::Value = remotes.get(&format!("remote{i}")).unwrap();
assert_eq!(remote["id"], i);
}
}
#[test]
fn test_save_null_value() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let result = fixture.manager.save_setting("ui", "theme", &json!(null));
assert!(result.is_err());
}
#[test]
fn test_save_very_long_string() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let long_string = "a".repeat(10_000);
let result = fixture
.manager
.save_setting("ui", "theme", &json!(long_string));
assert!(result.is_err());
}
#[test]
fn test_save_very_large_number() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let result = fixture
.manager
.save_setting("ui", "font_size", &json!(f64::MAX));
assert!(result.is_err());
}
#[test]
fn test_save_negative_number() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let result = fixture
.manager
.save_setting("ui", "font_size", &json!(-10.0));
assert!(result.is_err());
}
#[test]
fn test_save_infinity() {
let fixture = TestFixture::new();
let _ = fixture.manager.get_all().unwrap();
let result = fixture
.manager
.save_setting("ui", "font_size", &json!(f64::INFINITY));
assert!(result.is_err());
}