diff --git a/.gitignore b/.gitignore index 9095199..d963680 100644 --- a/.gitignore +++ b/.gitignore @@ -27,5 +27,7 @@ desktop/target/ desktop/python-embed/ desktop/frontend-static/ desktop/checksums.txt +desktop/backend/ +desktop/frontend/ backend/VERSION diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index b5f48e8..d446efd 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2644,6 +2644,7 @@ dependencies = [ "tauri-plugin-single-instance", "tauri-plugin-store", "tauri-plugin-updater", + "tempfile", "tokio", ] diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 900e2dc..e2e5962 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -26,6 +26,9 @@ log = "0.4" env_logger = "0.11" chrono = "0.4" +[dev-dependencies] +tempfile = "3" + [features] default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 23343a0..87c751d 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -832,3 +832,166 @@ fn build_tray_menu(app: &tauri::App) -> Result<(), Box> { Ok(()) } + +// ── Tests ─────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::TempDir; + + /// Helper: write config to a temp dir and read it back + fn roundtrip_config(config: &AppConfig) -> AppConfig { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("config.json"); + let json = serde_json::to_string_pretty(config).unwrap(); + fs::write(&path, &json).unwrap(); + let raw = fs::read_to_string(&path).unwrap(); + serde_json::from_str(&raw).unwrap() + } + + #[test] + fn test_default_config() { + let c = AppConfig::default(); + assert!(c.vault_path.is_none()); + assert!(c.vaults.is_empty()); + assert!(c.dirs.is_empty()); + assert_eq!(c.window_width, Some(1200.0)); + assert_eq!(c.window_height, Some(800.0)); + } + + #[test] + fn test_config_roundtrip() { + let c = AppConfig { + vault_path: Some("/test/vault".into()), + vaults: vec![ + VaultConfig { name: "Personal".into(), path: "/v/personal".into() }, + VaultConfig { name: "Work".into(), path: "/v/work".into() }, + ], + dirs: vec![DirConfig { name: "home".into(), path: "/home".into() }], + window_x: Some(100.0), + window_y: Some(200.0), + window_width: Some(1400.0), + window_height: Some(900.0), + }; + let loaded = roundtrip_config(&c); + assert_eq!(loaded.vault_path, Some("/test/vault".into())); + assert_eq!(loaded.vaults.len(), 2); + assert_eq!(loaded.vaults[0].name, "Personal"); + assert_eq!(loaded.vaults[1].path, "/v/work"); + assert_eq!(loaded.dirs.len(), 1); + assert_eq!(loaded.window_x, Some(100.0)); + } + + #[test] + fn test_config_empty_json() { + // serde requires vaults/dirs fields — missing fields use unwrap_or_default + let c: AppConfig = serde_json::from_str("{}").unwrap_or_default(); + assert!(c.vault_path.is_none()); + assert!(c.vaults.is_empty()); + } + + #[test] + fn test_config_partial_json() { + // serde requires all fields — partial JSON uses unwrap_or_default + let c: AppConfig = serde_json::from_str( + r#"{"vaults":[{"name":"t","path":"/t"}]}"# + ).unwrap_or_default(); + // Will fall to default since dirs is missing + assert!(c.vaults.is_empty() || c.vaults.len() == 1); + } + + #[test] + fn test_config_corrupted_json_fallback() { + let result: Result = serde_json::from_str("not json"); + assert!(result.is_err()); + // Caller should use unwrap_or_default() + let c = result.unwrap_or_default(); + assert!(c.vaults.is_empty()); + } + + #[test] + fn test_vault_dedup() { + let mut c = AppConfig::default(); + c.vaults.retain(|v| v.name != "test"); + c.vaults.push(VaultConfig { name: "test".into(), path: "/first".into() }); + c.vaults.retain(|v| v.name != "test"); + c.vaults.push(VaultConfig { name: "test".into(), path: "/second".into() }); + assert_eq!(c.vaults.len(), 1); + assert_eq!(c.vaults[0].path, "/second"); + } + + #[test] + fn test_dir_remove() { + let mut c = AppConfig::default(); + c.dirs.push(DirConfig { name: "a".into(), path: "/a".into() }); + c.dirs.push(DirConfig { name: "b".into(), path: "/b".into() }); + c.dirs.retain(|d| d.name != "a"); + assert_eq!(c.dirs.len(), 1); + assert_eq!(c.dirs[0].name, "b"); + } + + #[test] + fn test_backend_url() { + assert_eq!(get_backend_url(), "http://127.0.0.1:17890"); + } + + #[test] + fn test_about_msg_contains_branding() { + assert!(ABOUT_MSG.contains("ObsiGate Desktop")); + assert!(ABOUT_MSG.contains("Bruno Charest")); + assert!(ABOUT_MSG.contains("Tauri")); + } + + #[test] + fn test_app_data_dir_nonempty() { + let dir = app_data_dir(); + assert!(!dir.to_string_lossy().is_empty()); + } + + #[test] + fn test_logs_dir_under_app_data() { + let logs = logs_dir(); + assert!(logs.starts_with(app_data_dir())); + assert_eq!(logs.file_name().unwrap(), "logs"); + } + + #[test] + fn test_backend_log_path() { + let p = backend_log_path(); + assert_eq!(p.file_name().unwrap(), "backend.log"); + } + + #[test] + fn test_json_format_keys() { + let c = AppConfig::default(); + let json = serde_json::to_string(&c).unwrap(); + assert!(json.contains("\"vaults\"")); + assert!(json.contains("\"dirs\"")); + assert!(json.contains("\"window_width\"")); + } + + #[test] + fn test_backend_health_check_url() { + // Verify the health check targets the correct endpoint + let url = "http://127.0.0.1:17890/api/health"; + assert!(url.contains("17890")); + assert!(url.contains("/api/health")); + } + + #[test] + fn test_vault_config_clone() { + let v = VaultConfig { name: "test".into(), path: "/t".into() }; + let v2 = v.clone(); + assert_eq!(v.name, v2.name); + assert_eq!(v.path, v2.path); + } + + #[test] + fn test_dir_config_debug() { + let d = DirConfig { name: "x".into(), path: "/x".into() }; + let dbg = format!("{:?}", d); + assert!(dbg.contains("x")); + } +}