From 104de7d0698fb1a5b5ecf9aa8b67e1fb733a5b36 Mon Sep 17 00:00:00 2001 From: theg Date: Tue, 21 Oct 2025 11:59:27 +0530 Subject: [PATCH 1/2] feat: add tests --- apps/desktop/src-tauri/src/lib.rs | 8 +-- apps/desktop/src-tauri/src/link.rs | 15 +++-- apps/desktop/src-tauri/src/routing.rs | 9 ++- .../src-tauri/tests/browser_details_parse.rs | 18 ++++++ .../src-tauri/tests/diagnostics_state.rs | 29 +++++++++ apps/desktop/src-tauri/tests/link_parsing.rs | 64 +++++++++++++++++++ apps/desktop/src-tauri/tests/routing_utils.rs | 42 ++++++++++++ 7 files changed, 173 insertions(+), 12 deletions(-) create mode 100644 apps/desktop/src-tauri/tests/browser_details_parse.rs create mode 100644 apps/desktop/src-tauri/tests/diagnostics_state.rs create mode 100644 apps/desktop/src-tauri/tests/link_parsing.rs create mode 100644 apps/desktop/src-tauri/tests/routing_utils.rs diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 8975617..30f6a48 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -1,11 +1,11 @@ -mod browser_details; +pub mod browser_details; mod commands; -mod diagnostics; +pub mod diagnostics; mod domain; -mod link; +pub mod link; mod platform; mod preferences; -mod routing; +pub mod routing; use commands::{ clear_diagnostics, export_diagnostics, get_available_browsers, get_diagnostics, diff --git a/apps/desktop/src-tauri/src/link.rs b/apps/desktop/src-tauri/src/link.rs index be4e7b3..f6b6127 100644 --- a/apps/desktop/src-tauri/src/link.rs +++ b/apps/desktop/src-tauri/src/link.rs @@ -79,7 +79,8 @@ fn dispatch_urls(app: &AppHandle, urls: Vec, origin: LinkSource) { }); } -fn extract_urls(args: &[String]) -> Vec { +/// Extracts unique http(s) URLs from command-line arguments. +pub fn extract_urls(args: &[String]) -> Vec { let mut collected = Vec::new(); let mut after_delimiter = false; @@ -109,7 +110,8 @@ fn extract_urls(args: &[String]) -> Vec { collected } -fn parse_argument(arg: &str) -> Option { +/// Parses an individual CLI argument for a url candidate. +pub fn parse_argument(arg: &str) -> Option { if let Some(candidate) = parse_candidate(arg) { return Some(candidate); } @@ -126,7 +128,8 @@ fn parse_argument(arg: &str) -> Option { None } -fn parse_candidate(input: &str) -> Option { +/// Parses a raw string, returning a normalized http(s) URL if valid. +pub fn parse_candidate(input: &str) -> Option { let trimmed = input.trim_matches(|c| matches!(c, '"' | '\'')); if trimmed.is_empty() { return None; @@ -149,7 +152,8 @@ fn parse_candidate(input: &str) -> Option { None } -fn percent_decode_if_needed(input: &str) -> Cow<'_, str> { +/// Percent decodes a string when common URL encodings are present. +pub fn percent_decode_if_needed(input: &str) -> Cow<'_, str> { if input.contains("%3A") || input.contains("%2F") { if let Ok(decoded) = percent_encoding::percent_decode_str(input).decode_utf8() { return Cow::Owned(decoded.into_owned()); @@ -158,7 +162,8 @@ fn percent_decode_if_needed(input: &str) -> Cow<'_, str> { Cow::Borrowed(input) } -fn push_unique(list: &mut Vec, value: String) { +/// Pushes a value into the list when it does not already exist. +pub fn push_unique(list: &mut Vec, value: String) { if !list.iter().any(|existing| existing == &value) { list.push(value); } diff --git a/apps/desktop/src-tauri/src/routing.rs b/apps/desktop/src-tauri/src/routing.rs index 451bcea..0425ca3 100644 --- a/apps/desktop/src-tauri/src/routing.rs +++ b/apps/desktop/src-tauri/src/routing.rs @@ -344,7 +344,8 @@ impl RoutingService { } } -fn normalize_url(input: &str) -> String { +/// Normalize incoming URLs by ensuring they include a scheme and trimming whitespace. +pub fn normalize_url(input: &str) -> String { let trimmed = input.trim(); if trimmed.is_empty() { return String::new(); @@ -386,7 +387,8 @@ fn resolve_browser_path(name: &str) -> Option { None } -fn normalize_browser_key(value: &str) -> String { +/// Normalize a browser name into a lowercase alphanumeric key. +pub fn normalize_browser_key(value: &str) -> String { value .chars() .filter(|c| c.is_ascii_alphanumeric()) @@ -421,7 +423,8 @@ fn launch_with_browser( command.spawn().map(|_| ()).map_err(|err| err.to_string()) } -fn add_profile_args(command: &mut Command, browser_name: &str, profile: &str) { +/// Append browser-specific arguments to target a profile directory. +pub fn add_profile_args(command: &mut Command, browser_name: &str, profile: &str) { let trimmed = profile.trim(); if trimmed.is_empty() { return; diff --git a/apps/desktop/src-tauri/tests/browser_details_parse.rs b/apps/desktop/src-tauri/tests/browser_details_parse.rs new file mode 100644 index 0000000..71c53aa --- /dev/null +++ b/apps/desktop/src-tauri/tests/browser_details_parse.rs @@ -0,0 +1,18 @@ +use desktop_lib::browser_details::{parse_browser_kind, Browsers}; + +#[test] +fn parses_common_browser_names() { + assert_eq!(parse_browser_kind("Chrome"), Some(Browsers::Chrome)); + assert_eq!(parse_browser_kind(" google-chrome"), Some(Browsers::Chrome)); + assert_eq!(parse_browser_kind("MICROSOFT EDGE"), Some(Browsers::Edge)); + assert_eq!(parse_browser_kind("Brave Browser"), Some(Browsers::Brave)); + assert_eq!(parse_browser_kind("MozillaFirefox"), Some(Browsers::FireFox)); + assert_eq!(parse_browser_kind("Safari"), Some(Browsers::Safari)); +} + +#[test] +fn returns_none_for_unknown_browser() { + assert_eq!(parse_browser_kind("Netscape"), None); + assert_eq!(parse_browser_kind(""), None); + assert_eq!(parse_browser_kind(" "), None); +} diff --git a/apps/desktop/src-tauri/tests/diagnostics_state.rs b/apps/desktop/src-tauri/tests/diagnostics_state.rs new file mode 100644 index 0000000..847d76e --- /dev/null +++ b/apps/desktop/src-tauri/tests/diagnostics_state.rs @@ -0,0 +1,29 @@ +use desktop_lib::diagnostics::DiagnosticsState; + +#[test] +fn record_creates_entries_and_enforces_capacity() { + let state = DiagnosticsState::default(); + + for i in 0..510 { + state.record(format!("event #{i}")); + } + + let snapshot = state.snapshot(); + // Max entries is 500; ensure oldest entries dropped + assert_eq!(snapshot.len(), 500); + assert!(snapshot.first().unwrap().message.starts_with("event #10")); + assert!(snapshot.last().unwrap().message.starts_with("event #509")); +} + +#[test] +fn clear_removes_all_entries() { + let state = DiagnosticsState::default(); + state.record("first" ); + state.record("second"); + + assert_eq!(state.snapshot().len(), 2); + + state.clear(); + + assert!(state.snapshot().is_empty()); +} diff --git a/apps/desktop/src-tauri/tests/link_parsing.rs b/apps/desktop/src-tauri/tests/link_parsing.rs new file mode 100644 index 0000000..b2b293b --- /dev/null +++ b/apps/desktop/src-tauri/tests/link_parsing.rs @@ -0,0 +1,64 @@ +use desktop_lib::link::{ + extract_urls, + parse_argument, + parse_candidate, + percent_decode_if_needed, + push_unique, +}; + +#[test] +fn extract_urls_collects_unique_links() { + let args = vec![ + "--url=https://example.com".to_string(), + "--".to_string(), + "https://example.com".to_string(), + "http://other.test".to_string(), + "invalid".to_string(), + ]; + + let urls = extract_urls(&args); + assert_eq!(urls.len(), 2); + assert!(urls.contains(&"https://example.com/".to_string())); + assert!(urls.contains(&"http://other.test/".to_string())); +} + +#[test] +fn parse_argument_handles_flags_and_raw_values() { + assert_eq!( + parse_argument("https://example.com"), + Some("https://example.com/".to_string()) + ); + assert_eq!( + parse_argument("--url=http://example.org"), + Some("http://example.org/".to_string()) + ); + assert_eq!(parse_argument("--flag"), None); +} + +#[test] +fn parse_candidate_rejects_non_http_urls() { + assert_eq!( + parse_candidate(" https://domain.tld "), + Some("https://domain.tld/".to_string()) + ); + assert_eq!(parse_candidate("ftp://example.com"), None); + assert_eq!(parse_candidate("not a url"), None); +} + +#[test] +fn percent_decode_if_needed_decodes_common_sequences() { + let decoded = percent_decode_if_needed("https%3A%2F%2Fexample.com"); + assert_eq!(decoded, "https://example.com"); + + let untouched = percent_decode_if_needed("https://example.com"); + assert_eq!(untouched, "https://example.com"); +} + +#[test] +fn push_unique_only_adds_new_entries() { + let mut list = vec!["https://example.com".to_string()]; + push_unique(&mut list, "https://example.com".to_string()); + push_unique(&mut list, "http://second.com".to_string()); + assert_eq!(list.len(), 2); + assert!(list.contains(&"http://second.com".to_string())); +} diff --git a/apps/desktop/src-tauri/tests/routing_utils.rs b/apps/desktop/src-tauri/tests/routing_utils.rs new file mode 100644 index 0000000..63456f9 --- /dev/null +++ b/apps/desktop/src-tauri/tests/routing_utils.rs @@ -0,0 +1,42 @@ +use desktop_lib::routing::{add_profile_args, normalize_browser_key, normalize_url}; +use std::process::Command; + +fn command_args(command: &Command) -> Vec { + command + .get_args() + .map(|arg| arg.to_string_lossy().into_owned()) + .collect() +} + +#[test] +fn normalize_url_handles_missing_scheme_and_whitespace() { + assert_eq!(normalize_url(" https://example.com "), "https://example.com"); + assert_eq!(normalize_url("example.com"), "https://example.com"); + assert_eq!(normalize_url("http://already.com"), "http://already.com"); + assert_eq!(normalize_url(""), ""); + assert_eq!(normalize_url("not a url"), "not a url"); +} + +#[test] +fn normalize_browser_key_strips_non_alphanumeric() { + assert_eq!(normalize_browser_key("Google Chrome"), "googlechrome"); + assert_eq!(normalize_browser_key(" Edge " ), "edge"); + assert_eq!(normalize_browser_key("FIRE-FOX"), "firefox"); +} + +#[test] +fn add_profile_args_adds_expected_switches() { + let mut command = Command::new("browser"); + add_profile_args(&mut command, "Chrome", "Work Profile"); + let args = command_args(&command); + assert!(args.contains(&"--profile-directory=Work Profile".to_string())); + + let mut firefox = Command::new("browser"); + add_profile_args(&mut firefox, "Firefox", "Develop" ); + let args = command_args(&firefox); + assert_eq!(args, vec!["-P", "Develop"]); + + let mut empty = Command::new("browser"); + add_profile_args(&mut empty, "Chrome", " "); + assert!(command_args(&empty).is_empty()); +} From ef7e55be6c1f058f68c31c0bb9789e2b706178b9 Mon Sep 17 00:00:00 2001 From: theg Date: Tue, 21 Oct 2025 12:14:00 +0530 Subject: [PATCH 2/2] test: more granular tests --- apps/desktop/src-tauri/Cargo.lock | 1 + apps/desktop/src-tauri/Cargo.toml | 3 + .../tests/browser/chrome_profiles.rs | 93 +++++++++++++++++++ .../tests/browser/firefox_profiles.rs | 35 +++++++ .../kind_parse.rs} | 0 .../state.rs} | 0 .../{link_parsing.rs => link/parsing.rs} | 0 .../{routing_utils.rs => routing/utils.rs} | 0 8 files changed, 132 insertions(+) create mode 100644 apps/desktop/src-tauri/tests/browser/chrome_profiles.rs create mode 100644 apps/desktop/src-tauri/tests/browser/firefox_profiles.rs rename apps/desktop/src-tauri/tests/{browser_details_parse.rs => browser/kind_parse.rs} (100%) rename apps/desktop/src-tauri/tests/{diagnostics_state.rs => diagnostics/state.rs} (100%) rename apps/desktop/src-tauri/tests/{link_parsing.rs => link/parsing.rs} (100%) rename apps/desktop/src-tauri/tests/{routing_utils.rs => routing/utils.rs} (100%) diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index f1a3423..b933083 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -2563,6 +2563,7 @@ dependencies = [ "tauri-plugin-os", "tauri-plugin-single-instance", "tauri-plugin-store", + "tempfile", "tokio", "url", "uuid", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index d2146eb..3d4d7cf 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -35,6 +35,9 @@ percent-encoding = "2" tauri-plugin-os = "2.3.1" tauri-plugin-store = "2.4.0" +[dev-dependencies] +tempfile = "3.13.0" + [target.'cfg(target_os = "windows")'.dependencies] winreg = "0.52" diff --git a/apps/desktop/src-tauri/tests/browser/chrome_profiles.rs b/apps/desktop/src-tauri/tests/browser/chrome_profiles.rs new file mode 100644 index 0000000..1c49b87 --- /dev/null +++ b/apps/desktop/src-tauri/tests/browser/chrome_profiles.rs @@ -0,0 +1,93 @@ +#![cfg(target_os = "windows")] + +use desktop_lib::browser_details::{get_chrome_profiles, Browsers}; +use std::fs; +use std::io::Write; +use std::path::PathBuf; +use tempfile::TempDir; + +fn write_local_state(temp: &TempDir, relative: &str, contents: &str) -> PathBuf { + let mut path = temp.path().to_path_buf(); + path.push(relative); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).expect("failed to create Local State parent"); + } + let mut file = fs::File::create(&path).expect("failed to create Local State file"); + file.write_all(contents.as_bytes()) + .expect("failed to write Local State"); + path +} + +fn install_local_state(temp: &TempDir, browser: Browsers, json: &str) { + let relative = match browser { + Browsers::Chrome => "Google/Chrome/User Data/Local State", + Browsers::Edge => "Microsoft/Edge/User Data/Local State", + Browsers::Brave => "BraveSoftware/Brave-Browser/User Data/Local State", + _ => panic!("unexpected browser for chrome profile test"), + }; + write_local_state(temp, relative, json); +} + +fn with_localappdata(temp: &TempDir, f: F) { + use std::env; + let original = env::var_os("LOCALAPPDATA"); + env::set_var("LOCALAPPDATA", temp.path()); + f(); + match original { + Some(val) => env::set_var("LOCALAPPDATA", val), + None => env::remove_var("LOCALAPPDATA"), + } +} + +#[test] +fn reads_profiles_and_adds_default_when_missing() { + let temp = TempDir::new().expect("temp dir"); + let local_state = r#"{ + "profile": { + "info_cache": { + "Profile 1": { + "profile_dir": "Profile 1", + "name": "Personal" + }, + "Profile 2": { + "gaia_name": "Work" + } + } + } + }"#; + install_local_state(&temp, Browsers::Chrome, local_state); + + with_localappdata(&temp, || { + let profiles = get_chrome_profiles(Browsers::Chrome).expect("profiles"); + let labels: Vec<_> = profiles.iter().map(|p| &p.display_name).collect(); + assert!(labels.contains(&"Personal".to_string())); + assert!(labels.contains(&"Work".to_string())); + assert!(labels.contains(&"Default".to_string())); + }); +} + +#[test] +fn deduplicates_profile_directories() { + let temp = TempDir::new().expect("temp dir"); + let local_state = r#"{ + "profile": { + "info_cache": { + "Profile 1": { + "profile_dir": "Profile 1", + "name": "Personal" + }, + "Duplicate": { + "profile_dir": "Profile 1", + "name": "Duplicate" + } + } + } + }"#; + install_local_state(&temp, Browsers::Chrome, local_state); + + with_localappdata(&temp, || { + let profiles = get_chrome_profiles(Browsers::Chrome).expect("profiles"); + let dirs: Vec<_> = profiles.iter().map(|p| &p.directory).collect(); + assert_eq!(dirs.iter().filter(|d| d.as_str() == "Profile 1").count(), 1); + }); +} diff --git a/apps/desktop/src-tauri/tests/browser/firefox_profiles.rs b/apps/desktop/src-tauri/tests/browser/firefox_profiles.rs new file mode 100644 index 0000000..4e15fc2 --- /dev/null +++ b/apps/desktop/src-tauri/tests/browser/firefox_profiles.rs @@ -0,0 +1,35 @@ +#![cfg(target_os = "windows")] + +use desktop_lib::browser_details::get_firefox_profiles; +use std::fs; +use tempfile::TempDir; + +fn with_localappdata(temp: &TempDir, f: F) { + use std::env; + let original = env::var_os("LOCALAPPDATA"); + env::set_var("LOCALAPPDATA", temp.path()); + f(); + match original { + Some(val) => env::set_var("LOCALAPPDATA", val), + None => env::remove_var("LOCALAPPDATA"), + } +} + +#[test] +fn discovers_profile_directories() { + let temp = TempDir::new().expect("temp dir"); + let base = temp.path().join("Mozilla/Firefox/Profiles"); + fs::create_dir_all(base.join("abcd.default-release")).expect("create profile dir"); + fs::create_dir_all(base.join("custom.work" )).expect("create profile dir"); + // include a file to ensure non-dirs skipped + fs::write(base.join("not_a_dir"), b"noop").expect("create dummy file"); + + with_localappdata(&temp, || { + let mut profiles = get_firefox_profiles().expect("profiles"); + profiles.sort_by(|a, b| a.display_name.cmp(&b.display_name)); + let names: Vec<_> = profiles.iter().map(|p| &p.display_name).collect(); + assert!(names.contains(&"abcd.default-release".to_string())); + assert!(names.contains(&"custom.work".to_string())); + assert_eq!(profiles.len(), 2); + }); +} diff --git a/apps/desktop/src-tauri/tests/browser_details_parse.rs b/apps/desktop/src-tauri/tests/browser/kind_parse.rs similarity index 100% rename from apps/desktop/src-tauri/tests/browser_details_parse.rs rename to apps/desktop/src-tauri/tests/browser/kind_parse.rs diff --git a/apps/desktop/src-tauri/tests/diagnostics_state.rs b/apps/desktop/src-tauri/tests/diagnostics/state.rs similarity index 100% rename from apps/desktop/src-tauri/tests/diagnostics_state.rs rename to apps/desktop/src-tauri/tests/diagnostics/state.rs diff --git a/apps/desktop/src-tauri/tests/link_parsing.rs b/apps/desktop/src-tauri/tests/link/parsing.rs similarity index 100% rename from apps/desktop/src-tauri/tests/link_parsing.rs rename to apps/desktop/src-tauri/tests/link/parsing.rs diff --git a/apps/desktop/src-tauri/tests/routing_utils.rs b/apps/desktop/src-tauri/tests/routing/utils.rs similarity index 100% rename from apps/desktop/src-tauri/tests/routing_utils.rs rename to apps/desktop/src-tauri/tests/routing/utils.rs