use crate::browser_version_manager::{BrowserVersionInfo, BrowserVersionManager}; use crate::profile::{BrowserProfile, ProfileManager}; use crate::settings_manager::SettingsManager; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::fs; use std::path::PathBuf; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UpdateNotification { pub id: String, pub browser: String, pub current_version: String, pub new_version: String, pub affected_profiles: Vec, pub is_stable_update: bool, pub timestamp: u64, } #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct AutoUpdateState { pub pending_updates: Vec, pub disabled_browsers: HashSet, // browsers disabled during update #[serde(default)] pub auto_update_downloads: HashSet, // track auto-update downloads for toast suppression pub last_check_timestamp: u64, } pub struct AutoUpdater { browser_version_manager: &'static BrowserVersionManager, settings_manager: &'static SettingsManager, profile_manager: &'static ProfileManager, } impl AutoUpdater { fn new() -> Self { Self { browser_version_manager: BrowserVersionManager::instance(), settings_manager: SettingsManager::instance(), profile_manager: ProfileManager::instance(), } } pub fn instance() -> &'static AutoUpdater { &AUTO_UPDATER } /// Check for updates for all profiles pub async fn check_for_updates( &self, ) -> Result, Box> { let mut notifications = Vec::new(); let mut browser_versions: HashMap> = HashMap::new(); // Group profiles by browser let profiles = self .profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; let mut browser_profiles: HashMap> = HashMap::new(); for profile in profiles { if profile.is_cross_os() { continue; } // Only check supported browsers if !self .browser_version_manager .is_browser_supported(&profile.browser) .unwrap_or(false) { continue; } browser_profiles .entry(profile.browser.clone()) .or_default() .push(profile); } for (browser, profiles) in browser_profiles { // Always fetch fresh versions for update checks — stale cache would miss new releases let versions = match self .browser_version_manager .fetch_browser_versions_detailed(&browser, false) .await { Ok(versions) => versions, Err(e) => { log::warn!("Failed to fetch versions for {browser}: {e}, trying cache"); // Fall back to cache if network fails if let Some(cached) = self .browser_version_manager .get_cached_browser_versions_detailed(&browser) { cached } else { continue; } } }; browser_versions.insert(browser.clone(), versions.clone()); // Check each profile for updates for profile in profiles { if let Some(update) = self.check_profile_update(&profile, &versions)? { notifications.push(update); } } } Ok(notifications) } pub async fn check_for_updates_with_progress(&self, app_handle: &tauri::AppHandle) { log::info!("Starting auto-update check with progress..."); // Browser auto-updates are always enabled — the disable_auto_updates setting // only controls app self-updates, not browser version updates. // Check for browser updates and trigger auto-downloads match self.check_for_updates().await { Ok(update_notifications) => { // Group by browser+version to avoid duplicate downloads let grouped = self.group_update_notifications(update_notifications); if !grouped.is_empty() { log::info!("Found {} browser updates", grouped.len()); for notification in grouped { log::info!( "Auto-updating {} to version {} ({} profiles)", notification.browser, notification.new_version, notification.affected_profiles.len() ); let browser = notification.browser.clone(); let new_version = notification.new_version.clone(); let app_handle_clone = app_handle.clone(); // Spawn async task to handle the download and auto-update tokio::spawn(async move { let registry = crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(); // Skip if this browser-version pair is already being downloaded if crate::downloader::is_downloading(&browser, &new_version) { log::info!( "Browser {browser} {new_version} is already being downloaded, skipping duplicate" ); return; } if registry.is_browser_downloaded(&browser, &new_version) { log::info!("Browser {browser} {new_version} already downloaded, proceeding to auto-update profiles"); // Browser already exists, go straight to profile update match AutoUpdater::instance() .auto_update_profile_versions(&app_handle_clone, &browser, &new_version) .await { Ok(updated_profiles) => { if !updated_profiles.is_empty() { log::info!( "Auto-updated {} profiles to {browser} {new_version}: {:?}", updated_profiles.len(), updated_profiles ); } } Err(e) => { log::error!("Failed to auto-update profiles for {browser}: {e}"); } } } else { log::info!("Downloading browser {browser} version {new_version}..."); // Download directly from Rust — download_browser_full already // auto-updates non-running profiles after successful download. match crate::downloader::download_browser( app_handle_clone, browser.clone(), new_version.clone(), ) .await { Ok(actual_version) => { log::info!("Auto-download completed for {browser} {actual_version}"); } Err(e) => { log::error!("Failed to auto-download {browser} {new_version}: {e}"); } } } }); } } else { log::info!("No browser updates needed"); } } Err(e) => { log::error!("Failed to check for browser updates: {e}"); } } // Also update any profiles that can be bumped to an already-installed newer version. // This handles cases where a version was downloaded but profiles weren't updated // (e.g., they were running at the time, or the update was missed). match self.update_profiles_to_latest_installed(app_handle) { Ok(updated) => { if !updated.is_empty() { log::info!( "Updated {} profiles to latest installed versions: {:?}", updated.len(), updated ); } } Err(e) => { log::error!("Failed to update profiles to latest installed versions: {e}"); } } } /// Check if a specific profile has an available update fn check_profile_update( &self, profile: &BrowserProfile, available_versions: &[BrowserVersionInfo], ) -> Result, Box> { let current_version = &profile.version; let is_current_nightly = crate::api_client::is_browser_version_nightly(&profile.browser, current_version, None); // Find the best available update let best_update = available_versions .iter() .filter(|v| { // Only consider versions newer than current self.is_version_newer(&v.version, current_version) && crate::api_client::is_browser_version_nightly(&profile.browser, &v.version, None) == is_current_nightly }) .max_by(|a, b| self.compare_versions(&a.version, &b.version)); if let Some(update_version) = best_update { let notification = UpdateNotification { id: format!( "{}_{}_to_{}", profile.browser, current_version, update_version.version ), browser: profile.browser.clone(), current_version: current_version.clone(), new_version: update_version.version.clone(), affected_profiles: vec![profile.name.clone()], is_stable_update: !update_version.is_prerelease, timestamp: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(), }; Ok(Some(notification)) } else { Ok(None) } } /// Group update notifications by browser and version pub fn group_update_notifications( &self, notifications: Vec, ) -> Vec { let mut grouped: HashMap = HashMap::new(); for notification in notifications { let key = format!("{}_{}", notification.browser, notification.new_version); if let Some(existing) = grouped.get_mut(&key) { // Merge affected profiles existing .affected_profiles .extend(notification.affected_profiles); existing.affected_profiles.sort(); existing.affected_profiles.dedup(); } else { grouped.insert(key, notification); } } let mut result: Vec = grouped.into_values().collect(); // Sort by priority: stable updates first, then by timestamp result.sort_by(|a, b| match (a.is_stable_update, b.is_stable_update) { (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, _ => b.timestamp.cmp(&a.timestamp), }); result } /// Automatically update all affected profile versions after browser download pub async fn auto_update_profile_versions( &self, app_handle: &tauri::AppHandle, browser: &str, new_version: &str, ) -> Result, Box> { let profiles = self .profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; let mut updated_profiles = Vec::new(); // Find all profiles for this browser that should be updated for profile in profiles { if profile.browser == browser { if profile.is_cross_os() { continue; } // Check if profile is currently running if profile.process_id.is_some() { // Store as pending update so it gets applied when browser closes log::info!( "Profile {} is running, storing pending update {} -> {}", profile.name, profile.version, new_version ); let mut state = self.load_auto_update_state().unwrap_or_default(); let notification = UpdateNotification { id: format!("{}_{}_to_{}", browser, profile.version, new_version), browser: browser.to_string(), current_version: profile.version.clone(), new_version: new_version.to_string(), affected_profiles: vec![profile.name.clone()], is_stable_update: true, timestamp: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs(), }; // Add if not already pending if !state .pending_updates .iter() .any(|u| u.id == notification.id) { state.pending_updates.push(notification); let _ = self.save_auto_update_state(&state); } continue; } // Check if this is an update (newer version) if self.is_version_newer(new_version, &profile.version) { // Update the profile version match self.profile_manager.update_profile_version( app_handle, &profile.id.to_string(), new_version, ) { Ok(_) => { updated_profiles.push(profile.name); } Err(e) => { log::error!("Failed to update profile {}: {}", profile.name, e); } } } } } Ok(updated_profiles) } /// Complete browser update process with auto-update of profile versions pub async fn complete_browser_update_with_auto_update( &self, app_handle: &tauri::AppHandle, browser: &str, new_version: &str, ) -> Result, Box> { // Auto-update profile versions first let updated_profiles = self .auto_update_profile_versions(app_handle, browser, new_version) .await?; // Remove browser from disabled list and clean up auto-update tracking let mut state = self.load_auto_update_state()?; state.disabled_browsers.remove(browser); let download_key = format!("{browser}-{new_version}"); state.auto_update_downloads.remove(&download_key); self.save_auto_update_state(&state)?; Ok(updated_profiles) } /// Dismiss update notification pub fn dismiss_update_notification( &self, notification_id: &str, ) -> Result<(), Box> { let mut state = self.load_auto_update_state()?; state.pending_updates.retain(|n| n.id != notification_id); self.save_auto_update_state(&state)?; Ok(()) } fn is_version_newer(&self, version1: &str, version2: &str) -> bool { crate::api_client::is_version_newer(version1, version2) } fn compare_versions(&self, version1: &str, version2: &str) -> std::cmp::Ordering { crate::api_client::compare_versions(version1, version2) } fn get_auto_update_state_file(&self) -> PathBuf { self .settings_manager .get_settings_dir() .join("auto_update_state.json") } pub fn load_auto_update_state( &self, ) -> Result> { let state_file = self.get_auto_update_state_file(); if !state_file.exists() { return Ok(AutoUpdateState::default()); } let content = fs::read_to_string(state_file)?; let state: AutoUpdateState = serde_json::from_str(&content)?; Ok(state) } pub fn save_auto_update_state( &self, state: &AutoUpdateState, ) -> Result<(), Box> { let settings_dir = self.settings_manager.get_settings_dir(); std::fs::create_dir_all(&settings_dir)?; let state_file = self.get_auto_update_state_file(); let json = serde_json::to_string_pretty(state)?; fs::write(state_file, json)?; Ok(()) } /// Get pending update versions for a specific browser /// Returns a set of (browser, version) pairs that have pending updates pub fn get_pending_update_versions( &self, ) -> Result, Box> { let state = self.load_auto_update_state()?; let mut pending_versions = std::collections::HashSet::new(); for update in &state.pending_updates { pending_versions.insert((update.browser.clone(), update.new_version.clone())); } Ok(pending_versions) } /// Get pending update for a specific browser version if it exists pub fn get_pending_update( &self, browser: &str, current_version: &str, ) -> Result, Box> { let state = self.load_auto_update_state()?; for update in &state.pending_updates { if update.browser == browser && update.current_version == current_version { return Ok(Some(update.clone())); } } Ok(None) } /// Get the latest installed version for a browser from the downloaded browsers registry pub fn get_latest_installed_version(&self, browser: &str) -> Option { let registry = crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(); let versions = registry.get_downloaded_versions(browser); versions .into_iter() .filter(|v| registry.is_browser_downloaded(browser, v)) .max_by(|a, b| self.compare_versions(a, b)) } /// Update a single profile to the latest installed version for its browser. /// Used when a browser closes to ensure it's on the latest version. pub fn update_profile_to_latest_installed( &self, app_handle: &tauri::AppHandle, profile: &crate::profile::BrowserProfile, ) -> Option { let latest = self.get_latest_installed_version(&profile.browser)?; if !self.is_version_newer(&latest, &profile.version) { return None; } // Only update stable->stable and nightly->nightly let is_profile_nightly = crate::api_client::is_browser_version_nightly(&profile.browser, &profile.version, None); let is_latest_nightly = crate::api_client::is_browser_version_nightly(&profile.browser, &latest, None); if is_profile_nightly != is_latest_nightly { return None; } match self .profile_manager .update_profile_version(app_handle, &profile.id.to_string(), &latest) { Ok(updated) => { log::info!( "Updated profile {} from {} {} to latest installed version {}", profile.name, profile.browser, profile.version, latest ); Some(updated) } Err(e) => { log::error!( "Failed to update profile {} to latest installed version: {e}", profile.name ); None } } } /// Update all non-running profiles to the latest installed version for each browser. /// Handles the case where a newer version was downloaded but profiles weren't updated. pub fn update_profiles_to_latest_installed( &self, app_handle: &tauri::AppHandle, ) -> Result, Box> { let registry = crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(); let profiles = self .profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; let mut all_updated = Vec::new(); // Group profiles by browser let mut browser_profiles: HashMap> = HashMap::new(); for profile in profiles { if profile.is_cross_os() { continue; } browser_profiles .entry(profile.browser.clone()) .or_default() .push(profile); } for (browser, profiles) in browser_profiles { let installed_versions = registry.get_downloaded_versions(&browser); if installed_versions.is_empty() { continue; } // Find the latest installed version that actually exists on disk let latest_installed = installed_versions .iter() .filter(|v| registry.is_browser_downloaded(&browser, v)) .max_by(|a, b| self.compare_versions(a, b)); let latest_version = match latest_installed { Some(v) => v.clone(), None => continue, }; for profile in profiles { if profile.process_id.is_some() { continue; } if !self.is_version_newer(&latest_version, &profile.version) { continue; } // Only update stable->stable and nightly->nightly let is_profile_nightly = crate::api_client::is_browser_version_nightly(&browser, &profile.version, None); let is_latest_nightly = crate::api_client::is_browser_version_nightly(&browser, &latest_version, None); if is_profile_nightly != is_latest_nightly { continue; } match self.profile_manager.update_profile_version( app_handle, &profile.id.to_string(), &latest_version, ) { Ok(_) => { log::info!( "Updated profile {} from {} {} to latest installed version {}", profile.name, browser, profile.version, latest_version ); all_updated.push(profile.name); } Err(e) => { log::error!("Failed to update profile {}: {e}", profile.name); } } } } Ok(all_updated) } } // Tauri commands #[tauri::command] pub async fn check_for_browser_updates() -> Result, String> { let updater = AutoUpdater::instance(); let notifications = updater .check_for_updates() .await .map_err(|e| format!("Failed to check for updates: {e}"))?; let grouped = updater.group_update_notifications(notifications); Ok(grouped) } #[tauri::command] pub async fn dismiss_update_notification(notification_id: String) -> Result<(), String> { let updater = AutoUpdater::instance(); updater .dismiss_update_notification(¬ification_id) .map_err(|e| format!("Failed to dismiss notification: {e}")) } #[tauri::command] pub async fn complete_browser_update_with_auto_update( app_handle: tauri::AppHandle, browser: String, new_version: String, ) -> Result, String> { let updater = AutoUpdater::instance(); updater .complete_browser_update_with_auto_update(&app_handle, &browser, &new_version) .await .map_err(|e| format!("Failed to complete browser update: {e}")) } #[tauri::command] pub async fn check_for_updates_with_progress(app_handle: tauri::AppHandle) { let updater = AutoUpdater::instance(); updater.check_for_updates_with_progress(&app_handle).await; } #[cfg(test)] mod tests { use super::*; fn create_test_profile(name: &str, browser: &str, version: &str) -> BrowserProfile { BrowserProfile { id: uuid::Uuid::new_v4(), name: name.to_string(), browser: browser.to_string(), version: version.to_string(), process_id: None, proxy_id: None, vpn_id: None, launch_hook: None, last_launch: None, release_type: "stable".to_string(), camoufox_config: None, wayfern_config: None, group_id: None, tags: Vec::new(), note: None, sync_mode: crate::profile::types::SyncMode::Disabled, encryption_salt: None, last_sync: None, host_os: None, ephemeral: false, extension_group_id: None, proxy_bypass_rules: Vec::new(), created_by_id: None, created_by_email: None, dns_blocklist: None, password_protected: false, created_at: None, } } fn create_test_version_info(version: &str, is_prerelease: bool) -> BrowserVersionInfo { BrowserVersionInfo { version: version.to_string(), is_prerelease, date: "2024-01-01".to_string(), } } #[test] fn test_compare_versions() { let updater = AutoUpdater::instance(); assert_eq!( updater.compare_versions("1.0.0", "1.0.0"), std::cmp::Ordering::Equal ); assert_eq!( updater.compare_versions("1.0.1", "1.0.0"), std::cmp::Ordering::Greater ); assert_eq!( updater.compare_versions("1.0.0", "1.0.1"), std::cmp::Ordering::Less ); assert_eq!( updater.compare_versions("2.0.0", "1.9.9"), std::cmp::Ordering::Greater ); assert_eq!( updater.compare_versions("1.10.0", "1.9.0"), std::cmp::Ordering::Greater ); } #[test] fn test_is_version_newer() { let updater = AutoUpdater::instance(); assert!(updater.is_version_newer("1.0.1", "1.0.0")); assert!(updater.is_version_newer("2.0.0", "1.9.9")); assert!(!updater.is_version_newer("1.0.0", "1.0.1")); assert!(!updater.is_version_newer("1.0.0", "1.0.0")); } #[test] fn test_camoufox_beta_version_comparison() { let updater = AutoUpdater::instance(); // Test the exact user-reported scenario: 135.0.1beta24 vs 135.0beta22 assert!( updater.is_version_newer("135.0.1beta24", "135.0beta22"), "135.0.1beta24 should be newer than 135.0beta22" ); assert_eq!( updater.compare_versions("135.0.1beta24", "135.0beta22"), std::cmp::Ordering::Greater, "135.0.1beta24 should compare as greater than 135.0beta22" ); // Test other camoufox beta version combinations assert!( updater.is_version_newer("135.0.5beta24", "135.0.5beta22"), "135.0.5beta24 should be newer than 135.0.5beta22" ); assert!( updater.is_version_newer("135.0.1beta1", "135.0beta1"), "135.0.1beta1 should be newer than 135.0beta1 due to patch version" ); // Test that older versions are not considered newer assert!( !updater.is_version_newer("135.0beta22", "135.0.1beta24"), "135.0beta22 should NOT be newer than 135.0.1beta24" ); } #[test] fn test_beta_version_ordering_comprehensive() { let updater = AutoUpdater::instance(); // Test various beta version patterns that could appear in camoufox let test_cases = vec![ ("135.0.1beta24", "135.0beta22", true), // User reported case ("135.0.5beta24", "135.0.5beta22", true), // Same patch, different beta ("135.1beta1", "135.0beta99", true), // Higher minor beats beta number ("136.0beta1", "135.9.9beta99", true), // Higher major beats everything ("135.0.1beta1", "135.0beta1", true), // Patch version matters ("135.0beta22", "135.0.1beta24", false), // Reverse of user case ]; for (newer, older, should_be_newer) in test_cases { let result = updater.is_version_newer(newer, older); assert_eq!( result, should_be_newer, "Expected {} {} {} but got {}", newer, if should_be_newer { ">" } else { "<=" }, older, if result { "true" } else { "false" } ); } } #[test] fn test_check_profile_update_stable_to_stable() { let updater = AutoUpdater::instance(); let profile = create_test_profile("test", "firefox", "1.0.0"); let versions = vec![ create_test_version_info("1.0.1", false), // stable, newer create_test_version_info("1.1.0-alpha", true), // alpha, should be ignored create_test_version_info("0.9.0", false), // stable, older ]; let result = updater.check_profile_update(&profile, &versions).unwrap(); assert!(result.is_some()); let update = result.unwrap(); assert_eq!(update.new_version, "1.0.1"); assert!(update.is_stable_update); } #[test] fn test_check_profile_update_alpha_to_alpha() { let updater = AutoUpdater::instance(); let profile = create_test_profile("test", "firefox", "1.0.0-alpha"); let versions = vec![ create_test_version_info("1.0.1", false), // stable, should be included create_test_version_info("1.1.0-alpha", true), // alpha, newer create_test_version_info("0.9.0-alpha", true), // alpha, older ]; let result = updater.check_profile_update(&profile, &versions).unwrap(); assert!(result.is_some()); let update = result.unwrap(); // Should pick the newest version (alpha user can upgrade to stable or newer alpha) assert_eq!(update.new_version, "1.1.0-alpha"); assert!(!update.is_stable_update); } #[test] fn test_check_profile_update_no_update_available() { let updater = AutoUpdater::instance(); let profile = create_test_profile("test", "firefox", "1.0.0"); let versions = vec![ create_test_version_info("0.9.0", false), // older create_test_version_info("1.0.0", false), // same version ]; let result = updater.check_profile_update(&profile, &versions).unwrap(); assert!(result.is_none()); } #[test] fn test_group_update_notifications() { let updater = AutoUpdater::instance(); let notifications = vec![ UpdateNotification { id: "firefox_1.0.0_to_1.1.0_profile1".to_string(), browser: "firefox".to_string(), current_version: "1.0.0".to_string(), new_version: "1.1.0".to_string(), affected_profiles: vec!["profile1".to_string()], is_stable_update: true, timestamp: 1000, }, UpdateNotification { id: "firefox_1.0.0_to_1.1.0_profile2".to_string(), browser: "firefox".to_string(), current_version: "1.0.0".to_string(), new_version: "1.1.0".to_string(), affected_profiles: vec!["profile2".to_string()], is_stable_update: true, timestamp: 1001, }, UpdateNotification { id: "chrome_1.0.0_to_1.1.0-alpha".to_string(), browser: "chrome".to_string(), current_version: "1.0.0".to_string(), new_version: "1.1.0-alpha".to_string(), affected_profiles: vec!["profile3".to_string()], is_stable_update: false, timestamp: 1002, }, ]; let grouped = updater.group_update_notifications(notifications); assert_eq!(grouped.len(), 2); // Find the Firefox notification let firefox_notification = grouped.iter().find(|n| n.browser == "firefox").unwrap(); assert_eq!(firefox_notification.affected_profiles.len(), 2); assert!(firefox_notification .affected_profiles .contains(&"profile1".to_string())); assert!(firefox_notification .affected_profiles .contains(&"profile2".to_string())); // Stable updates should come first assert!(grouped[0].is_stable_update); } #[test] fn test_auto_update_state_persistence() { use std::sync::Once; use tempfile::TempDir; static INIT: Once = Once::new(); INIT.call_once(|| { // Initialize any required static data }); // Create a temporary directory for testing let temp_dir = TempDir::new().unwrap(); // Create a mock settings manager that uses the temp directory struct TestSettingsManager { settings_dir: std::path::PathBuf, } impl TestSettingsManager { fn new(settings_dir: std::path::PathBuf) -> Self { Self { settings_dir } } fn get_settings_dir(&self) -> std::path::PathBuf { self.settings_dir.clone() } } let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); let mut state = AutoUpdateState::default(); state.disabled_browsers.insert("firefox".to_string()); state .auto_update_downloads .insert("firefox-1.1.0".to_string()); state.pending_updates.push(UpdateNotification { id: "test".to_string(), browser: "firefox".to_string(), current_version: "1.0.0".to_string(), new_version: "1.1.0".to_string(), affected_profiles: vec!["profile1".to_string()], is_stable_update: true, timestamp: 1000, }); // Test save and load let state_file = test_settings_manager .get_settings_dir() .join("auto_update_state.json"); std::fs::create_dir_all(test_settings_manager.get_settings_dir()) .expect("Failed to create settings directory"); let json = serde_json::to_string_pretty(&state).expect("Failed to serialize state"); std::fs::write(&state_file, json).expect("Failed to write state file"); // Load state let content = std::fs::read_to_string(&state_file).expect("Failed to read state file"); let loaded_state: AutoUpdateState = serde_json::from_str(&content).expect("Failed to deserialize state"); assert_eq!(loaded_state.disabled_browsers.len(), 1); assert!(loaded_state.disabled_browsers.contains("firefox")); assert_eq!(loaded_state.auto_update_downloads.len(), 1); assert!(loaded_state.auto_update_downloads.contains("firefox-1.1.0")); assert_eq!(loaded_state.pending_updates.len(), 1); assert_eq!(loaded_state.pending_updates[0].id, "test"); } #[tokio::test] async fn test_browser_disable_enable_cycle() { use tempfile::TempDir; // Create a temporary directory for testing let temp_dir = TempDir::new().unwrap(); // Create a mock settings manager that uses the temp directory struct TestSettingsManager { settings_dir: std::path::PathBuf, } impl TestSettingsManager { fn new(settings_dir: std::path::PathBuf) -> Self { Self { settings_dir } } fn get_settings_dir(&self) -> std::path::PathBuf { self.settings_dir.clone() } } let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); // Test browser disable/enable cycle with manual state management let state_file = test_settings_manager .get_settings_dir() .join("auto_update_state.json"); std::fs::create_dir_all(test_settings_manager.get_settings_dir()) .expect("Failed to create settings directory"); // Initially not disabled (empty state file means default state) let state = AutoUpdateState::default(); assert!( !state.disabled_browsers.contains("firefox"), "Firefox should not be disabled initially" ); // Start update (should disable) let mut state = AutoUpdateState::default(); state.disabled_browsers.insert("firefox".to_string()); state .auto_update_downloads .insert("firefox-1.1.0".to_string()); let json = serde_json::to_string_pretty(&state).expect("Failed to serialize state"); std::fs::write(&state_file, json).expect("Failed to write state file"); // Check that it's disabled let content = std::fs::read_to_string(&state_file).expect("Failed to read state file"); let loaded_state: AutoUpdateState = serde_json::from_str(&content).expect("Failed to deserialize state"); assert!( loaded_state.disabled_browsers.contains("firefox"), "Firefox should be disabled" ); assert!( loaded_state.auto_update_downloads.contains("firefox-1.1.0"), "Firefox download should be tracked" ); // Complete update (should enable) let mut state = loaded_state; state.disabled_browsers.remove("firefox"); state.auto_update_downloads.remove("firefox-1.1.0"); let json = serde_json::to_string_pretty(&state).expect("Failed to serialize final state"); std::fs::write(&state_file, json).expect("Failed to write final state file"); // Check that it's enabled again let content = std::fs::read_to_string(&state_file).expect("Failed to read final state file"); let final_state: AutoUpdateState = serde_json::from_str(&content).expect("Failed to deserialize final state"); assert!( !final_state.disabled_browsers.contains("firefox"), "Firefox should be enabled again" ); assert!( !final_state.auto_update_downloads.contains("firefox-1.1.0"), "Firefox download should not be tracked anymore" ); } #[test] fn test_dismiss_update_notification() { use tempfile::TempDir; // Create a temporary directory for testing let temp_dir = TempDir::new().unwrap(); // Create a mock settings manager that uses the temp directory struct TestSettingsManager { settings_dir: std::path::PathBuf, } impl TestSettingsManager { fn new(settings_dir: std::path::PathBuf) -> Self { Self { settings_dir } } fn get_settings_dir(&self) -> std::path::PathBuf { self.settings_dir.clone() } } let test_settings_manager = TestSettingsManager::new(temp_dir.path().to_path_buf()); let mut state = AutoUpdateState::default(); state.pending_updates.push(UpdateNotification { id: "test_notification".to_string(), browser: "firefox".to_string(), current_version: "1.0.0".to_string(), new_version: "1.1.0".to_string(), affected_profiles: vec!["profile1".to_string()], is_stable_update: true, timestamp: 1000, }); // Save initial state let state_file = test_settings_manager .get_settings_dir() .join("auto_update_state.json"); std::fs::create_dir_all(test_settings_manager.get_settings_dir()) .expect("Failed to create settings directory"); let json = serde_json::to_string_pretty(&state).expect("Failed to serialize initial state"); std::fs::write(&state_file, json).expect("Failed to write initial state file"); // Dismiss notification (remove from pending updates) state .pending_updates .retain(|n| n.id != "test_notification"); let json = serde_json::to_string_pretty(&state).expect("Failed to serialize updated state"); std::fs::write(&state_file, json).expect("Failed to write updated state file"); // Check that it's removed let content = std::fs::read_to_string(&state_file).expect("Failed to read updated state file"); let loaded_state: AutoUpdateState = serde_json::from_str(&content).expect("Failed to deserialize updated state"); assert_eq!( loaded_state.pending_updates.len(), 0, "Pending updates should be empty after dismissal" ); } } // Global singleton instance lazy_static::lazy_static! { static ref AUTO_UPDATER: AutoUpdater = AutoUpdater::new(); }