Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
15 changes: 10 additions & 5 deletions apps/desktop/src-tauri/src/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ fn dispatch_urls(app: &AppHandle, urls: Vec<String>, origin: LinkSource) {
});
}

fn extract_urls(args: &[String]) -> Vec<String> {
/// Extracts unique http(s) URLs from command-line arguments.
pub fn extract_urls(args: &[String]) -> Vec<String> {
let mut collected = Vec::new();
let mut after_delimiter = false;

Expand Down Expand Up @@ -109,7 +110,8 @@ fn extract_urls(args: &[String]) -> Vec<String> {
collected
}

fn parse_argument(arg: &str) -> Option<String> {
/// Parses an individual CLI argument for a url candidate.
pub fn parse_argument(arg: &str) -> Option<String> {
if let Some(candidate) = parse_candidate(arg) {
return Some(candidate);
}
Expand All @@ -126,7 +128,8 @@ fn parse_argument(arg: &str) -> Option<String> {
None
}

fn parse_candidate(input: &str) -> Option<String> {
/// Parses a raw string, returning a normalized http(s) URL if valid.
pub fn parse_candidate(input: &str) -> Option<String> {
let trimmed = input.trim_matches(|c| matches!(c, '"' | '\''));
if trimmed.is_empty() {
return None;
Expand All @@ -149,7 +152,8 @@ fn parse_candidate(input: &str) -> Option<String> {
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());
Expand All @@ -158,7 +162,8 @@ fn percent_decode_if_needed(input: &str) -> Cow<'_, str> {
Cow::Borrowed(input)
}

fn push_unique(list: &mut Vec<String>, value: String) {
/// Pushes a value into the list when it does not already exist.
pub fn push_unique(list: &mut Vec<String>, value: String) {
if !list.iter().any(|existing| existing == &value) {
list.push(value);
}
Expand Down
9 changes: 6 additions & 3 deletions apps/desktop/src-tauri/src/routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -386,7 +387,8 @@ fn resolve_browser_path(name: &str) -> Option<PathBuf> {
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())
Expand Down Expand Up @@ -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;
Expand Down
93 changes: 93 additions & 0 deletions apps/desktop/src-tauri/tests/browser/chrome_profiles.rs
Original file line number Diff line number Diff line change
@@ -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<F: FnOnce()>(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);
});
}
35 changes: 35 additions & 0 deletions apps/desktop/src-tauri/tests/browser/firefox_profiles.rs
Original file line number Diff line number Diff line change
@@ -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<F: FnOnce()>(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);
});
}
18 changes: 18 additions & 0 deletions apps/desktop/src-tauri/tests/browser/kind_parse.rs
Original file line number Diff line number Diff line change
@@ -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);
}
29 changes: 29 additions & 0 deletions apps/desktop/src-tauri/tests/diagnostics/state.rs
Original file line number Diff line number Diff line change
@@ -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());
}
64 changes: 64 additions & 0 deletions apps/desktop/src-tauri/tests/link/parsing.rs
Original file line number Diff line number Diff line change
@@ -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()));
}
Loading
Loading