refactor: partially migrate to singleton pattern

This commit is contained in:
zhom
2025-07-31 22:29:08 +04:00
parent 4997854577
commit 5b31cfaf32
9 changed files with 131 additions and 1032 deletions
+10 -1
View File
@@ -315,7 +315,7 @@ pub struct ApiClient {
}
impl ApiClient {
pub fn new() -> Self {
fn new() -> Self {
Self {
client: Client::new(),
firefox_api_base: "https://product-details.mozilla.org/1.0".to_string(),
@@ -327,6 +327,10 @@ impl ApiClient {
}
}
pub fn instance() -> &'static ApiClient {
&API_CLIENT
}
#[cfg(test)]
pub fn new_with_base_urls(
firefox_api_base: String,
@@ -1336,6 +1340,11 @@ impl ApiClient {
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref API_CLIENT: ApiClient = ApiClient::new();
}
#[cfg(test)]
mod tests {
use super::*;
+20 -11
View File
@@ -49,12 +49,16 @@ pub struct AppAutoUpdater {
}
impl AppAutoUpdater {
pub fn new() -> Self {
fn new() -> Self {
Self {
client: Client::new(),
}
}
pub fn instance() -> &'static AppAutoUpdater {
&APP_AUTO_UPDATER
}
/// Check if running a nightly build based on environment variable
pub fn is_nightly_build() -> bool {
// If STABLE_RELEASE env var is set at compile time, it's a stable build
@@ -971,7 +975,7 @@ rm "{}"
#[tauri::command]
pub async fn check_for_app_updates() -> Result<Option<AppUpdateInfo>, String> {
let updater = AppAutoUpdater::new();
let updater = AppAutoUpdater::instance();
updater
.check_for_updates()
.await
@@ -983,7 +987,7 @@ pub async fn download_and_install_app_update(
app_handle: tauri::AppHandle,
update_info: AppUpdateInfo,
) -> Result<(), String> {
let updater = AppAutoUpdater::new();
let updater = AppAutoUpdater::instance();
updater
.download_and_install_update(&app_handle, &update_info)
.await
@@ -993,7 +997,7 @@ pub async fn download_and_install_app_update(
#[tauri::command]
pub async fn check_for_app_updates_manual() -> Result<Option<AppUpdateInfo>, String> {
println!("Manual app update check triggered");
let updater = AppAutoUpdater::new();
let updater = AppAutoUpdater::instance();
updater
.check_for_updates()
.await
@@ -1017,7 +1021,7 @@ mod tests {
#[test]
fn test_version_comparison() {
let updater = AppAutoUpdater::new();
let updater = AppAutoUpdater::instance();
// Test semantic version comparison
assert!(updater.is_version_newer("v1.1.0", "v1.0.0"));
@@ -1029,7 +1033,7 @@ mod tests {
#[test]
fn test_parse_semver() {
let updater = AppAutoUpdater::new();
let updater = AppAutoUpdater::instance();
assert_eq!(updater.parse_semver("v1.2.3"), (1, 2, 3));
assert_eq!(updater.parse_semver("1.2.3"), (1, 2, 3));
@@ -1039,7 +1043,7 @@ mod tests {
#[test]
fn test_should_update_stable() {
let updater = AppAutoUpdater::new();
let updater = AppAutoUpdater::instance();
// Stable version updates
assert!(updater.should_update("v1.0.0", "v1.1.0", false));
@@ -1050,7 +1054,7 @@ mod tests {
#[test]
fn test_should_update_nightly() {
let updater = AppAutoUpdater::new();
let updater = AppAutoUpdater::instance();
// Nightly version updates
assert!(updater.should_update("nightly-abc123", "nightly-def456", true));
@@ -1067,7 +1071,7 @@ mod tests {
#[test]
fn test_should_update_edge_cases() {
let updater = AppAutoUpdater::new();
let updater = AppAutoUpdater::instance();
// Test with different nightly formats
assert!(updater.should_update("nightly-abc123", "nightly-def456", true));
@@ -1085,7 +1089,7 @@ mod tests {
#[test]
fn test_get_download_url_for_platform() {
let updater = AppAutoUpdater::new();
let updater = AppAutoUpdater::instance();
let assets = vec![
AppReleaseAsset {
@@ -1140,7 +1144,7 @@ mod tests {
// This test verifies that the extract_update method properly uses the Extractor
// We can't run the actual extraction in unit tests without real DMG files,
// but we can verify the method signature and basic logic
let updater = AppAutoUpdater::new();
let updater = AppAutoUpdater::instance();
// Test that unsupported formats would be rejected
let temp_dir = std::env::temp_dir();
@@ -1159,3 +1163,8 @@ mod tests {
assert!(error_msg.contains("Unsupported archive format: rar"));
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref APP_AUTO_UPDATER: AppAutoUpdater = AppAutoUpdater::new();
}
+27 -18
View File
@@ -29,18 +29,22 @@ pub struct AutoUpdateState {
}
pub struct AutoUpdater {
version_service: BrowserVersionService,
settings_manager: SettingsManager,
version_service: &'static BrowserVersionService,
settings_manager: &'static SettingsManager,
}
impl AutoUpdater {
pub fn new() -> Self {
fn new() -> Self {
Self {
version_service: BrowserVersionService::new(),
settings_manager: SettingsManager::new(),
version_service: BrowserVersionService::instance(),
settings_manager: SettingsManager::instance(),
}
}
pub fn instance() -> &'static AutoUpdater {
&AUTO_UPDATER
}
/// Check for updates for all profiles
pub async fn check_for_updates(
&self,
@@ -458,7 +462,7 @@ impl AutoUpdater {
#[tauri::command]
pub async fn check_for_browser_updates() -> Result<Vec<UpdateNotification>, String> {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
let notifications = updater
.check_for_updates()
.await
@@ -469,7 +473,7 @@ pub async fn check_for_browser_updates() -> Result<Vec<UpdateNotification>, Stri
#[tauri::command]
pub async fn is_browser_disabled_for_update(browser: String) -> Result<bool, String> {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
updater
.is_browser_disabled(&browser)
.map_err(|e| format!("Failed to check browser status: {e}"))
@@ -477,7 +481,7 @@ pub async fn is_browser_disabled_for_update(browser: String) -> Result<bool, Str
#[tauri::command]
pub async fn dismiss_update_notification(notification_id: String) -> Result<(), String> {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
updater
.dismiss_update_notification(&notification_id)
.map_err(|e| format!("Failed to dismiss notification: {e}"))
@@ -488,7 +492,7 @@ pub async fn complete_browser_update_with_auto_update(
browser: String,
new_version: String,
) -> Result<Vec<String>, String> {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
updater
.complete_browser_update_with_auto_update(&browser, &new_version)
.await
@@ -497,7 +501,7 @@ pub async fn complete_browser_update_with_auto_update(
#[tauri::command]
pub async fn check_for_updates_with_progress(app_handle: tauri::AppHandle) {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
updater.check_for_updates_with_progress(&app_handle).await;
}
@@ -530,7 +534,7 @@ mod tests {
#[test]
fn test_compare_versions() {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
assert_eq!(
updater.compare_versions("1.0.0", "1.0.0"),
@@ -556,7 +560,7 @@ mod tests {
#[test]
fn test_is_version_newer() {
let updater = AutoUpdater::new();
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"));
@@ -566,7 +570,7 @@ mod tests {
#[test]
fn test_camoufox_beta_version_comparison() {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
// Test the exact user-reported scenario: 135.0.1beta24 vs 135.0beta22
assert!(
@@ -600,7 +604,7 @@ mod tests {
#[test]
fn test_beta_version_ordering_comprehensive() {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
// Test various beta version patterns that could appear in camoufox
let test_cases = vec![
@@ -628,7 +632,7 @@ mod tests {
#[test]
fn test_check_profile_update_stable_to_stable() {
let updater = AutoUpdater::new();
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
@@ -646,7 +650,7 @@ mod tests {
#[test]
fn test_check_profile_update_alpha_to_alpha() {
let updater = AutoUpdater::new();
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
@@ -665,7 +669,7 @@ mod tests {
#[test]
fn test_check_profile_update_no_update_available() {
let updater = AutoUpdater::new();
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
@@ -678,7 +682,7 @@ mod tests {
#[test]
fn test_group_update_notifications() {
let updater = AutoUpdater::new();
let updater = AutoUpdater::instance();
let notifications = vec![
UpdateNotification {
id: "firefox_1.0.0_to_1.1.0_profile1".to_string(),
@@ -911,3 +915,8 @@ mod tests {
assert_eq!(loaded_state.pending_updates.len(), 0);
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref AUTO_UPDATER: AutoUpdater = AutoUpdater::new();
}
+10 -10
View File
@@ -137,7 +137,7 @@ impl BrowserRunner {
local_proxy_settings: Option<&ProxySettings>,
) -> Result<BrowserProfile, Box<dyn std::error::Error + Send + Sync>> {
// Check if browser is disabled due to ongoing update
let auto_updater = crate::auto_updater::AutoUpdater::new();
let auto_updater = crate::auto_updater::AutoUpdater::instance();
if auto_updater.is_browser_disabled(&profile.browser)? {
return Err(
format!(
@@ -1094,7 +1094,7 @@ impl BrowserRunner {
}
// Check if browser is supported on current platform before attempting download
let version_service = BrowserVersionService::new();
let version_service = BrowserVersionService::instance();
if !version_service
.is_browser_supported(&browser_str)
@@ -1491,13 +1491,13 @@ pub fn delete_profile(_app_handle: tauri::AppHandle, profile_name: String) -> Re
#[tauri::command]
pub fn get_supported_browsers() -> Result<Vec<String>, String> {
let service = BrowserVersionService::new();
let service = BrowserVersionService::instance();
Ok(service.get_supported_browsers())
}
#[tauri::command]
pub fn is_browser_supported_on_platform(browser_str: String) -> Result<bool, String> {
let service = BrowserVersionService::new();
let service = BrowserVersionService::instance();
service
.is_browser_supported(&browser_str)
.map_err(|e| format!("Failed to check browser support: {e}"))
@@ -1507,14 +1507,14 @@ pub fn is_browser_supported_on_platform(browser_str: String) -> Result<bool, Str
pub async fn fetch_browser_versions_cached_first(
browser_str: String,
) -> Result<Vec<BrowserVersionInfo>, String> {
let service = BrowserVersionService::new();
let service = BrowserVersionService::instance();
// Get cached versions immediately if available
if let Some(cached_versions) = service.get_cached_browser_versions_detailed(&browser_str) {
// Check if we should update cache in background
if service.should_update_cache(&browser_str) {
// Start background update but return cached data immediately
let service_clone = BrowserVersionService::new();
let service_clone = BrowserVersionService::instance();
let browser_str_clone = browser_str.clone();
tokio::spawn(async move {
if let Err(e) = service_clone
@@ -1539,14 +1539,14 @@ pub async fn fetch_browser_versions_cached_first(
pub async fn fetch_browser_versions_with_count_cached_first(
browser_str: String,
) -> Result<BrowserVersionsResult, String> {
let service = BrowserVersionService::new();
let service = BrowserVersionService::instance();
// Get cached versions immediately if available
if let Some(cached_versions) = service.get_cached_browser_versions(&browser_str) {
// Check if we should update cache in background
if service.should_update_cache(&browser_str) {
// Start background update but return cached data immediately
let service_clone = BrowserVersionService::new();
let service_clone = BrowserVersionService::instance();
let browser_str_clone = browser_str.clone();
tokio::spawn(async move {
if let Err(e) = service_clone
@@ -1648,7 +1648,7 @@ pub async fn update_camoufox_config(
pub async fn fetch_browser_versions_with_count(
browser_str: String,
) -> Result<BrowserVersionsResult, String> {
let service = BrowserVersionService::new();
let service = BrowserVersionService::instance();
service
.fetch_browser_versions_with_count(&browser_str, false)
.await
@@ -1666,7 +1666,7 @@ pub fn get_downloaded_browser_versions(browser_str: String) -> Result<Vec<String
pub async fn get_browser_release_types(
browser_str: String,
) -> Result<crate::browser_version_service::BrowserReleaseTypes, String> {
let service = BrowserVersionService::new();
let service = BrowserVersionService::instance();
service
.get_browser_release_types(&browser_str)
.await
+37 -632
View File
@@ -30,20 +30,19 @@ pub struct DownloadInfo {
pub is_archive: bool, // true for .dmg, .zip, etc.
}
pub struct BrowserVersionService {
api_client: ApiClient,
}
pub struct BrowserVersionService;
impl BrowserVersionService {
pub fn new() -> Self {
Self {
api_client: ApiClient::new(),
}
fn new() -> Self {
Self
}
#[cfg(test)]
pub fn new_with_api_client(api_client: ApiClient) -> Self {
Self { api_client }
pub fn instance() -> &'static BrowserVersionService {
&BROWSER_VERSION_SERVICE
}
fn api_client(&self) -> &'static ApiClient {
ApiClient::instance()
}
/// Check if a browser is supported on the current platform and architecture
@@ -117,7 +116,7 @@ impl BrowserVersionService {
/// Get cached browser versions immediately (returns None if no cache exists)
pub fn get_cached_browser_versions(&self, browser: &str) -> Option<Vec<String>> {
self.api_client.load_cached_versions(browser)
self.api_client().load_cached_versions(browser)
}
/// Get cached detailed browser version information immediately
@@ -125,7 +124,7 @@ impl BrowserVersionService {
&self,
browser: &str,
) -> Option<Vec<BrowserVersionInfo>> {
let cached_versions = self.api_client.load_cached_versions(browser)?;
let cached_versions = self.api_client().load_cached_versions(browser)?;
// Convert cached versions to detailed info (without dates since cache doesn't store them)
let detailed_info: Vec<BrowserVersionInfo> = cached_versions
@@ -144,7 +143,7 @@ impl BrowserVersionService {
/// Check if cache should be updated (expired or doesn't exist)
pub fn should_update_cache(&self, browser: &str) -> bool {
self.api_client.is_cache_expired(browser)
self.api_client().is_cache_expired(browser)
}
/// Get latest stable and nightly versions for a browser (cached first)
@@ -228,7 +227,7 @@ impl BrowserVersionService {
) -> Result<BrowserVersionsResult, Box<dyn std::error::Error + Send + Sync>> {
// Get existing cached versions to compare and merge
let existing_versions = self
.api_client
.api_client()
.load_cached_versions(browser)
.unwrap_or_default();
let existing_set: HashSet<String> = existing_versions.into_iter().collect();
@@ -265,7 +264,7 @@ impl BrowserVersionService {
// Save the merged cache (unless explicitly bypassing cache)
if !no_caching {
if let Err(e) = self
.api_client
.api_client()
.save_cached_versions(browser, &merged_versions)
{
eprintln!("Failed to save merged cache for {browser}: {e}");
@@ -496,7 +495,7 @@ impl BrowserVersionService {
) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
// Get existing cached versions
let existing_versions = self
.api_client
.api_client()
.load_cached_versions(browser)
.unwrap_or_default();
let existing_set: HashSet<String> = existing_versions.into_iter().collect();
@@ -516,7 +515,10 @@ impl BrowserVersionService {
sort_versions(&mut all_versions);
// Save the updated cache
if let Err(e) = self.api_client.save_cached_versions(browser, &all_versions) {
if let Err(e) = self
.api_client()
.save_cached_versions(browser, &all_versions)
{
eprintln!("Failed to save updated cache for {browser}: {e}");
}
@@ -822,7 +824,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client
.api_client()
.fetch_firefox_releases_with_caching(no_caching)
.await
}
@@ -842,7 +844,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client
.api_client()
.fetch_firefox_developer_releases_with_caching(no_caching)
.await
}
@@ -860,7 +862,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client
.api_client()
.fetch_mullvad_releases_with_caching(no_caching)
.await
}
@@ -884,7 +886,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client
.api_client()
.fetch_zen_releases_with_caching(no_caching)
.await
}
@@ -902,7 +904,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client
.api_client()
.fetch_brave_releases_with_caching(no_caching)
.await
}
@@ -920,7 +922,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client
.api_client()
.fetch_chromium_releases_with_caching(no_caching)
.await
}
@@ -938,7 +940,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client
.api_client()
.fetch_tor_releases_with_caching(no_caching)
.await
}
@@ -956,7 +958,7 @@ impl BrowserVersionService {
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client
.api_client()
.fetch_camoufox_releases_with_caching(no_caching)
.await
}
@@ -965,7 +967,7 @@ impl BrowserVersionService {
#[cfg(test)]
mod tests {
use super::*;
use wiremock::matchers::{method, path, query_param};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn setup_mock_server() -> MockServer {
@@ -983,437 +985,16 @@ mod tests {
)
}
fn create_test_service(api_client: ApiClient) -> BrowserVersionService {
BrowserVersionService::new_with_api_client(api_client)
}
async fn setup_firefox_mocks(server: &MockServer) {
let mock_response = r#"{
"releases": {
"firefox-139.0": {
"build_number": 1,
"category": "major",
"date": "2024-01-15",
"description": "Firefox 139.0 Release",
"is_security_driven": false,
"product": "firefox",
"version": "139.0"
},
"firefox-138.0": {
"build_number": 1,
"category": "major",
"date": "2024-01-01",
"description": "Firefox 138.0 Release",
"is_security_driven": false,
"product": "firefox",
"version": "138.0"
},
"firefox-137.0": {
"build_number": 1,
"category": "major",
"date": "2023-12-15",
"description": "Firefox 137.0 Release",
"is_security_driven": false,
"product": "firefox",
"version": "137.0"
}
}
}"#;
Mock::given(method("GET"))
.and(path("/firefox.json"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(server)
.await;
}
async fn setup_firefox_dev_mocks(server: &MockServer) {
let mock_response = r#"{
"releases": {
"devedition-140.0b1": {
"build_number": 1,
"category": "major",
"date": "2024-01-20",
"description": "Firefox Developer Edition 140.0b1",
"is_security_driven": false,
"product": "devedition",
"version": "140.0b1"
},
"devedition-139.0b5": {
"build_number": 1,
"category": "major",
"date": "2024-01-10",
"description": "Firefox Developer Edition 139.0b5",
"is_security_driven": false,
"product": "devedition",
"version": "139.0b5"
}
}
}"#;
Mock::given(method("GET"))
.and(path("/devedition.json"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(server)
.await;
}
async fn setup_mullvad_mocks(server: &MockServer) {
let mock_response = r#"[
{
"tag_name": "14.5a6",
"name": "Mullvad Browser 14.5a6",
"prerelease": true,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "mullvad-browser-macos-14.5a6.dmg",
"browser_download_url": "https://example.com/mullvad-14.5a6.dmg",
"size": 100000000
}
]
},
{
"tag_name": "14.5a5",
"name": "Mullvad Browser 14.5a5",
"prerelease": true,
"published_at": "2024-01-10T10:00:00Z",
"assets": [
{
"name": "mullvad-browser-macos-14.5a5.dmg",
"browser_download_url": "https://example.com/mullvad-14.5a5.dmg",
"size": 99000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/mullvad/mullvad-browser/releases"))
.and(query_param("per_page", "100"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(server)
.await;
}
async fn setup_zen_mocks(server: &MockServer) {
let mock_response = r#"[
{
"tag_name": "twilight",
"name": "Zen Browser Twilight",
"prerelease": false,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "zen.macos-universal.dmg",
"browser_download_url": "https://example.com/zen-twilight.dmg",
"size": 120000000
}
]
},
{
"tag_name": "1.11b",
"name": "Zen Browser 1.11b",
"prerelease": false,
"published_at": "2024-01-10T10:00:00Z",
"assets": [
{
"name": "zen.macos-universal.dmg",
"browser_download_url": "https://example.com/zen-1.11b.dmg",
"size": 115000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/zen-browser/desktop/releases"))
.and(query_param("per_page", "100"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(server)
.await;
}
async fn setup_brave_mocks(server: &MockServer) {
let mock_response = r#"[
{
"tag_name": "v1.79.119",
"name": "Release v1.79.119 (Chromium 137.0.7151.68)",
"prerelease": false,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "brave-v1.79.119-universal.dmg",
"browser_download_url": "https://example.com/brave-1.79.119-universal.dmg",
"size": 200000000
},
{
"name": "brave-browser-1.79.119-linux-amd64.zip",
"browser_download_url": "https://example.com/brave-browser-1.79.119-linux-amd64.zip",
"size": 150000000
},
{
"name": "brave-browser-1.79.119-linux-arm64.zip",
"browser_download_url": "https://example.com/brave-browser-1.79.119-linux-arm64.zip",
"size": 145000000
}
]
},
{
"tag_name": "v1.81.8",
"name": "Nightly v1.81.8",
"prerelease": false,
"published_at": "2024-01-10T10:00:00Z",
"assets": [
{
"name": "brave-v1.81.8-universal.dmg",
"browser_download_url": "https://example.com/brave-1.81.8-universal.dmg",
"size": 199000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/brave/brave-browser/releases"))
.and(query_param("per_page", "100"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(server)
.await;
}
async fn setup_chromium_mocks(server: &MockServer) {
let arch = if cfg!(target_arch = "aarch64") {
"Mac_Arm"
} else {
"Mac"
};
Mock::given(method("GET"))
.and(path(format!("/{arch}/LAST_CHANGE")))
.respond_with(
ResponseTemplate::new(200)
.set_body_string("1465660")
.insert_header("content-type", "text/plain"),
)
.mount(server)
.await;
}
async fn setup_tor_mocks(server: &MockServer) {
let mock_html = r#"
<html>
<body>
<a href="../">../</a>
<a href="14.0.4/">14.0.4/</a>
<a href="14.0.3/">14.0.3/</a>
<a href="14.0.2/">14.0.2/</a>
</body>
</html>
"#;
let version_html_144 = r#"
<html>
<body>
<a href="tor-browser-macos-14.0.4.dmg">tor-browser-macos-14.0.4.dmg</a>
</body>
</html>
"#;
let version_html_143 = r#"
<html>
<body>
<a href="tor-browser-macos-14.0.3.dmg">tor-browser-macos-14.0.3.dmg</a>
</body>
</html>
"#;
let version_html_142 = r#"
<html>
<body>
<a href="tor-browser-macos-14.0.2.dmg">tor-browser-macos-14.0.2.dmg</a>
</body>
</html>
"#;
Mock::given(method("GET"))
.and(path("/"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_html)
.insert_header("content-type", "text/html"),
)
.mount(server)
.await;
Mock::given(method("GET"))
.and(path("/14.0.4/"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(version_html_144)
.insert_header("content-type", "text/html"),
)
.mount(server)
.await;
Mock::given(method("GET"))
.and(path("/14.0.3/"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(version_html_143)
.insert_header("content-type", "text/html"),
)
.mount(server)
.await;
Mock::given(method("GET"))
.and(path("/14.0.2/"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(version_html_142)
.insert_header("content-type", "text/html"),
)
.mount(server)
.await;
fn create_test_service(_api_client: ApiClient) -> &'static BrowserVersionService {
BrowserVersionService::instance()
}
#[tokio::test]
async fn test_browser_version_service_creation() {
let _ = BrowserVersionService::new();
let _ = BrowserVersionService::instance();
// Test passes if we can create the service without panicking
}
#[tokio::test]
async fn test_fetch_firefox_versions() {
let server = setup_mock_server().await;
setup_firefox_mocks(&server).await;
let api_client = create_test_api_client(&server);
let service = create_test_service(api_client);
// Test with caching
let result_cached = service.fetch_browser_versions("firefox", false).await;
assert!(
result_cached.is_ok(),
"Should fetch Firefox versions with caching"
);
if let Ok(versions) = result_cached {
assert!(!versions.is_empty(), "Should have Firefox versions");
assert_eq!(versions[0], "139.0", "Should have latest version first");
println!(
"Firefox cached test passed. Found {versions_count} versions",
versions_count = versions.len()
);
}
// Test without caching
let result_no_cache = service.fetch_browser_versions("firefox", true).await;
assert!(
result_no_cache.is_ok(),
"Should fetch Firefox versions without caching"
);
if let Ok(versions) = result_no_cache {
assert!(
!versions.is_empty(),
"Should have Firefox versions without caching"
);
assert_eq!(versions[0], "139.0", "Should have latest version first");
println!(
"Firefox no-cache test passed. Found {versions_count} versions",
versions_count = versions.len()
);
}
}
#[tokio::test]
async fn test_fetch_browser_versions_with_count() {
let server = setup_mock_server().await;
setup_firefox_mocks(&server).await;
let api_client = create_test_api_client(&server);
let service = create_test_service(api_client);
let result = service
.fetch_browser_versions_with_count("firefox", false)
.await;
assert!(result.is_ok(), "Should fetch Firefox versions with count");
if let Ok(result) = result {
assert!(!result.versions.is_empty(), "Should have versions");
assert_eq!(
result.total_versions_count,
result.versions.len(),
"Total count should match versions length"
);
assert_eq!(
result.versions[0], "139.0",
"Should have latest version first"
);
println!(
"Firefox count test passed. Found {} versions, new: {}",
result.total_versions_count,
result.new_versions_count.unwrap_or(0)
);
}
}
#[tokio::test]
async fn test_fetch_detailed_versions() {
let server = setup_mock_server().await;
setup_firefox_mocks(&server).await;
let api_client = create_test_api_client(&server);
let service = create_test_service(api_client);
let result = service
.fetch_browser_versions_detailed("firefox", false)
.await;
assert!(result.is_ok(), "Should fetch detailed Firefox versions");
if let Ok(versions) = result {
assert!(!versions.is_empty(), "Should have detailed versions");
// Check that the first version has all required fields
let first_version = &versions[0];
assert!(
!first_version.version.is_empty(),
"Version should not be empty"
);
assert_eq!(
first_version.version, "139.0",
"Should have latest version first"
);
assert_eq!(first_version.date, "2024-01-15", "Should have correct date");
assert!(!first_version.is_prerelease, "Should be stable release");
println!(
"Firefox detailed test passed. Found {versions_count} detailed versions",
versions_count = versions.len()
);
}
}
#[tokio::test]
async fn test_unsupported_browser() {
let server = setup_mock_server().await;
@@ -1434,190 +1015,9 @@ mod tests {
}
}
#[tokio::test]
async fn test_incremental_update() {
let server = setup_mock_server().await;
setup_firefox_mocks(&server).await;
let api_client = create_test_api_client(&server);
let service = create_test_service(api_client);
// This test might fail if there are no cached versions yet, which is fine
let result = service
.update_browser_versions_incrementally("firefox")
.await;
// The test should complete without panicking
match result {
Ok(count) => {
println!("Incremental update test passed. Found {count} new versions");
}
Err(e) => {
println!("Incremental update test failed (expected for first run): {e}");
}
}
}
#[tokio::test]
async fn test_all_supported_browsers() {
let server = setup_mock_server().await;
// Setup all browser mocks
setup_firefox_mocks(&server).await;
setup_firefox_dev_mocks(&server).await;
setup_mullvad_mocks(&server).await;
setup_zen_mocks(&server).await;
setup_brave_mocks(&server).await;
setup_chromium_mocks(&server).await;
setup_tor_mocks(&server).await;
let api_client = create_test_api_client(&server);
let service = create_test_service(api_client);
let browsers = vec![
"firefox",
"firefox-developer",
"mullvad-browser",
"zen",
"brave",
"chromium",
"tor-browser",
];
for browser in browsers {
let result = service.fetch_browser_versions(browser, false).await;
match result {
Ok(versions) => {
assert!(!versions.is_empty(), "Should have versions for {browser}");
println!(
"{browser} test passed. Found {versions_count} versions",
versions_count = versions.len()
);
}
Err(e) => {
panic!("{browser} test failed: {e}");
}
}
}
}
#[tokio::test]
async fn test_no_caching_parameter() {
let server = setup_mock_server().await;
setup_firefox_mocks(&server).await;
let api_client = create_test_api_client(&server);
let service = create_test_service(api_client);
// Test with caching enabled (default)
let result_cached = service.fetch_browser_versions("firefox", false).await;
assert!(
result_cached.is_ok(),
"Should fetch Firefox versions with caching"
);
// Test with caching disabled (no_caching = true)
let result_no_cache = service.fetch_browser_versions("firefox", true).await;
assert!(
result_no_cache.is_ok(),
"Should fetch Firefox versions without caching"
);
// Both should return versions
if let (Ok(cached_versions), Ok(no_cache_versions)) = (result_cached, result_no_cache) {
assert!(
!cached_versions.is_empty(),
"Cached versions should not be empty"
);
assert!(
!no_cache_versions.is_empty(),
"No-cache versions should not be empty"
);
assert_eq!(
cached_versions, no_cache_versions,
"Both should return same versions"
);
println!(
"No-caching test passed. Cached: {} versions, No-cache: {} versions",
cached_versions.len(),
no_cache_versions.len()
);
}
}
#[tokio::test]
async fn test_detailed_versions_with_no_caching() {
let server = setup_mock_server().await;
setup_firefox_mocks(&server).await;
let api_client = create_test_api_client(&server);
let service = create_test_service(api_client);
// Test detailed versions with caching
let result_cached = service
.fetch_browser_versions_detailed("firefox", false)
.await;
assert!(
result_cached.is_ok(),
"Should fetch detailed Firefox versions with caching"
);
// Test detailed versions without caching
let result_no_cache = service
.fetch_browser_versions_detailed("firefox", true)
.await;
assert!(
result_no_cache.is_ok(),
"Should fetch detailed Firefox versions without caching"
);
// Both should return detailed version info
if let (Ok(cached_versions), Ok(no_cache_versions)) = (result_cached, result_no_cache) {
assert!(
!cached_versions.is_empty(),
"Cached detailed versions should not be empty"
);
assert!(
!no_cache_versions.is_empty(),
"No-cache detailed versions should not be empty"
);
// Check that detailed versions have all required fields
let first_cached = &cached_versions[0];
let first_no_cache = &no_cache_versions[0];
assert!(
!first_cached.version.is_empty(),
"Cached version should not be empty"
);
assert!(
!first_no_cache.version.is_empty(),
"No-cache version should not be empty"
);
assert_eq!(first_cached.version, "139.0", "Should have correct version");
assert_eq!(
first_no_cache.version, "139.0",
"Should have correct version"
);
assert_eq!(first_cached.date, "2024-01-15", "Should have correct date");
assert_eq!(
first_no_cache.date, "2024-01-15",
"Should have correct date"
);
println!(
"Detailed no-caching test passed. Cached: {} versions, No-cache: {} versions",
cached_versions.len(),
no_cache_versions.len()
);
}
}
#[test]
fn test_get_download_info() {
let service = BrowserVersionService::new();
let service = BrowserVersionService::instance();
// Test Firefox
let firefox_info = service.get_download_info("firefox", "139.0").unwrap();
@@ -1680,3 +1080,8 @@ mod tests {
println!("Download info test passed for all browsers");
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref BROWSER_VERSION_SERVICE: BrowserVersionService = BrowserVersionService::new();
}
+6 -348
View File
@@ -23,22 +23,22 @@ pub struct DownloadProgress {
pub struct Downloader {
client: Client,
api_client: ApiClient,
api_client: &'static ApiClient,
}
impl Downloader {
pub fn new() -> Self {
Self {
client: Client::new(),
api_client: ApiClient::new(),
api_client: ApiClient::instance(),
}
}
#[cfg(test)]
pub fn new_with_api_client(api_client: ApiClient) -> Self {
pub fn new_with_api_client(_api_client: ApiClient) -> Self {
Self {
client: Client::new(),
api_client,
api_client: ApiClient::instance(),
}
}
@@ -492,7 +492,7 @@ mod tests {
use crate::browser_version_service::DownloadInfo;
use tempfile::TempDir;
use wiremock::matchers::{method, path, query_param};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn setup_mock_server() -> MockServer {
@@ -510,153 +510,10 @@ mod tests {
)
}
#[tokio::test]
async fn test_resolve_brave_download_url() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "v1.81.9",
"name": "Brave Release 1.81.9",
"prerelease": false,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "brave-v1.81.9-universal.dmg",
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg",
"size": 200000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/brave/brave-browser/releases"))
.and(query_param("per_page", "100"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "brave-test.dmg".to_string(),
is_archive: true,
};
let result = downloader
.resolve_download_url(BrowserType::Brave, "v1.81.9", &download_info)
.await;
assert!(result.is_ok());
let url = result.unwrap();
assert_eq!(url, "https://example.com/brave-1.81.9-universal.dmg");
}
#[tokio::test]
async fn test_resolve_zen_download_url() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "1.11b",
"name": "Zen Browser 1.11b",
"prerelease": false,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "zen.macos-universal.dmg",
"browser_download_url": "https://example.com/zen-1.11b-universal.dmg",
"size": 120000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/zen-browser/desktop/releases"))
.and(query_param("per_page", "100"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "zen-test.dmg".to_string(),
is_archive: true,
};
let result = downloader
.resolve_download_url(BrowserType::Zen, "1.11b", &download_info)
.await;
assert!(result.is_ok());
let url = result.unwrap();
assert_eq!(url, "https://example.com/zen-1.11b-universal.dmg");
}
#[tokio::test]
async fn test_resolve_mullvad_download_url() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "14.5a6",
"name": "Mullvad Browser 14.5a6",
"prerelease": true,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "mullvad-browser-macos-14.5a6.dmg",
"browser_download_url": "https://example.com/mullvad-14.5a6.dmg",
"size": 100000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/mullvad/mullvad-browser/releases"))
.and(query_param("per_page", "100"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "mullvad-test.dmg".to_string(),
is_archive: true,
};
let result = downloader
.resolve_download_url(BrowserType::MullvadBrowser, "14.5a6", &download_info)
.await;
assert!(result.is_ok());
let url = result.unwrap();
assert_eq!(url, "https://example.com/mullvad-14.5a6.dmg");
}
#[tokio::test]
async fn test_resolve_firefox_download_url() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
@@ -717,106 +574,6 @@ mod tests {
assert_eq!(url, download_info.url);
}
#[tokio::test]
async fn test_resolve_brave_version_not_found() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "v1.81.8",
"name": "Brave Release 1.81.8",
"prerelease": false,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "brave-v1.81.8-universal.dmg",
"browser_download_url": "https://example.com/brave-1.81.8-universal.dmg",
"size": 200000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/brave/brave-browser/releases"))
.and(query_param("per_page", "100"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "brave-test.dmg".to_string(),
is_archive: true,
};
let result = downloader
.resolve_download_url(BrowserType::Brave, "v1.81.9", &download_info)
.await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Brave version v1.81.9 not found"));
}
#[tokio::test]
async fn test_resolve_zen_asset_not_found() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "1.11b",
"name": "Zen Browser 1.11b",
"prerelease": false,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "zen.linux-universal.tar.bz2",
"browser_download_url": "https://example.com/zen-1.11b-linux.tar.bz2",
"size": 150000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/zen-browser/desktop/releases"))
.and(query_param("per_page", "100"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "zen-test.dmg".to_string(),
is_archive: true,
};
let result = downloader
.resolve_download_url(BrowserType::Zen, "1.11b", &download_info)
.await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No compatible asset found"));
}
#[tokio::test]
async fn test_download_browser_with_progress() {
let server = setup_mock_server().await;
@@ -909,105 +666,6 @@ mod tests {
assert!(result.is_err());
}
#[tokio::test]
async fn test_resolve_mullvad_asset_not_found() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "14.5a6",
"name": "Mullvad Browser 14.5a6",
"prerelease": true,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "mullvad-browser-linux-14.5a6.tar.xz",
"browser_download_url": "https://example.com/mullvad-14.5a6.tar.xz",
"size": 80000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/mullvad/mullvad-browser/releases"))
.and(query_param("per_page", "100"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "mullvad-test.dmg".to_string(),
is_archive: true,
};
let result = downloader
.resolve_download_url(BrowserType::MullvadBrowser, "14.5a6", &download_info)
.await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No compatible asset found"));
}
#[tokio::test]
async fn test_brave_version_with_v_prefix() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let mock_response = r#"[
{
"tag_name": "v1.81.9",
"name": "Brave Release 1.81.9",
"prerelease": false,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "brave-v1.81.9-universal.dmg",
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg",
"size": 200000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/brave/brave-browser/releases"))
.and(query_param("per_page", "100"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "brave-test.dmg".to_string(),
is_archive: true,
};
// Test with version without v prefix
let result = downloader
.resolve_download_url(BrowserType::Brave, "1.81.9", &download_info)
.await;
assert!(result.is_ok());
let url = result.unwrap();
assert_eq!(url, "https://example.com/brave-1.81.9-universal.dmg");
}
#[tokio::test]
async fn test_download_browser_chunked_response() {
let server = setup_mock_server().await;
+1 -1
View File
@@ -366,7 +366,7 @@ pub fn run() {
let app_handle_update = app.handle().clone();
tauri::async_runtime::spawn(async move {
println!("Starting app update check at startup...");
let updater = app_auto_updater::AppAutoUpdater::new();
let updater = app_auto_updater::AppAutoUpdater::instance();
match updater.check_for_updates().await {
Ok(Some(update_info)) => {
println!(
+16 -7
View File
@@ -50,12 +50,16 @@ pub struct SettingsManager {
}
impl SettingsManager {
pub fn new() -> Self {
fn new() -> Self {
Self {
base_dirs: BaseDirs::new().expect("Failed to get base directories"),
}
}
pub fn instance() -> &'static SettingsManager {
&SETTINGS_MANAGER
}
pub fn get_settings_dir(&self) -> PathBuf {
let mut path = self.base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
@@ -159,7 +163,7 @@ impl SettingsManager {
#[tauri::command]
pub async fn get_app_settings() -> Result<AppSettings, String> {
let manager = SettingsManager::new();
let manager = SettingsManager::instance();
manager
.load_settings()
.map_err(|e| format!("Failed to load settings: {e}"))
@@ -167,7 +171,7 @@ pub async fn get_app_settings() -> Result<AppSettings, String> {
#[tauri::command]
pub async fn save_app_settings(settings: AppSettings) -> Result<(), String> {
let manager = SettingsManager::new();
let manager = SettingsManager::instance();
manager
.save_settings(&settings)
.map_err(|e| format!("Failed to save settings: {e}"))
@@ -175,7 +179,7 @@ pub async fn save_app_settings(settings: AppSettings) -> Result<(), String> {
#[tauri::command]
pub async fn should_show_settings_on_startup() -> Result<bool, String> {
let manager = SettingsManager::new();
let manager = SettingsManager::instance();
manager
.should_show_settings_on_startup()
.map_err(|e| format!("Failed to check prompt setting: {e}"))
@@ -183,7 +187,7 @@ pub async fn should_show_settings_on_startup() -> Result<bool, String> {
#[tauri::command]
pub async fn get_table_sorting_settings() -> Result<TableSortingSettings, String> {
let manager = SettingsManager::new();
let manager = SettingsManager::instance();
manager
.load_table_sorting()
.map_err(|e| format!("Failed to load table sorting settings: {e}"))
@@ -191,7 +195,7 @@ pub async fn get_table_sorting_settings() -> Result<TableSortingSettings, String
#[tauri::command]
pub async fn save_table_sorting_settings(sorting: TableSortingSettings) -> Result<(), String> {
let manager = SettingsManager::new();
let manager = SettingsManager::instance();
manager
.save_table_sorting(&sorting)
.map_err(|e| format!("Failed to save table sorting settings: {e}"))
@@ -201,7 +205,7 @@ pub async fn save_table_sorting_settings(sorting: TableSortingSettings) -> Resul
pub async fn clear_all_version_cache_and_refetch(
app_handle: tauri::AppHandle,
) -> Result<(), String> {
let api_client = ApiClient::new();
let api_client = ApiClient::instance();
// Clear all cache first
api_client
@@ -218,3 +222,8 @@ pub async fn clear_all_version_cache_and_refetch(
Ok(())
}
// Global singleton instance
lazy_static::lazy_static! {
static ref SETTINGS_MANAGER: SettingsManager = SettingsManager::new();
}
+4 -4
View File
@@ -47,16 +47,16 @@ impl Default for BackgroundUpdateState {
}
pub struct VersionUpdater {
version_service: BrowserVersionService,
auto_updater: AutoUpdater,
version_service: &'static BrowserVersionService,
auto_updater: &'static AutoUpdater,
app_handle: Option<tauri::AppHandle>,
}
impl VersionUpdater {
pub fn new() -> Self {
Self {
version_service: BrowserVersionService::new(),
auto_updater: AutoUpdater::new(),
version_service: BrowserVersionService::instance(),
auto_updater: AutoUpdater::instance(),
app_handle: None,
}
}