mod common;
use rcman::{SettingsConfig, SettingsManager, SubSettingsConfig};
use serde_json::json;
use std::sync::{Arc, Mutex};
use tempfile::TempDir;
#[test]
fn test_create_profile() {
let temp_dir = TempDir::new().unwrap();
let manager = SettingsManager::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_sub_settings(SubSettingsConfig::new("remotes").with_profiles())
.build()
.unwrap();
let remotes = manager.sub_settings("remotes").unwrap();
let profiles = remotes.profiles().unwrap();
profiles.create("work").unwrap();
let list = profiles.list().unwrap();
assert!(list.contains(&"default".to_string()));
assert!(list.contains(&"work".to_string()));
}
#[test]
fn test_switch_profile() {
let temp_dir = TempDir::new().unwrap();
let manager = SettingsManager::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_sub_settings(SubSettingsConfig::new("remotes").with_profiles())
.build()
.unwrap();
let remotes = manager.sub_settings("remotes").unwrap();
let profiles = remotes.profiles().unwrap();
profiles.create("work").unwrap();
profiles.switch("work").unwrap();
assert_eq!(profiles.active().unwrap(), "work");
}
#[test]
fn test_seamless_profile_switching() {
let temp_dir = TempDir::new().unwrap();
let manager = SettingsManager::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_sub_settings(SubSettingsConfig::new("remotes").with_profiles())
.build()
.unwrap();
let remotes = manager.sub_settings("remotes").unwrap();
remotes
.set("personal-gdrive", &json!({"type": "drive"}))
.unwrap();
assert!(remotes.exists("personal-gdrive").unwrap());
remotes.profiles().unwrap().create("work").unwrap();
remotes.switch_profile("work").unwrap();
assert!(!remotes.exists("personal-gdrive").unwrap());
remotes
.set("company-drive", &json!({"type": "sharepoint"}))
.unwrap();
assert!(remotes.exists("company-drive").unwrap());
remotes.switch_profile("default").unwrap();
assert!(remotes.exists("personal-gdrive").unwrap());
assert!(!remotes.exists("company-drive").unwrap());
}
#[test]
fn test_delete_profile() {
let temp_dir = TempDir::new().unwrap();
let manager = SettingsManager::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_sub_settings(SubSettingsConfig::new("remotes").with_profiles())
.build()
.unwrap();
let remotes = manager.sub_settings("remotes").unwrap();
let profiles = remotes.profiles().unwrap();
profiles.create("temp").unwrap();
assert!(profiles.exists("temp").unwrap());
profiles.delete("temp").unwrap();
assert!(!profiles.exists("temp").unwrap());
}
#[test]
fn test_cannot_delete_active_profile() {
let temp_dir = TempDir::new().unwrap();
let manager = SettingsManager::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_sub_settings(SubSettingsConfig::new("remotes").with_profiles())
.build()
.unwrap();
let remotes = manager.sub_settings("remotes").unwrap();
let profiles = remotes.profiles().unwrap();
let result = profiles.delete("default");
assert!(result.is_err());
}
#[test]
fn test_rename_profile() {
let temp_dir = TempDir::new().unwrap();
let manager = SettingsManager::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_sub_settings(SubSettingsConfig::new("remotes").with_profiles())
.build()
.unwrap();
let remotes = manager.sub_settings("remotes").unwrap();
let profiles = remotes.profiles().unwrap();
profiles.create("old-name").unwrap();
profiles.rename("old-name", "new-name").unwrap();
let list = profiles.list().unwrap();
assert!(!list.contains(&"old-name".to_string()));
assert!(list.contains(&"new-name".to_string()));
}
#[test]
fn test_duplicate_profile() {
let temp_dir = TempDir::new().unwrap();
let manager = SettingsManager::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_sub_settings(SubSettingsConfig::new("remotes").with_profiles())
.build()
.unwrap();
let remotes = manager.sub_settings("remotes").unwrap();
remotes.set("gdrive", &json!({"type": "drive"})).unwrap();
let profiles = remotes.profiles().unwrap();
profiles.duplicate("default", "backup").unwrap();
let list = profiles.list().unwrap();
assert!(list.contains(&"default".to_string()));
assert!(list.contains(&"backup".to_string()));
let backup_dir = temp_dir
.path()
.join("remotes")
.join("profiles")
.join("backup");
assert!(backup_dir.join("gdrive.json").exists());
}
#[test]
fn test_profile_events() {
let temp_dir = TempDir::new().unwrap();
let manager = SettingsManager::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_sub_settings(SubSettingsConfig::new("remotes").with_profiles())
.build()
.unwrap();
let remotes = manager.sub_settings("remotes").unwrap();
let profiles = remotes.profiles().unwrap();
let events = Arc::new(Mutex::new(Vec::new()));
let events_clone = events.clone();
profiles.set_on_event(move |event| {
events_clone.lock().unwrap().push(format!("{event:?}"));
});
profiles.create("work").unwrap();
profiles.switch("work").unwrap();
profiles.rename("work", "job").unwrap();
let recorded = events.lock().unwrap();
assert_eq!(recorded.len(), 3);
}
#[test]
fn test_profiles_not_enabled_error() {
let temp_dir = TempDir::new().unwrap();
let manager = SettingsManager::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_sub_settings(SubSettingsConfig::new("remotes")) .build()
.unwrap();
let remotes = manager.sub_settings("remotes").unwrap();
let result = remotes.profiles();
assert!(result.is_err());
}
#[test]
fn test_profile_directory_structure() {
let temp_dir = TempDir::new().unwrap();
let manager = SettingsManager::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_sub_settings(SubSettingsConfig::new("remotes").with_profiles())
.build()
.unwrap();
let remotes = manager.sub_settings("remotes").unwrap();
remotes.set("gdrive", &json!({"type": "drive"})).unwrap();
let profiles = remotes.profiles().unwrap();
profiles.create("work").unwrap();
let remotes_dir = temp_dir.path().join("remotes");
assert!(remotes_dir.join(".profiles.json").exists());
assert!(remotes_dir.join("profiles").join("default").is_dir());
assert!(remotes_dir.join("profiles").join("work").is_dir());
assert!(
remotes_dir
.join("profiles")
.join("default")
.join("gdrive.json")
.exists()
);
}
#[test]
fn test_single_file_mode_with_profiles() {
let temp_dir = TempDir::new().unwrap();
let manager = SettingsManager::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_sub_settings(SubSettingsConfig::singlefile("backends").with_profiles())
.build()
.unwrap();
let backends = manager.sub_settings("backends").unwrap();
backends
.set("local", &json!({"host": "localhost"}))
.unwrap();
let profiles = backends.profiles().unwrap();
profiles.create("work").unwrap();
let backends_dir = temp_dir.path().join("backends");
assert!(backends_dir.join(".profiles.json").exists());
assert!(
backends_dir
.join("profiles")
.join("default")
.join("backends.json")
.exists()
);
}
#[test]
fn test_single_file_profile_migration() {
let temp_dir = TempDir::new().unwrap();
let backends_file = temp_dir.path().join("backends.json");
std::fs::write(&backends_file, r#"{ "local": { "type": "local" } }"#).unwrap();
let _manager = SettingsManager::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_sub_settings(SubSettingsConfig::singlefile("backends").with_profiles())
.build()
.unwrap();
assert!(
!backends_file.exists(),
"Legacy file should have been moved"
);
let backends_dir = temp_dir.path().join("backends");
assert!(backends_dir.is_dir());
assert!(backends_dir.join(".profiles.json").exists());
let migrated_file = backends_dir
.join("profiles")
.join("default")
.join("backends.json");
assert!(
migrated_file.exists(),
"File should be migrated to default profile"
);
let content = std::fs::read_to_string(migrated_file).unwrap();
assert!(content.contains("local"));
}
#[test]
fn test_main_settings_profiles() {
use rcman::{SettingMetadata, SettingsSchema};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize, Default)]
struct TestSettings {
#[serde(default)]
general: GeneralSettings,
}
#[derive(Serialize, Deserialize)]
struct GeneralSettings {
#[serde(default = "default_theme")]
theme: String,
}
fn default_theme() -> String {
"light".to_string()
}
impl Default for GeneralSettings {
fn default() -> Self {
Self {
theme: default_theme(),
}
}
}
impl SettingsSchema for TestSettings {
fn get_metadata() -> HashMap<String, SettingMetadata> {
let mut map = HashMap::new();
map.insert(
"general.theme".to_string(),
SettingMetadata::text("light").meta_str("label", "Theme"),
);
map
}
}
let temp_dir = TempDir::new().unwrap();
let config = SettingsConfig::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_schema::<TestSettings>()
.with_profiles() .build();
let manager = SettingsManager::new(config).unwrap();
assert!(manager.is_profiles_enabled());
assert_eq!(manager.active_profile().unwrap(), "default");
manager
.save_setting("general", "theme", &json!("dark"))
.unwrap();
manager.create_profile("work").unwrap();
manager.switch_profile("work").unwrap();
assert_eq!(manager.active_profile().unwrap(), "work");
let settings: TestSettings = manager.get_all().unwrap();
assert_eq!(settings.general.theme, "light");
manager
.save_setting("general", "theme", &json!("ocean"))
.unwrap();
manager.switch_profile("default").unwrap();
let settings: TestSettings = manager.get_all().unwrap();
assert_eq!(settings.general.theme, "dark");
manager.switch_profile("work").unwrap();
let settings: TestSettings = manager.get_all().unwrap();
assert_eq!(settings.general.theme, "ocean");
}
#[test]
fn test_main_profile_switch_emits_changed_setting_callbacks() {
use rcman::{SettingMetadata, SettingsSchema};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Default, Serialize, Deserialize)]
struct TestSettings {
general: GeneralSettings,
}
#[derive(Default, Serialize, Deserialize)]
struct GeneralSettings {
theme: String,
}
impl SettingsSchema for TestSettings {
fn get_metadata() -> HashMap<String, SettingMetadata> {
let mut map = HashMap::new();
map.insert(
"general.theme".to_string(),
SettingMetadata::text("light").meta_str("label", "Theme"),
);
map
}
}
let temp_dir = TempDir::new().unwrap();
let config = SettingsConfig::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_schema::<TestSettings>()
.with_profiles()
.build();
let manager = SettingsManager::new(config).unwrap();
manager
.save_setting("general", "theme", &json!("dark"))
.unwrap();
manager.create_profile("work").unwrap();
manager.switch_profile("work").unwrap();
manager
.save_setting("general", "theme", &json!("ocean"))
.unwrap();
let events = Arc::new(Mutex::new(Vec::new()));
let events_clone = Arc::clone(&events);
manager.events().on_change(move |key, old, new| {
events_clone
.lock()
.unwrap()
.push((key.to_string(), old.clone(), new.clone()));
});
manager.switch_profile("default").unwrap();
manager.switch_profile("work").unwrap();
let recorded = events.lock().unwrap();
assert_eq!(recorded.len(), 2);
assert!(recorded.iter().any(|(key, old, new)| key == "general.theme"
&& *old == json!("ocean")
&& *new == json!("dark")));
assert!(recorded.iter().any(|(key, old, new)| key == "general.theme"
&& *old == json!("dark")
&& *new == json!("ocean")));
}
#[test]
fn test_main_profile_switch_without_value_change_emits_no_callbacks() {
use rcman::{SettingMetadata, SettingsSchema};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Default, Serialize, Deserialize)]
struct TestSettings {
general: GeneralSettings,
}
#[derive(Default, Serialize, Deserialize)]
struct GeneralSettings {
theme: String,
}
impl SettingsSchema for TestSettings {
fn get_metadata() -> HashMap<String, SettingMetadata> {
let mut map = HashMap::new();
map.insert(
"general.theme".to_string(),
SettingMetadata::text("light").meta_str("label", "Theme"),
);
map
}
}
let temp_dir = TempDir::new().unwrap();
let config = SettingsConfig::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_schema::<TestSettings>()
.with_profiles()
.build();
let manager = SettingsManager::new(config).unwrap();
manager.create_profile("work").unwrap();
let callback_count = Arc::new(Mutex::new(0usize));
let callback_count_clone = Arc::clone(&callback_count);
manager.events().on_change(move |_key, _old, _new| {
let mut guard = callback_count_clone.lock().unwrap();
*guard += 1;
});
manager.switch_profile("work").unwrap();
manager.switch_profile("default").unwrap();
assert_eq!(*callback_count.lock().unwrap(), 0);
}
#[test]
fn test_main_settings_profiles_directory_structure() {
use rcman::{SettingMetadata, SettingsSchema};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Default, Serialize, Deserialize)]
struct TestSettings {
ui: UiSettings,
}
#[derive(Default, Serialize, Deserialize)]
struct UiSettings {
theme: String,
}
impl SettingsSchema for TestSettings {
fn get_metadata() -> HashMap<String, SettingMetadata> {
let mut map = HashMap::new();
map.insert(
"ui.theme".to_string(),
SettingMetadata::text("light").meta_str("label", "Theme"),
);
map
}
}
let temp_dir = TempDir::new().unwrap();
let config = SettingsConfig::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_schema::<TestSettings>()
.with_profiles()
.build();
let manager = SettingsManager::new(config).unwrap();
manager.save_setting("ui", "theme", &json!("dark")).unwrap();
manager.create_profile("work").unwrap();
assert!(
temp_dir.path().join(".profiles.json").exists(),
".profiles.json should exist"
);
let profiles_dir = temp_dir.path().join("profiles");
assert!(profiles_dir.exists(), "profiles/ directory should exist");
assert!(
profiles_dir.join("work").exists(),
"work profile directory should exist"
);
assert!(
profiles_dir.join("default").exists(),
"default profile directory should exist after saving"
);
assert!(
profiles_dir.join("default").join("settings.json").exists(),
"settings.json should exist in default profile"
);
}
#[test]
fn test_main_profile_propagation_with_mixed_sub_settings() {
let temp_dir = TempDir::new().unwrap();
let manager = SettingsManager::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_profiles()
.with_sub_settings(SubSettingsConfig::new("remotes").with_profiles())
.with_sub_settings(SubSettingsConfig::new("shared"))
.build()
.unwrap();
manager.create_profile("work").unwrap();
manager.switch_profile("work").unwrap();
let remotes = manager.sub_settings("remotes").unwrap();
assert_eq!(remotes.profiles().unwrap().active().unwrap(), "work");
let shared = manager.sub_settings("shared").unwrap();
assert!(matches!(
shared.profiles(),
Err(rcman::Error::ProfilesNotEnabled)
));
}
#[cfg(any(feature = "keychain", feature = "encrypted-file"))]
#[test]
#[cfg_attr(
feature = "keychain",
ignore = "Requires Secret Service daemon (not available in CI)"
)]
fn test_secret_reset_is_profile_scoped() {
use rcman::{SettingMetadata, SettingsSchema};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Default, Serialize, Deserialize)]
struct TestSettings {
api: ApiSettings,
}
#[derive(Default, Serialize, Deserialize)]
struct ApiSettings {
key: String,
}
impl SettingsSchema for TestSettings {
fn get_metadata() -> HashMap<String, SettingMetadata> {
let mut map = HashMap::new();
map.insert(
"api.key".to_string(),
SettingMetadata::text("")
.meta_str("label", "API Key")
.secret(),
);
map
}
}
let temp_dir = TempDir::new().unwrap();
let config = SettingsConfig::builder("test-app", "1.0.0")
.with_config_dir(temp_dir.path())
.with_schema::<TestSettings>()
.with_profiles()
.with_credentials()
.build();
let manager = SettingsManager::new(config).unwrap();
manager
.save_setting("api", "key", &json!("default-secret"))
.unwrap();
manager.create_profile("work").unwrap();
manager.switch_profile("work").unwrap();
manager
.save_setting("api", "key", &json!("work-secret"))
.unwrap();
manager.reset_setting("api", "key").unwrap();
let work_metadata = manager.metadata().unwrap();
assert_eq!(work_metadata.get("api.key").unwrap().value, Some(json!("")));
manager.switch_profile("default").unwrap();
let default_metadata = manager.metadata().unwrap();
assert_eq!(
default_metadata.get("api.key").unwrap().value,
Some(json!("default-secret"))
);
}