use super::*;
use crate::core::embed::Embedder;
use axum::extract::State;
use axum::http::StatusCode;
use axum::Json;
use std::sync::Arc;
#[tokio::test]
async fn validate_root_path_denylist_rejects_ssh() {
let home = dirs::home_dir().expect("home dir required for this test");
let ssh_path = home.join(".ssh");
if !ssh_path.is_dir() {
return;
}
use crate::core::registry::IndexRegistry;
let state = SearchAppState::new(IndexRegistry::new());
let embedder: Arc<dyn Embedder> = Arc::new(crate::core::embed::MockEmbedder::new(8));
state.install_embedder(embedder).await;
let state_arc = Arc::new(state);
let resp = create_index_handler(
State(Arc::clone(&state_arc)),
Json(CreateIndexRequest {
id: "sensitive-ssh".into(),
root_path: ssh_path,
include_paths: None,
exclude_globs: None,
extensions: None,
domain_terms: None,
path_filter: None,
include_docs: None,
respect_gitignore: None,
lexical_only: None,
skip_kg: None,
defer_embed: None,
extra_skip_dirs: None,
data_file_max_bytes: None,
}),
)
.await;
assert_eq!(
resp.status(),
StatusCode::BAD_REQUEST,
"~/.ssh must be refused with 400"
);
let body = axum::body::to_bytes(resp.into_body(), 65536)
.await
.expect("body");
let json: serde_json::Value = serde_json::from_slice(&body).expect("json");
let err = json.get("error").and_then(|v| v.as_str()).unwrap_or("");
assert!(
err.contains("indexing refused"),
"error must mention 'indexing refused', got: {err:?}"
);
}
#[tokio::test]
async fn validate_root_path_denylist_rejects_home() {
let home = dirs::home_dir().expect("home dir required");
if !home.is_dir() {
return;
}
let result = super::helpers::validate_root_path(&home).await;
assert!(
result.is_err(),
"$HOME itself must be rejected by validate_root_path"
);
}
#[test]
fn validate_root_path_denylist_rejects_tmp_literal_paths() {
use crate::allowlist::{is_denied, SENSITIVE_PATH_PREFIXES};
let tmp_subpaths: &[&str] = &[
"/tmp/ts-denylist-probe", "/private/tmp/ts-denylist-probe", "/var/folders/ts-denylist-probe", "/private/var/folders/ts-denylist-probe", ];
for subpath in tmp_subpaths {
let covered = SENSITIVE_PATH_PREFIXES
.iter()
.any(|prefix| subpath.starts_with(prefix));
assert!(
covered,
"test path {subpath:?} is not covered by any SENSITIVE_PATH_PREFIXES entry — \
update the test or the denylist"
);
}
for subpath in tmp_subpaths {
let path = std::path::Path::new(subpath);
assert!(
is_denied(path).is_some(),
"expected is_denied to block {subpath:?} (covers SENSITIVE_PATH_PREFIXES rule)"
);
}
}
#[tokio::test]
async fn validate_root_path_accepts_safe_project_dir() {
let candidate = [
std::path::Path::new("/usr/local/share"),
std::path::Path::new("/usr/share"),
std::path::Path::new("/opt"),
std::path::Path::new("/srv"),
]
.iter()
.find(|p| p.is_dir())
.copied();
if let Some(path) = candidate {
let result = super::helpers::validate_root_path(path).await;
assert!(
result.is_ok(),
"expected Ok for safe directory {:?}, got Err",
path
);
}
}
#[cfg(unix)]
#[tokio::test]
async fn validate_root_path_denylist_blocks_symlink_to_ssh() {
let home = dirs::home_dir().expect("home dir");
let ssh = home.join(".ssh");
if !ssh.is_dir() {
return; }
let base = std::env::current_dir().expect("cwd").join("target");
std::fs::create_dir_all(&base).ok();
let link = base.join(format!("ts-denylist-ssh-link-{}", std::process::id()));
let _ = std::fs::remove_file(&link);
if std::os::unix::fs::symlink(&ssh, &link).is_err() {
return; }
let result = super::helpers::validate_root_path(&link).await;
let _ = std::fs::remove_file(&link);
assert!(
result.is_err(),
"symlink to ~/.ssh must be refused (canonicalization must resolve the symlink)"
);
}