From 03a3e9fc563fa0710d36895ad2ec5a7166910ab6 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Fri, 30 May 2025 06:52:23 +0400 Subject: [PATCH] feat: mock all network requests in unit tests --- src-tauri/Cargo.lock | 95 ++++- src-tauri/Cargo.toml | 3 +- src-tauri/src/api_client.rs | 668 +++++++++++++++++++++++++----------- src-tauri/src/download.rs | 535 +++++++++++++++++++++++++---- 4 files changed, 1035 insertions(+), 266 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index dd96ff3..ee711e8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -82,6 +82,16 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -803,6 +813,24 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "deadpool" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "deflate64" version = "0.1.9" @@ -966,6 +994,7 @@ dependencies = [ "tempfile", "tokio", "tokio-test", + "wiremock", "zip", ] @@ -1211,6 +1240,21 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1218,6 +1262,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1285,6 +1330,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1653,6 +1699,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hermit-abi" version = "0.4.0" @@ -1728,6 +1780,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.6.0" @@ -1741,6 +1799,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa 1.0.14", "pin-project-lite", "smallvec", @@ -2471,6 +2530,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + [[package]] name = "num_enum" version = "0.7.3" @@ -3095,7 +3164,7 @@ checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.4.0", "pin-project-lite", "rustix 0.38.44", "tracing", @@ -5716,6 +5785,30 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wiremock" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "101681b74cd87b5899e87bcf5a64e83334dd313fcd3053ea72e6dba18928e301" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64 0.22.1", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "wit-bindgen-rt" version = "0.33.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 40c179d..2cfc44b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,7 +20,7 @@ tauri-build = { version = "2", features = [] } [dependencies] serde_json = "1" serde = { version = "1", features = ["derive"] } -tauri = { version = "2", features = ["devtools"] } +tauri = { version = "2", features = ["devtools", "test"] } tauri-plugin-opener = "2" tauri-plugin-fs = "2" tauri-plugin-shell = "2" @@ -41,6 +41,7 @@ core-foundation="0.10" [dev-dependencies] tempfile = "3.13.0" tokio-test = "0.4.4" +wiremock = "0.6" [features] # by default Tauri runs in production mode diff --git a/src-tauri/src/api_client.rs b/src-tauri/src/api_client.rs index a3198d1..f358231 100644 --- a/src-tauri/src/api_client.rs +++ b/src-tauri/src/api_client.rs @@ -231,12 +231,44 @@ struct CachedGithubData { pub struct ApiClient { client: Client, + firefox_api_base: String, + firefox_dev_api_base: String, + github_api_base: String, + chromium_api_base: String, + tor_archive_base: String, + mozilla_download_base: String, } impl ApiClient { pub fn new() -> Self { Self { client: Client::new(), + firefox_api_base: "https://product-details.mozilla.org/1.0".to_string(), + firefox_dev_api_base: "https://product-details.mozilla.org/1.0".to_string(), + github_api_base: "https://api.github.com".to_string(), + chromium_api_base: "https://commondatastorage.googleapis.com/chromium-browser-snapshots".to_string(), + tor_archive_base: "https://archive.torproject.org/tor-package-archive/torbrowser".to_string(), + mozilla_download_base: "https://download.mozilla.org".to_string(), + } + } + + #[cfg(test)] + pub fn new_with_base_urls( + firefox_api_base: String, + firefox_dev_api_base: String, + github_api_base: String, + chromium_api_base: String, + tor_archive_base: String, + mozilla_download_base: String, + ) -> Self { + Self { + client: Client::new(), + firefox_api_base, + firefox_dev_api_base, + github_api_base, + chromium_api_base, + tor_archive_base, + mozilla_download_base, } } @@ -376,7 +408,8 @@ impl ApiClient { date: "".to_string(), // Cache doesn't store dates is_prerelease: is_alpha_version(&version), download_url: Some(format!( - "https://download.mozilla.org/?product=firefox-{version}&os=osx&lang=en-US" + "{}/?product=firefox-{}&os=osx&lang=en-US", + self.mozilla_download_base, version )), } }) @@ -386,7 +419,7 @@ impl ApiClient { } println!("Fetching Firefox releases from Mozilla API..."); - let url = "https://product-details.mozilla.org/1.0/firefox.json"; + let url = format!("{}/firefox.json", self.firefox_api_base); let response = self .client @@ -414,8 +447,8 @@ impl ApiClient { date: release.date, is_prerelease: !is_stable, download_url: Some(format!( - "https://download.mozilla.org/?product=firefox-{}&os=osx&lang=en-US", - release.version + "{}/?product=firefox-{}&os=osx&lang=en-US", + self.mozilla_download_base, release.version )), }) } else { @@ -460,7 +493,8 @@ impl ApiClient { date: "".to_string(), // Cache doesn't store dates is_prerelease: is_alpha_version(&version), download_url: Some(format!( - "https://download.mozilla.org/?product=devedition-{version}&os=osx&lang=en-US" + "{}/?product=devedition-{}&os=osx&lang=en-US", + self.mozilla_download_base, version )), } }) @@ -470,7 +504,7 @@ impl ApiClient { } println!("Fetching Firefox Developer Edition releases from Mozilla API..."); - let url = "https://product-details.mozilla.org/1.0/devedition.json"; + let url = format!("{}/devedition.json", self.firefox_dev_api_base); let response = self .client @@ -504,8 +538,8 @@ impl ApiClient { date: release.date, is_prerelease: !is_stable, download_url: Some(format!( - "https://download.mozilla.org/?product=devedition-{}&os=osx&lang=en-US", - release.version + "{}/?product=devedition-{}&os=osx&lang=en-US", + self.mozilla_download_base, release.version )), }) } else { @@ -534,6 +568,7 @@ impl ApiClient { Ok(releases) } + #[allow(dead_code)] pub async fn fetch_mullvad_releases( &self, ) -> Result, Box> { @@ -552,7 +587,7 @@ impl ApiClient { } println!("Fetching Mullvad releases from GitHub API..."); - let url = "https://api.github.com/repos/mullvad/mullvad-browser/releases"; + let url = format!("{}/repos/mullvad/mullvad-browser/releases", self.github_api_base); let releases = self .client .get(url) @@ -583,6 +618,7 @@ impl ApiClient { Ok(releases) } + #[allow(dead_code)] pub async fn fetch_zen_releases( &self, ) -> Result, Box> { @@ -601,7 +637,7 @@ impl ApiClient { } println!("Fetching Zen releases from GitHub API..."); - let url = "https://api.github.com/repos/zen-browser/desktop/releases"; + let url = format!("{}/repos/zen-browser/desktop/releases", self.github_api_base); let mut releases = self .client .get(url) @@ -624,6 +660,7 @@ impl ApiClient { Ok(releases) } + #[allow(dead_code)] pub async fn fetch_brave_releases( &self, ) -> Result, Box> { @@ -642,7 +679,7 @@ impl ApiClient { } println!("Fetching Brave releases from GitHub API..."); - let url = "https://api.github.com/repos/brave/brave-browser/releases"; + let url = format!("{}/repos/brave/brave-browser/releases", self.github_api_base); let releases = self .client .get(url) @@ -696,7 +733,8 @@ impl ApiClient { "Mac" }; let url = format!( - "https://commondatastorage.googleapis.com/chromium-browser-snapshots/{arch}/LAST_CHANGE" + "{}/{arch}/LAST_CHANGE", + self.chromium_api_base ); let version = self .client @@ -783,7 +821,8 @@ impl ApiClient { date: "".to_string(), // Cache doesn't store dates is_prerelease: false, // Assume all archived versions are stable download_url: Some(format!( - "https://archive.torproject.org/tor-package-archive/torbrowser/{version}/tor-browser-macos-{version}.dmg" + "{}/{version}/tor-browser-macos-{version}.dmg", + self.tor_archive_base )), } }).collect()); @@ -791,7 +830,7 @@ impl ApiClient { } println!("Fetching TOR releases from archive..."); - let url = "https://archive.torproject.org/tor-package-archive/torbrowser/"; + let url = format!("{}/", self.tor_archive_base); let html = self .client .get(url) @@ -855,7 +894,8 @@ impl ApiClient { date: "".to_string(), // TOR archive doesn't provide structured dates is_prerelease: false, // Assume all archived versions are stable download_url: Some(format!( - "https://archive.torproject.org/tor-package-archive/torbrowser/{version}/tor-browser-macos-{version}.dmg" + "{}/{version}/tor-browser-macos-{version}.dmg", + self.tor_archive_base )), } }).collect()) @@ -865,7 +905,7 @@ impl ApiClient { &self, version: &str, ) -> Result> { - let url = format!("https://archive.torproject.org/tor-package-archive/torbrowser/{version}/"); + let url = format!("{}/{version}/", self.tor_archive_base); let html = self .client .get(&url) @@ -883,6 +923,24 @@ impl ApiClient { #[cfg(test)] mod tests { use super::*; + use wiremock::{MockServer, Mock, ResponseTemplate}; + use wiremock::matchers::{method, path, header}; + + async fn setup_mock_server() -> MockServer { + MockServer::start().await + } + + fn create_test_client(server: &MockServer) -> ApiClient { + let base_url = server.uri(); + ApiClient::new_with_base_urls( + base_url.clone(), // firefox_api_base + base_url.clone(), // firefox_dev_api_base + base_url.clone(), // github_api_base + base_url.clone(), // chromium_api_base + base_url.clone(), // tor_archive_base + base_url.clone(), // mozilla_download_base + ) + } #[test] fn test_version_parsing() { @@ -981,236 +1039,456 @@ mod tests { #[tokio::test] async fn test_firefox_api() { - let client = ApiClient::new(); - let result = client.fetch_firefox_releases_with_caching(false).await; + let server = setup_mock_server().await; + let client = create_test_client(&server); - match result { - Ok(releases) => { - assert!(!releases.is_empty(), "Should have Firefox releases"); - - // Check that releases have required fields - let first_release = &releases[0]; - assert!( - !first_release.version.is_empty(), - "Version should not be empty" - ); - assert!( - first_release.download_url.is_some(), - "Should have download URL" - ); - - println!("Firefox API test passed. Found {} releases", releases.len()); - println!("Latest version: {}", releases[0].version); + 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" + } } - Err(e) => { - println!("Firefox API test failed: {e}"); - panic!("Firefox API should work"); - } - } + }"#; + + Mock::given(method("GET")) + .and(path("/firefox.json")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string(mock_response) + .insert_header("content-type", "application/json")) + .mount(&server) + .await; + + let result = client.fetch_firefox_releases_with_caching(true).await; + + assert!(result.is_ok()); + let releases = result.unwrap(); + assert!(!releases.is_empty()); + assert_eq!(releases[0].version, "139.0"); + assert!(releases[0].download_url.is_some()); + assert!(releases[0].download_url.as_ref().unwrap().contains(&server.uri())); } #[tokio::test] async fn test_firefox_developer_api() { - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; // Rate limiting + let server = setup_mock_server().await; + let client = create_test_client(&server); - let client = ApiClient::new(); - let result = client - .fetch_firefox_developer_releases_with_caching(false) + 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" + } + } + }"#; + + Mock::given(method("GET")) + .and(path("/devedition.json")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string(mock_response) + .insert_header("content-type", "application/json")) + .mount(&server) .await; - match result { - Ok(releases) => { - assert!( - !releases.is_empty(), - "Should have Firefox Developer releases" - ); + let result = client.fetch_firefox_developer_releases_with_caching(true).await; - let first_release = &releases[0]; - assert!( - !first_release.version.is_empty(), - "Version should not be empty" - ); - assert!( - first_release.download_url.is_some(), - "Should have download URL" - ); - - println!( - "Firefox Developer API test passed. Found {} releases", - releases.len() - ); - println!("Latest version: {}", releases[0].version); - } - Err(e) => { - println!("Firefox Developer API test failed: {e}"); - panic!("Firefox Developer API should work"); - } - } + assert!(result.is_ok()); + let releases = result.unwrap(); + assert!(!releases.is_empty()); + assert_eq!(releases[0].version, "140.0b1"); + assert!(releases[0].download_url.is_some()); + assert!(releases[0].download_url.as_ref().unwrap().contains(&server.uri())); } #[tokio::test] async fn test_mullvad_api() { - tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; // Rate limiting + let server = setup_mock_server().await; + let client = create_test_client(&server); - let client = ApiClient::new(); - let result = client.fetch_mullvad_releases().await; - - match result { - Ok(releases) => { - assert!(!releases.is_empty(), "Should have Mullvad releases"); - - let first_release = &releases[0]; - assert!( - !first_release.tag_name.is_empty(), - "Tag name should not be empty" - ); - - println!("Mullvad API test passed. Found {} releases", releases.len()); - println!("Latest version: {}", releases[0].tag_name); + 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" + } + ] } - Err(e) => { - println!("Mullvad API test failed: {e}"); - panic!("Mullvad API should work"); - } - } + ]"#; + + Mock::given(method("GET")) + .and(path("/repos/mullvad/mullvad-browser/releases")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string(mock_response) + .insert_header("content-type", "application/json")) + .mount(&server) + .await; + + let result = client.fetch_mullvad_releases_with_caching(true).await; + + assert!(result.is_ok()); + let releases = result.unwrap(); + assert!(!releases.is_empty()); + assert_eq!(releases[0].tag_name, "14.5a6"); + assert!(releases[0].is_alpha); } #[tokio::test] async fn test_zen_api() { - tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await; // Rate limiting + let server = setup_mock_server().await; + let client = create_test_client(&server); - let client = ApiClient::new(); - let result = client.fetch_zen_releases().await; - - match result { - Ok(releases) => { - assert!(!releases.is_empty(), "Should have Zen releases"); - - let first_release = &releases[0]; - assert!( - !first_release.tag_name.is_empty(), - "Tag name should not be empty" - ); - - println!("Zen API test passed. Found {} releases", releases.len()); - println!("Latest version: {}", releases[0].tag_name); + let mock_response = r#"[ + { + "tag_name": "1.0.0-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" + } + ] } - Err(e) => { - println!("Zen API test failed: {e}"); - panic!("Zen API should work"); - } - } + ]"#; + + Mock::given(method("GET")) + .and(path("/repos/zen-browser/desktop/releases")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string(mock_response) + .insert_header("content-type", "application/json")) + .mount(&server) + .await; + + let result = client.fetch_zen_releases_with_caching(true).await; + + assert!(result.is_ok()); + let releases = result.unwrap(); + assert!(!releases.is_empty()); + assert_eq!(releases[0].tag_name, "1.0.0-twilight"); } #[tokio::test] async fn test_brave_api() { - tokio::time::sleep(tokio::time::Duration::from_millis(2000)).await; // Rate limiting + let server = setup_mock_server().await; + let client = create_test_client(&server); - let client = ApiClient::new(); - let result = client.fetch_brave_releases().await; + 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" + } + ] + } + ]"#; - match result { - Ok(releases) => { - // Note: Brave might not always have macOS releases, so we don't assert non-empty - println!( - "Brave API test passed. Found {} releases with macOS assets", - releases.len() - ); - if !releases.is_empty() { - println!("Latest version: {}", releases[0].tag_name); - } - } - Err(e) => { - println!("Brave API test failed: {e}"); - panic!("Brave API should work"); - } - } + Mock::given(method("GET")) + .and(path("/repos/brave/brave-browser/releases")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string(mock_response) + .insert_header("content-type", "application/json")) + .mount(&server) + .await; + + let result = client.fetch_brave_releases_with_caching(true).await; + + assert!(result.is_ok()); + let releases = result.unwrap(); + assert!(!releases.is_empty()); + assert_eq!(releases[0].tag_name, "v1.81.9"); + assert!(!releases[0].is_alpha); } #[tokio::test] async fn test_chromium_api() { - tokio::time::sleep(tokio::time::Duration::from_millis(2500)).await; // Rate limiting + let server = setup_mock_server().await; + let client = create_test_client(&server); + + let arch = if cfg!(target_arch = "aarch64") { + "Mac_Arm" + } else { + "Mac" + }; + + Mock::given(method("GET")) + .and(path(format!("/{arch}/LAST_CHANGE"))) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string("1465660") + .insert_header("content-type", "text/plain")) + .mount(&server) + .await; - let client = ApiClient::new(); let result = client.fetch_chromium_latest_version().await; - match result { - Ok(version) => { - assert!(!version.is_empty(), "Version should not be empty"); - assert!( - version.chars().all(|c| c.is_ascii_digit()), - "Version should be numeric" - ); + assert!(result.is_ok()); + let version = result.unwrap(); + assert_eq!(version, "1465660"); + } - println!("Chromium API test passed. Latest version: {version}"); - } - Err(e) => { - println!("Chromium API test failed: {e}"); - panic!("Chromium API should work"); - } - } + #[tokio::test] + async fn test_chromium_releases_with_caching() { + let server = setup_mock_server().await; + let client = create_test_client(&server); + + let arch = if cfg!(target_arch = "aarch64") { + "Mac_Arm" + } else { + "Mac" + }; + + Mock::given(method("GET")) + .and(path(format!("/{arch}/LAST_CHANGE"))) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string("1465660") + .insert_header("content-type", "text/plain")) + .mount(&server) + .await; + + let result = client.fetch_chromium_releases_with_caching(true).await; + + assert!(result.is_ok()); + let releases = result.unwrap(); + assert!(!releases.is_empty()); + assert_eq!(releases[0].version, "1465660"); + assert!(!releases[0].is_prerelease); } #[tokio::test] async fn test_tor_api() { - tokio::time::sleep(tokio::time::Duration::from_millis(3000)).await; // Rate limiting + let server = setup_mock_server().await; + let client = create_test_client(&server); - let client = ApiClient::new(); + let mock_html = r#" + + + ../ + 14.0.4/ + 14.0.3/ + + + "#; - // Use a timeout for this test since TOR API can be slow - let timeout_duration = tokio::time::Duration::from_secs(30); - let result = tokio::time::timeout( - timeout_duration, - client.fetch_tor_releases_with_caching(false), - ) - .await; + let version_html = r#" + + + tor-browser-macos-14.0.4.dmg + + + "#; - match result { - Ok(Ok(releases)) => { - assert!(!releases.is_empty(), "Should have TOR releases"); + Mock::given(method("GET")) + .and(path("/")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string(mock_html) + .insert_header("content-type", "text/html")) + .mount(&server) + .await; - let first_release = &releases[0]; - assert!( - !first_release.version.is_empty(), - "Version should not be empty" - ); - assert!( - first_release.download_url.is_some(), - "Should have download URL" - ); + Mock::given(method("GET")) + .and(path("/14.0.4/")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string(version_html) + .insert_header("content-type", "text/html")) + .mount(&server) + .await; - println!("TOR API test passed. Found {} releases", releases.len()); - println!("Latest version: {}", releases[0].version); - } - Ok(Err(e)) => { - println!("TOR API test failed: {e}"); - // Don't panic for TOR API since it can be unreliable - println!("TOR API test skipped due to network issues"); - } - Err(_) => { - println!("TOR API test timed out after 30 seconds"); - // Don't panic for timeout, just skip - println!("TOR API test skipped due to timeout"); - } - } + Mock::given(method("GET")) + .and(path("/14.0.3/")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string(version_html.replace("14.0.4", "14.0.3")) + .insert_header("content-type", "text/html")) + .mount(&server) + .await; + + let result = client.fetch_tor_releases_with_caching(true).await; + + assert!(result.is_ok()); + let releases = result.unwrap(); + assert!(!releases.is_empty()); + assert_eq!(releases[0].version, "14.0.4"); + assert!(releases[0].download_url.is_some()); + assert!(releases[0].download_url.as_ref().unwrap().contains(&server.uri())); } #[tokio::test] async fn test_tor_version_check() { - tokio::time::sleep(tokio::time::Duration::from_millis(3500)).await; // Rate limiting + let server = setup_mock_server().await; + let client = create_test_client(&server); + + let version_html = r#" + + + tor-browser-macos-14.0.4.dmg + + + "#; + + Mock::given(method("GET")) + .and(path("/14.0.4/")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string(version_html) + .insert_header("content-type", "text/html")) + .mount(&server) + .await; - let client = ApiClient::new(); let result = client.check_tor_version_has_macos("14.0.4").await; - match result { - Ok(has_macos) => { - assert!(has_macos, "Version 14.0.4 should have macOS support"); - println!("TOR version check test passed. Version 14.0.4 has macOS: {has_macos}"); - } - Err(e) => { - println!("TOR version check test failed: {e}"); - panic!("TOR version check should work"); - } - } + assert!(result.is_ok()); + assert!(result.unwrap()); + } + + #[tokio::test] + async fn test_tor_version_check_no_macos() { + let server = setup_mock_server().await; + let client = create_test_client(&server); + + let version_html = r#" + + + tor-browser-linux-14.0.4.tar.xz + + + "#; + + Mock::given(method("GET")) + .and(path("/14.0.5/")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string(version_html) + .insert_header("content-type", "text/html")) + .mount(&server) + .await; + + let result = client.check_tor_version_has_macos("14.0.5").await; + + assert!(result.is_ok()); + assert!(!result.unwrap()); + } + + #[test] + fn test_is_alpha_version() { + assert!(is_alpha_version("1.2.3a1")); + assert!(is_alpha_version("137.0b5")); + assert!(is_alpha_version("140.0rc1")); + assert!(!is_alpha_version("139.0")); + assert!(!is_alpha_version("1.2.3")); + } + + #[test] + fn test_sort_versions_comprehensive() { + let mut versions = vec![ + "1.0.0".to_string(), + "1.0.1".to_string(), + "1.1.0".to_string(), + "2.0.0a1".to_string(), + "2.0.0b1".to_string(), + "2.0.0rc1".to_string(), + "2.0.0".to_string(), + "10.0.0".to_string(), + "1.0.0-twilight".to_string(), + ]; + + sort_versions(&mut versions); + + // Twilight should be first, then normal semantic versioning + assert_eq!(versions[0], "1.0.0-twilight"); + assert_eq!(versions[1], "10.0.0"); + assert_eq!(versions[2], "2.0.0"); + assert_eq!(versions[3], "2.0.0rc1"); + assert_eq!(versions[4], "2.0.0b1"); + assert_eq!(versions[5], "2.0.0a1"); + } + + #[tokio::test] + async fn test_error_handling_404() { + let server = setup_mock_server().await; + let client = create_test_client(&server); + + Mock::given(method("GET")) + .and(path("/firefox.json")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(404)) + .mount(&server) + .await; + + let result = client.fetch_firefox_releases_with_caching(true).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_error_handling_invalid_json() { + let server = setup_mock_server().await; + let client = create_test_client(&server); + + Mock::given(method("GET")) + .and(path("/firefox.json")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string("invalid json") + .insert_header("content-type", "application/json")) + .mount(&server) + .await; + + let result = client.fetch_firefox_releases_with_caching(true).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_github_api_rate_limit() { + let server = setup_mock_server().await; + let client = create_test_client(&server); + + Mock::given(method("GET")) + .and(path("/repos/zen-browser/desktop/releases")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(429) + .insert_header("retry-after", "60")) + .mount(&server) + .await; + + let result = client.fetch_zen_releases_with_caching(true).await; + assert!(result.is_err()); } } diff --git a/src-tauri/src/download.rs b/src-tauri/src/download.rs index be12de6..de10a9f 100644 --- a/src-tauri/src/download.rs +++ b/src-tauri/src/download.rs @@ -34,6 +34,14 @@ impl Downloader { } } + #[cfg(test)] + pub fn new_with_api_client(api_client: ApiClient) -> Self { + Self { + client: Client::new(), + api_client, + } + } + /// Resolve the actual download URL for browsers that need dynamic asset resolution pub async fn resolve_download_url( &self, @@ -44,7 +52,7 @@ impl Downloader { match browser_type { BrowserType::Brave => { // For Brave, we need to find the actual macOS asset - let releases = self.api_client.fetch_brave_releases().await?; + let releases = self.api_client.fetch_brave_releases_with_caching(true).await?; // Find the release with the matching version let release = releases @@ -67,7 +75,7 @@ impl Downloader { } BrowserType::Zen => { // For Zen, verify the asset exists - let releases = self.api_client.fetch_zen_releases().await?; + let releases = self.api_client.fetch_zen_releases_with_caching(true).await?; let release = releases .iter() @@ -87,7 +95,7 @@ impl Downloader { } BrowserType::MullvadBrowser => { // For Mullvad, verify the asset exists - let releases = self.api_client.fetch_mullvad_releases().await?; + let releases = self.api_client.fetch_mullvad_releases_with_caching(true).await?; let release = releases .iter() @@ -112,9 +120,9 @@ impl Downloader { } } - pub async fn download_browser( + pub async fn download_browser( &self, - app_handle: &tauri::AppHandle, + app_handle: &tauri::AppHandle, browser_type: BrowserType, version: &str, download_info: &DownloadInfo, @@ -149,6 +157,11 @@ impl Downloader { .send() .await?; + // Check if the response is successful + if !response.status().is_success() { + return Err(format!("Download failed with status: {}", response.status()).into()); + } + let total_size = response.content_length(); let mut downloaded = 0u64; let start_time = std::time::Instant::now(); @@ -206,12 +219,60 @@ impl Downloader { #[cfg(test)] mod tests { use super::*; + use crate::api_client::ApiClient; + use crate::browser::BrowserType; + use crate::browser_version_service::DownloadInfo; + + use wiremock::{MockServer, Mock, ResponseTemplate}; + use wiremock::matchers::{method, path, header}; + use tempfile::TempDir; + + async fn setup_mock_server() -> MockServer { + MockServer::start().await + } + + fn create_test_api_client(server: &MockServer) -> ApiClient { + let base_url = server.uri(); + ApiClient::new_with_base_urls( + base_url.clone(), // firefox_api_base + base_url.clone(), // firefox_dev_api_base + base_url.clone(), // github_api_base + base_url.clone(), // chromium_api_base + base_url.clone(), // tor_archive_base + base_url.clone(), // mozilla_download_base + ) + } #[tokio::test] async fn test_resolve_brave_download_url() { - let downloader = Downloader::new(); + 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" + } + ] + } + ]"#; + + Mock::given(method("GET")) + .and(path("/repos/brave/brave-browser/releases")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_string(mock_response) + .insert_header("content-type", "application/json")) + .mount(&server) + .await; - // Test with a known Brave version let download_info = DownloadInfo { url: "placeholder".to_string(), filename: "brave-test.dmg".to_string(), @@ -222,23 +283,40 @@ mod tests { .resolve_download_url(BrowserType::Brave, "v1.81.9", &download_info) .await; - match result { - Ok(url) => { - assert!(url.contains("github.com/brave/brave-browser")); - assert!(url.contains(".dmg")); - assert!(url.contains("universal")); - println!("Brave download URL resolved: {url}"); - } - Err(e) => { - println!("Brave URL resolution failed (expected if version doesn't exist): {e}"); - // This might fail if the version doesn't exist, which is okay for testing - } - } + 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 downloader = Downloader::new(); + 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" + } + ] + } + ]"#; + + Mock::given(method("GET")) + .and(path("/repos/zen-browser/desktop/releases")) + .and(header("user-agent", "donutbrowser")) + .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(), @@ -250,21 +328,40 @@ mod tests { .resolve_download_url(BrowserType::Zen, "1.11b", &download_info) .await; - match result { - Ok(url) => { - assert!(url.contains("github.com/zen-browser/desktop")); - assert!(url.contains("zen.macos-universal.dmg")); - println!("Zen download URL resolved: {url}"); - } - Err(e) => { - println!("Zen URL resolution failed (expected if version doesn't exist): {e}"); - } - } + 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 downloader = Downloader::new(); + 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" + } + ] + } + ]"#; + + Mock::given(method("GET")) + .and(path("/repos/mullvad/mullvad-browser/releases")) + .and(header("user-agent", "donutbrowser")) + .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(), @@ -276,21 +373,16 @@ mod tests { .resolve_download_url(BrowserType::MullvadBrowser, "14.5a6", &download_info) .await; - match result { - Ok(url) => { - assert!(url.contains("github.com/mullvad/mullvad-browser")); - assert!(url.contains(".dmg")); - println!("Mullvad download URL resolved: {url}"); - } - Err(e) => { - println!("Mullvad URL resolution failed (expected if version doesn't exist): {e}"); - } - } + 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 downloader = Downloader::new(); + let server = setup_mock_server().await; + let api_client = create_test_api_client(&server); + let downloader = Downloader::new_with_api_client(api_client); let download_info = DownloadInfo { url: "https://download.mozilla.org/?product=firefox-139.0&os=osx&lang=en-US".to_string(), @@ -302,20 +394,16 @@ mod tests { .resolve_download_url(BrowserType::Firefox, "139.0", &download_info) .await; - match result { - Ok(url) => { - assert_eq!(url, download_info.url); - println!("Firefox download URL (passthrough): {url}"); - } - Err(e) => { - panic!("Firefox URL resolution should not fail: {e}"); - } - } + assert!(result.is_ok()); + let url = result.unwrap(); + assert_eq!(url, download_info.url); } #[tokio::test] async fn test_resolve_chromium_download_url() { - let downloader = Downloader::new(); + let server = setup_mock_server().await; + let api_client = create_test_api_client(&server); + let downloader = Downloader::new_with_api_client(api_client); let download_info = DownloadInfo { url: "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac/1465660/chrome-mac.zip".to_string(), @@ -327,20 +415,16 @@ mod tests { .resolve_download_url(BrowserType::Chromium, "1465660", &download_info) .await; - match result { - Ok(url) => { - assert_eq!(url, download_info.url); - println!("Chromium download URL (passthrough): {url}"); - } - Err(e) => { - panic!("Chromium URL resolution should not fail: {e}"); - } - } + assert!(result.is_ok()); + let url = result.unwrap(); + assert_eq!(url, download_info.url); } #[tokio::test] async fn test_resolve_tor_download_url() { - let downloader = Downloader::new(); + let server = setup_mock_server().await; + let api_client = create_test_api_client(&server); + let downloader = Downloader::new_with_api_client(api_client); let download_info = DownloadInfo { url: "https://archive.torproject.org/tor-package-archive/torbrowser/14.0.4/tor-browser-macos-14.0.4.dmg".to_string(), @@ -352,14 +436,327 @@ mod tests { .resolve_download_url(BrowserType::TorBrowser, "14.0.4", &download_info) .await; - match result { - Ok(url) => { - assert_eq!(url, download_info.url); - println!("TOR download URL (passthrough): {url}"); + assert!(result.is_ok()); + let url = result.unwrap(); + 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" + } + ] } - Err(e) => { - panic!("TOR URL resolution should not fail: {e}"); + ]"#; + + Mock::given(method("GET")) + .and(path("/repos/brave/brave-browser/releases")) + .and(header("user-agent", "donutbrowser")) + .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" + } + ] } - } + ]"#; + + Mock::given(method("GET")) + .and(path("/repos/zen-browser/desktop/releases")) + .and(header("user-agent", "donutbrowser")) + .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 macOS universal asset found")); + } + + #[tokio::test] + async fn test_download_browser_with_progress() { + let server = setup_mock_server().await; + let api_client = create_test_api_client(&server); + let downloader = Downloader::new_with_api_client(api_client); + + // Create a temporary directory for the test + let temp_dir = TempDir::new().unwrap(); + let dest_path = temp_dir.path(); + + // Create test file content (simulating a small download) + let test_content = b"This is a test file content for download simulation"; + + // Mock the download endpoint + Mock::given(method("GET")) + .and(path("/test-download")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_bytes(test_content) + .insert_header("content-length", test_content.len().to_string()) + .insert_header("content-type", "application/octet-stream")) + .mount(&server) + .await; + + let download_info = DownloadInfo { + url: format!("{}/test-download", server.uri()), + filename: "test-file.dmg".to_string(), + is_archive: true, + }; + + // Create a mock app handle for testing + let app = tauri::test::mock_app(); + let app_handle = app.handle().clone(); + + let result = downloader + .download_browser( + &app_handle, + BrowserType::Firefox, + "139.0", + &download_info, + dest_path, + ) + .await; + + assert!(result.is_ok()); + let downloaded_file = result.unwrap(); + assert!(downloaded_file.exists()); + + // Verify file content + let downloaded_content = std::fs::read(&downloaded_file).unwrap(); + assert_eq!(downloaded_content, test_content); + } + + #[tokio::test] + async fn test_download_browser_network_error() { + let server = setup_mock_server().await; + let api_client = create_test_api_client(&server); + let downloader = Downloader::new_with_api_client(api_client); + + let temp_dir = TempDir::new().unwrap(); + let dest_path = temp_dir.path(); + + // Mock a 404 response + Mock::given(method("GET")) + .and(path("/missing-file")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(404)) + .mount(&server) + .await; + + let download_info = DownloadInfo { + url: format!("{}/missing-file", server.uri()), + filename: "missing-file.dmg".to_string(), + is_archive: true, + }; + + let app = tauri::test::mock_app(); + let app_handle = app.handle().clone(); + + let result = downloader + .download_browser( + &app_handle, + BrowserType::Firefox, + "139.0", + &download_info, + dest_path, + ) + .await; + + 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" + } + ] + } + ]"#; + + Mock::given(method("GET")) + .and(path("/repos/mullvad/mullvad-browser/releases")) + .and(header("user-agent", "donutbrowser")) + .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 macOS 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" + } + ] + } + ]"#; + + Mock::given(method("GET")) + .and(path("/repos/brave/brave-browser/releases")) + .and(header("user-agent", "donutbrowser")) + .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; + let api_client = create_test_api_client(&server); + let downloader = Downloader::new_with_api_client(api_client); + + let temp_dir = TempDir::new().unwrap(); + let dest_path = temp_dir.path(); + + // Create larger test content to simulate chunked transfer + let test_content = vec![42u8; 1024]; // 1KB of data + + Mock::given(method("GET")) + .and(path("/chunked-download")) + .and(header("user-agent", "donutbrowser")) + .respond_with(ResponseTemplate::new(200) + .set_body_bytes(test_content.clone()) + .insert_header("content-length", test_content.len().to_string()) + .insert_header("content-type", "application/octet-stream")) + .mount(&server) + .await; + + let download_info = DownloadInfo { + url: format!("{}/chunked-download", server.uri()), + filename: "chunked-file.dmg".to_string(), + is_archive: true, + }; + + let app = tauri::test::mock_app(); + let app_handle = app.handle().clone(); + + let result = downloader + .download_browser( + &app_handle, + BrowserType::Chromium, + "1465660", + &download_info, + dest_path, + ) + .await; + + assert!(result.is_ok()); + let downloaded_file = result.unwrap(); + assert!(downloaded_file.exists()); + + let downloaded_content = std::fs::read(&downloaded_file).unwrap(); + assert_eq!(downloaded_content.len(), test_content.len()); } }