//! 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()); } }