Files
2026-05-08 01:59:04 +02:00

191 lines
6.4 KiB
Rust

//! Integration tests for path normalization and containment.
//!
//! These tests use temporary directories to test real filesystem operations.
use dirigent_tools::config::SandboxConfig;
use dirigent_tools::path::{canonicalize_path, check_containment, validate_path, SymlinkPolicy};
use dirigent_tools::ToolError;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn test_validate_path_with_real_filesystem() {
// Create a temp directory structure
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path().join("project");
let src_dir = project_dir.join("src");
fs::create_dir_all(&src_dir).unwrap();
// Create a test file
let test_file = src_dir.join("main.rs");
fs::write(&test_file, "fn main() {}").unwrap();
// Create config
let mut config = SandboxConfig::default();
config.allowed_roots = vec![project_dir.clone()];
config.blocked_paths = vec!["**/.env".to_string()];
// Test: Valid path within allowed root
let result = validate_path(test_file.to_str().unwrap(), &config);
assert!(result.is_ok());
// Test: Path at the root level (should be allowed)
let root_file = project_dir.join("README.md");
fs::write(&root_file, "# Project").unwrap();
let result = validate_path(root_file.to_str().unwrap(), &config);
assert!(result.is_ok());
// Test: Path outside allowed roots
let outside_path = temp_dir.path().join("outside.txt");
fs::write(&outside_path, "outside").unwrap();
let result = validate_path(outside_path.to_str().unwrap(), &config);
assert!(result.is_err());
assert!(matches!(result, Err(ToolError::SandboxViolation { .. })));
}
#[test]
fn test_validate_path_blocked() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path().join("project");
fs::create_dir_all(&project_dir).unwrap();
// Create a .env file
let env_file = project_dir.join(".env");
fs::write(&env_file, "SECRET=value").unwrap();
// Create config with blocklist
let mut config = SandboxConfig::default();
config.allowed_roots = vec![project_dir.clone()];
config.blocked_paths = vec!["**/.env".to_string()];
// Test: Blocked path
let result = validate_path(env_file.to_str().unwrap(), &config);
assert!(result.is_err());
assert!(matches!(result, Err(ToolError::BlockedPath { .. })));
}
#[test]
fn test_validate_path_non_existent_for_write() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path().join("project");
fs::create_dir_all(&project_dir).unwrap();
// Non-existent file (for write operations)
let new_file = project_dir.join("new_file.txt");
let mut config = SandboxConfig::default();
config.allowed_roots = vec![project_dir.clone()];
// Test: Non-existent file within allowed root
let result = validate_path(new_file.to_str().unwrap(), &config);
assert!(result.is_ok());
}
#[test]
fn test_canonicalize_path_with_real_filesystem() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path().join("project");
fs::create_dir_all(&project_dir).unwrap();
let test_file = project_dir.join("test.txt");
fs::write(&test_file, "content").unwrap();
let policy = SymlinkPolicy::default();
// Test: Canonicalize existing file
let canonical = canonicalize_path(&test_file, &policy).unwrap();
assert!(canonical.is_absolute());
assert!(canonical.exists());
// Verify no ".." components
assert!(!canonical.to_string_lossy().contains(".."));
}
#[test]
fn test_containment_with_real_filesystem() {
let temp_dir = TempDir::new().unwrap();
let root_dir = temp_dir.path().join("root");
let subdir = root_dir.join("subdir");
fs::create_dir_all(&subdir).unwrap();
let file = subdir.join("file.txt");
fs::write(&file, "content").unwrap();
// Canonicalize paths
let canonical_root = dunce::canonicalize(&root_dir).unwrap();
let canonical_file = dunce::canonicalize(&file).unwrap();
// Test: File is contained in root
let result = check_containment(&canonical_file, &[canonical_root.clone()]);
assert!(result.is_ok());
// Test: Root is not strictly contained in itself
let result = check_containment(&canonical_root, &[canonical_root.clone()]);
assert!(result.is_err());
}
#[cfg(unix)]
#[test]
fn test_symlink_handling_unix() {
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new().unwrap();
let allowed_dir = temp_dir.path().join("allowed");
let outside_dir = temp_dir.path().join("outside");
fs::create_dir_all(&allowed_dir).unwrap();
fs::create_dir_all(&outside_dir).unwrap();
// Create a file outside the allowed root
let outside_file = outside_dir.join("secret.txt");
fs::write(&outside_file, "secret").unwrap();
// Create a symlink inside allowed root pointing outside
let symlink_path = allowed_dir.join("link_to_secret");
symlink(&outside_file, &symlink_path).unwrap();
let mut config = SandboxConfig::default();
config.allowed_roots = vec![dunce::canonicalize(&allowed_dir).unwrap()];
config.allow_symlink_escape = false; // Don't allow escapes
// Test: Symlink escape should be blocked
let result = validate_path(symlink_path.to_str().unwrap(), &config);
assert!(result.is_err());
}
#[cfg(windows)]
#[test]
fn test_windows_reserved_device_names() {
let policy = SymlinkPolicy::default();
// Reserved device names should be rejected
let reserved_names = vec!["CON", "PRN", "AUX", "NUL", "COM1", "COM2", "LPT1", "LPT2"];
for name in reserved_names {
let path = PathBuf::from(format!("C:\\{}", name));
let result = canonicalize_path(&path, &policy);
assert!(result.is_err(), "Should reject reserved name: {}", name);
}
}
#[cfg(windows)]
#[test]
fn test_windows_path_forms() {
// Test various Windows path forms with temp directory
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.txt");
fs::write(&test_file, "content").unwrap();
let policy = SymlinkPolicy::default();
// Test: Standard absolute path
let canonical = canonicalize_path(&test_file, &policy).unwrap();
assert!(canonical.is_absolute());
// Test: Drive letter normalization (uppercase)
let path_str = canonical.to_string_lossy();
if path_str.len() >= 2 && path_str.chars().nth(1) == Some(':') {
assert!(path_str.chars().nth(0).unwrap().is_ascii_uppercase());
}
}