Skip to content

Commit 814309f

Browse files
test: cover high-value scanners, webdav, and editor composables
Add frontend tests for project editor, auto-scanner settings, and keyboard navigation. Expand Rust coverage for JetBrains/VS Code recent history, analyzer path helpers, and WebDAV upload/URL helpers. Fix $USER_HOME$ recent-path filtering and WebDAV double-slash URL joins uncovered by the new tests.
1 parent 8feba83 commit 814309f

9 files changed

Lines changed: 1407 additions & 17 deletions

File tree

src-tauri/src/analyzer/paths.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,58 @@ pub fn strip_windows_verbatim_prefix(path: &str) -> String {
2929
}
3030
path.to_string()
3131
}
32+
33+
#[cfg(test)]
34+
mod tests {
35+
use std::path::Path;
36+
37+
use super::{
38+
canonical_or_original, display_path, normalize_path_key, strip_windows_verbatim_prefix,
39+
};
40+
41+
#[test]
42+
fn strip_windows_verbatim_prefix_removes_drive_and_unc_prefixes() {
43+
assert_eq!(
44+
strip_windows_verbatim_prefix(r"\\?\E:\Projects\CodeNest"),
45+
r"E:\Projects\CodeNest"
46+
);
47+
assert_eq!(
48+
strip_windows_verbatim_prefix(r"\\?\UNC\server\share\CodeNest"),
49+
r"\\server\share\CodeNest"
50+
);
51+
assert_eq!(
52+
strip_windows_verbatim_prefix(r"E:\Projects\CodeNest"),
53+
r"E:\Projects\CodeNest"
54+
);
55+
}
56+
57+
#[test]
58+
fn normalize_path_key_unifies_slashes_and_trims_trailing_separators() {
59+
let key = normalize_path_key(Path::new(r"E:\Projects\CodeNest\"));
60+
assert!(!key.ends_with('/'));
61+
assert!(key.contains("Projects") || key.contains("projects"));
62+
assert!(key.contains('/'));
63+
assert!(!key.contains('\\'));
64+
}
65+
66+
#[test]
67+
#[cfg(windows)]
68+
fn normalize_path_key_lowercases_on_windows() {
69+
let key = normalize_path_key(Path::new(r"E:\Projects\CodeNest"));
70+
assert_eq!(key, key.to_ascii_lowercase());
71+
}
72+
73+
#[test]
74+
fn display_path_strips_verbatim_prefix() {
75+
assert_eq!(
76+
display_path(Path::new(r"\\?\E:\Projects\CodeNest")),
77+
r"E:\Projects\CodeNest"
78+
);
79+
}
80+
81+
#[test]
82+
fn canonical_or_original_keeps_missing_paths() {
83+
let missing = Path::new(r"E:\definitely-missing-codenest-path-xyz");
84+
assert_eq!(canonical_or_original(missing), missing.to_path_buf());
85+
}
86+
}

src-tauri/src/recent/jetbrains.rs

Lines changed: 145 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,15 @@ pub fn collect_from_jetbrains(config_root: Option<&str>) -> Vec<RecentProject> {
4949
let Some(raw_path) = project_entry.attribute("key") else {
5050
continue;
5151
};
52-
if raw_path.contains('$')
53-
|| raw_path.contains("light-edit")
54-
|| raw_path.contains("scratches")
55-
{
52+
// light-edit / scratches are transient JetBrains workspaces.
53+
// Expand $USER_HOME$ first, then drop any still-unresolved macros.
54+
if raw_path.contains("light-edit") || raw_path.contains("scratches") {
5655
continue;
5756
}
5857
let normalized = normalize_jetbrains_path(raw_path, &home);
58+
if normalized.contains('$') {
59+
continue;
60+
}
5961
candidates.push(RecentProject {
6062
path: normalized,
6163
ide: ide.clone(),
@@ -92,3 +94,142 @@ fn map_jetbrains_ide(raw_name: &str) -> Option<String> {
9294
};
9395
Some(value.to_string())
9496
}
97+
98+
#[cfg(test)]
99+
mod tests {
100+
use std::{
101+
fs,
102+
path::Path,
103+
time::{SystemTime, UNIX_EPOCH},
104+
};
105+
106+
use crate::analyzer::normalize_path_key;
107+
108+
use super::{collect_from_jetbrains, map_jetbrains_ide, normalize_jetbrains_path};
109+
110+
fn unique_temp_dir(label: &str) -> std::path::PathBuf {
111+
let unique = SystemTime::now()
112+
.duration_since(UNIX_EPOCH)
113+
.expect("system time should be valid")
114+
.as_nanos();
115+
std::env::temp_dir().join(format!(
116+
"codenest-jetbrains-{label}-{}-{unique}",
117+
std::process::id()
118+
))
119+
}
120+
121+
fn write_recent_projects_xml(product_dir: &Path, entries: &[&str]) {
122+
let options_dir = product_dir.join("options");
123+
fs::create_dir_all(&options_dir).expect("options dir should be created");
124+
let mut body = String::from(
125+
r#"<application><component name="RecentProjectsManager"><option name="additionalInfo"><map>"#,
126+
);
127+
for entry in entries {
128+
body.push_str(&format!(r#"<entry key="{entry}"/>"#));
129+
}
130+
body.push_str("</map></option></component></application>");
131+
fs::write(options_dir.join("recentProjects.xml"), body)
132+
.expect("recentProjects.xml should be written");
133+
}
134+
135+
#[test]
136+
fn collect_from_jetbrains_returns_empty_for_missing_root() {
137+
assert!(collect_from_jetbrains(None).is_empty());
138+
assert!(collect_from_jetbrains(Some("")).is_empty());
139+
assert!(collect_from_jetbrains(Some(
140+
r"E:\definitely-missing-jetbrains-config-root"
141+
))
142+
.is_empty());
143+
}
144+
145+
#[test]
146+
fn map_jetbrains_ide_recognizes_common_products() {
147+
assert_eq!(
148+
map_jetbrains_ide("IntelliJIdea").as_deref(),
149+
Some("intellij-idea")
150+
);
151+
assert_eq!(map_jetbrains_ide("WebStorm").as_deref(), Some("webstorm"));
152+
assert_eq!(map_jetbrains_ide("GoLand").as_deref(), Some("goLand"));
153+
assert_eq!(map_jetbrains_ide("UnknownIde"), None);
154+
}
155+
156+
#[test]
157+
fn normalize_jetbrains_path_expands_user_home() {
158+
let home = Path::new(r"C:\Users\example");
159+
let normalized = normalize_jetbrains_path("$USER_HOME$/Projects/CodeNest", home);
160+
#[cfg(windows)]
161+
assert_eq!(normalized, r"C:\Users\example\Projects\CodeNest");
162+
#[cfg(not(windows))]
163+
assert_eq!(normalized, "C:\\Users\\example/Projects/CodeNest");
164+
}
165+
166+
#[test]
167+
fn collect_from_jetbrains_reads_existing_projects_and_skips_transients() {
168+
let root = unique_temp_dir("collect");
169+
let config_root = root.join("config");
170+
let project = root.join("project_a");
171+
fs::create_dir_all(&project).expect("project dir should be created");
172+
fs::create_dir_all(&config_root).expect("config root should be created");
173+
174+
let product_dir = config_root.join("WebStorm2024.1");
175+
let project_key = project.to_string_lossy().replace('\\', "/");
176+
write_recent_projects_xml(
177+
&product_dir,
178+
&[
179+
project_key.as_str(),
180+
"$USER_HOME$/light-edit",
181+
"/tmp/scratches/demo",
182+
"$APPLICATION_CONFIG_DIR$/system",
183+
],
184+
);
185+
186+
let items = collect_from_jetbrains(Some(&config_root.to_string_lossy()));
187+
assert_eq!(items.len(), 1);
188+
assert_eq!(items[0].ide.as_deref(), Some("webstorm"));
189+
assert_eq!(
190+
normalize_path_key(Path::new(&items[0].path)),
191+
normalize_path_key(&project)
192+
);
193+
194+
let _ = fs::remove_dir_all(root);
195+
}
196+
197+
#[test]
198+
fn collect_from_jetbrains_expands_user_home_macro() {
199+
let home = dirs::home_dir().expect("home dir should exist for jetbrains macro test");
200+
let project = home.join(format!(
201+
"codenest-jetbrains-home-project-{}-{}",
202+
std::process::id(),
203+
SystemTime::now()
204+
.duration_since(UNIX_EPOCH)
205+
.expect("system time should be valid")
206+
.as_nanos()
207+
));
208+
fs::create_dir_all(&project).expect("home project dir should be created");
209+
210+
let root = unique_temp_dir("home-macro");
211+
let config_root = root.join("config");
212+
fs::create_dir_all(&config_root).expect("config root should be created");
213+
let product_dir = config_root.join("IntelliJIdea2023.3");
214+
let relative = project
215+
.strip_prefix(&home)
216+
.expect("project should be under home")
217+
.to_string_lossy()
218+
.replace('\\', "/");
219+
write_recent_projects_xml(
220+
&product_dir,
221+
&[&format!("$USER_HOME$/{relative}")],
222+
);
223+
224+
let items = collect_from_jetbrains(Some(&config_root.to_string_lossy()));
225+
assert_eq!(items.len(), 1);
226+
assert_eq!(items[0].ide.as_deref(), Some("intellij-idea"));
227+
assert_eq!(
228+
normalize_path_key(Path::new(&items[0].path)),
229+
normalize_path_key(&project)
230+
);
231+
232+
let _ = fs::remove_dir_all(project);
233+
let _ = fs::remove_dir_all(root);
234+
}
235+
}

src-tauri/src/recent/vscode.rs

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ struct VscodeRecentEntry {
9797
#[derive(Deserialize)]
9898
#[serde(untagged)]
9999
enum VscodeWorkspace {
100-
Object { config_path: String },
100+
Object {
101+
#[serde(rename = "configPath")]
102+
config_path: String,
103+
},
101104
String(String),
102105
}
103106

@@ -109,3 +112,143 @@ impl VscodeWorkspace {
109112
}
110113
}
111114
}
115+
116+
#[cfg(test)]
117+
mod tests {
118+
use std::{
119+
fs,
120+
path::{Path, PathBuf},
121+
time::{SystemTime, UNIX_EPOCH},
122+
};
123+
124+
use rusqlite::Connection;
125+
use serde_json::json;
126+
127+
use crate::analyzer::normalize_path_key;
128+
129+
use super::collect_from_vscode;
130+
131+
fn unique_temp_dir(label: &str) -> PathBuf {
132+
let unique = SystemTime::now()
133+
.duration_since(UNIX_EPOCH)
134+
.expect("system time should be valid")
135+
.as_nanos();
136+
std::env::temp_dir().join(format!(
137+
"codenest-vscode-{label}-{}-{unique}",
138+
std::process::id()
139+
))
140+
}
141+
142+
fn path_to_file_uri(path: &Path) -> String {
143+
let url = url::Url::from_file_path(path).expect("path should convert to file uri");
144+
url.into()
145+
}
146+
147+
fn write_vscdb(db_path: &Path, history_json: &str) {
148+
if let Some(parent) = db_path.parent() {
149+
fs::create_dir_all(parent).expect("db parent should be created");
150+
}
151+
let connection = Connection::open(db_path).expect("sqlite db should open");
152+
connection
153+
.execute(
154+
"CREATE TABLE ItemTable (key TEXT PRIMARY KEY, value TEXT)",
155+
[],
156+
)
157+
.expect("ItemTable should be created");
158+
connection
159+
.execute(
160+
"INSERT INTO ItemTable (key, value) VALUES (?1, ?2)",
161+
("history.recentlyOpenedPathsList", history_json),
162+
)
163+
.expect("history row should be inserted");
164+
}
165+
166+
#[test]
167+
fn collect_from_vscode_returns_empty_for_missing_db() {
168+
assert!(collect_from_vscode(None).is_empty());
169+
assert!(collect_from_vscode(Some("")).is_empty());
170+
assert!(collect_from_vscode(Some(r"E:\missing-state.vscdb")).is_empty());
171+
}
172+
173+
#[test]
174+
fn collect_from_vscode_reads_folder_file_and_workspace_entries() {
175+
let root = unique_temp_dir("entries");
176+
let project = root.join("project");
177+
let file_project = root.join("file-project");
178+
let workspace_project = root.join("workspace-project");
179+
fs::create_dir_all(&project).expect("project dir should be created");
180+
fs::create_dir_all(&file_project).expect("file project dir should be created");
181+
fs::create_dir_all(&workspace_project).expect("workspace project dir should be created");
182+
183+
let file_uri = path_to_file_uri(&file_project.join("main.ts"));
184+
let workspace_uri = path_to_file_uri(&workspace_project.join("demo.code-workspace"));
185+
let history = json!({
186+
"entries": [
187+
{ "folderUri": path_to_file_uri(&project) },
188+
{ "fileUri": file_uri },
189+
{ "workspace": { "configPath": workspace_uri } },
190+
{
191+
"folderUri": path_to_file_uri(&root.join("remote-only")),
192+
"remoteAuthority": "ssh-remote+devbox"
193+
}
194+
]
195+
})
196+
.to_string();
197+
198+
let db_path = root.join("state.vscdb");
199+
write_vscdb(&db_path, &history);
200+
201+
let mut items = collect_from_vscode(Some(&db_path.to_string_lossy()));
202+
items.sort_by(|a, b| a.path.cmp(&b.path));
203+
204+
let paths: Vec<_> = items
205+
.iter()
206+
.map(|item| normalize_path_key(Path::new(&item.path)))
207+
.collect();
208+
209+
assert!(paths.contains(&normalize_path_key(&project)));
210+
assert!(paths.contains(&normalize_path_key(&file_project)));
211+
assert!(paths.contains(&normalize_path_key(&workspace_project)));
212+
assert_eq!(items.len(), 3);
213+
assert!(items.iter().all(|item| item.ide.is_none()));
214+
215+
let _ = fs::remove_dir_all(root);
216+
}
217+
218+
#[test]
219+
fn collect_from_vscode_accepts_string_workspace_entries() {
220+
let root = unique_temp_dir("workspace-string");
221+
let workspace_project = root.join("ws");
222+
fs::create_dir_all(&workspace_project).expect("workspace dir should be created");
223+
let workspace_uri = path_to_file_uri(&workspace_project.join("app.code-workspace"));
224+
let history = json!({
225+
"entries": [
226+
{ "workspace": workspace_uri }
227+
]
228+
})
229+
.to_string();
230+
231+
let db_path = root.join("state.vscdb");
232+
write_vscdb(&db_path, &history);
233+
234+
let items = collect_from_vscode(Some(&db_path.to_string_lossy()));
235+
assert_eq!(items.len(), 1);
236+
assert_eq!(
237+
normalize_path_key(Path::new(&items[0].path)),
238+
normalize_path_key(&workspace_project)
239+
);
240+
241+
let _ = fs::remove_dir_all(root);
242+
}
243+
244+
#[test]
245+
fn collect_from_vscode_returns_empty_for_invalid_history_json() {
246+
let root = unique_temp_dir("invalid-json");
247+
let db_path = root.join("state.vscdb");
248+
write_vscdb(&db_path, "{not-json");
249+
250+
assert!(collect_from_vscode(Some(&db_path.to_string_lossy())).is_empty());
251+
252+
let _ = fs::remove_dir_all(root);
253+
}
254+
}

0 commit comments

Comments
 (0)