feat: mock all network requests in unit tests

This commit is contained in:
zhom
2025-05-30 06:52:23 +04:00
parent 26a5be55f1
commit 03a3e9fc56
4 changed files with 1035 additions and 266 deletions
+94 -1
View File
@@ -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"
+2 -1
View File
@@ -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
+473 -195
View File
@@ -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<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
@@ -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<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
@@ -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<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
@@ -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<bool, Box<dyn std::error::Error + Send + Sync>> {
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#"
<html>
<body>
<a href="../">../</a>
<a href="14.0.4/">14.0.4/</a>
<a href="14.0.3/">14.0.3/</a>
</body>
</html>
"#;
// 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#"
<html>
<body>
<a href="tor-browser-macos-14.0.4.dmg">tor-browser-macos-14.0.4.dmg</a>
</body>
</html>
"#;
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#"
<html>
<body>
<a href="tor-browser-macos-14.0.4.dmg">tor-browser-macos-14.0.4.dmg</a>
</body>
</html>
"#;
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#"
<html>
<body>
<a href="tor-browser-linux-14.0.4.tar.xz">tor-browser-linux-14.0.4.tar.xz</a>
</body>
</html>
"#;
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());
}
}
+466 -69
View File
@@ -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<R: tauri::Runtime>(
&self,
app_handle: &tauri::AppHandle,
app_handle: &tauri::AppHandle<R>,
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());
}
}