"""High-value tests for security/path_validator.py - Path Validation.""" from pathlib import Path import pytest from local_deep_research.security.path_validator import PathValidator # --------------------------------------------------------------------------- # validate_safe_path() # --------------------------------------------------------------------------- class TestValidateSafePath: """Safe path validation with base directory containment.""" def test_null_bytes_rejected(self, tmp_path): with pytest.raises(ValueError, match="[Nn]ull"): PathValidator.validate_safe_path("file\x00.txt", tmp_path) def test_traversal_blocked(self, tmp_path): with pytest.raises(ValueError, match="traversal"): PathValidator.validate_safe_path("../../etc/passwd", tmp_path) def test_valid_relative_path(self, tmp_path): result = PathValidator.validate_safe_path("subdir/file.txt", tmp_path) assert result is not None assert str(tmp_path) in str(result) def test_extension_filtering_valid(self, tmp_path): result = PathValidator.validate_safe_path( "config.json", tmp_path, required_extensions=(".json", ".yaml") ) assert result is not None assert result.suffix == ".json" def test_extension_filtering_invalid(self, tmp_path): with pytest.raises(ValueError, match="file type"): PathValidator.validate_safe_path( "script.py", tmp_path, required_extensions=(".json",) ) def test_empty_input_rejected(self, tmp_path): with pytest.raises(ValueError, match="Invalid path input"): PathValidator.validate_safe_path("", tmp_path) def test_none_input_rejected(self, tmp_path): with pytest.raises(ValueError, match="Invalid path input"): PathValidator.validate_safe_path(None, tmp_path) def test_simple_filename(self, tmp_path): result = PathValidator.validate_safe_path("readme.txt", tmp_path) assert result.name == "readme.txt" # --------------------------------------------------------------------------- # validate_local_filesystem_path() # --------------------------------------------------------------------------- class TestValidateLocalFilesystemPath: """Local filesystem path validation.""" def test_etc_restricted(self): with pytest.raises(ValueError, match="system directories"): PathValidator.validate_local_filesystem_path("/etc/passwd") def test_sys_restricted(self): with pytest.raises(ValueError, match="system directories"): PathValidator.validate_local_filesystem_path("/sys/kernel") def test_proc_restricted(self): with pytest.raises(ValueError, match="system directories"): PathValidator.validate_local_filesystem_path("/proc/self/environ") def test_dev_restricted(self): with pytest.raises(ValueError, match="system directories"): PathValidator.validate_local_filesystem_path("/dev/null") def test_tilde_expansion(self): result = PathValidator.validate_local_filesystem_path( "~/Documents", restricted_dirs=[] ) assert str(result).startswith(str(Path.home())) def test_tilde_alone(self): result = PathValidator.validate_local_filesystem_path( "~", restricted_dirs=[] ) assert result == Path.home().resolve() def test_control_chars_rejected(self): with pytest.raises(ValueError, match="Control characters"): PathValidator.validate_local_filesystem_path("/tmp/file\x01name") def test_null_bytes_rejected(self): with pytest.raises(ValueError, match="[Nn]ull"): PathValidator.validate_local_filesystem_path("/tmp/file\x00name") def test_dotdot_blocked(self): with pytest.raises(ValueError, match="traversal"): PathValidator.validate_local_filesystem_path("/tmp/../etc/passwd") def test_absolute_unix_path_valid(self): result = PathValidator.validate_local_filesystem_path("/tmp") assert result.is_absolute() def test_relative_path_resolved(self): result = PathValidator.validate_local_filesystem_path("somedir") assert result.is_absolute() def test_empty_rejected(self): with pytest.raises(ValueError, match="Invalid path input"): PathValidator.validate_local_filesystem_path("") def test_none_rejected(self): with pytest.raises(ValueError, match="Invalid path input"): PathValidator.validate_local_filesystem_path(None) def test_custom_restricted_dirs(self): with pytest.raises(ValueError, match="system directories"): PathValidator.validate_local_filesystem_path( "/custom/restricted/path", restricted_dirs=[Path("/custom/restricted")], ) def test_boot_restricted(self): with pytest.raises(ValueError, match="system directories"): PathValidator.validate_local_filesystem_path("/boot/vmlinuz") def test_var_log_restricted(self): with pytest.raises(ValueError, match="system directories"): PathValidator.validate_local_filesystem_path("/var/log/syslog") # --------------------------------------------------------------------------- # sanitize_for_filesystem_ops() # --------------------------------------------------------------------------- class TestSanitizeForFilesystemOps: """Re-sanitization for static analyzers.""" def test_non_absolute_rejected(self): with pytest.raises(ValueError, match="absolute"): PathValidator.sanitize_for_filesystem_ops(Path("relative/path")) def test_absolute_path_passes(self): result = PathValidator.sanitize_for_filesystem_ops(Path("/tmp/safe")) assert result == Path("/tmp/safe") def test_root_path(self): result = PathValidator.sanitize_for_filesystem_ops(Path("/")) assert result == Path("/") # --------------------------------------------------------------------------- # validate_config_path() # --------------------------------------------------------------------------- class TestValidateConfigPath: """Config file path validation.""" def test_etc_prefix_rejected(self): with pytest.raises(ValueError, match="restricted"): PathValidator.validate_config_path("/etc/config.json") def test_proc_prefix_rejected(self): with pytest.raises(ValueError, match="restricted"): PathValidator.validate_config_path("/proc/config.yaml") def test_sys_prefix_rejected(self): with pytest.raises(ValueError, match="restricted"): PathValidator.validate_config_path("/sys/config.toml") def test_dev_prefix_rejected(self): with pytest.raises(ValueError, match="restricted"): PathValidator.validate_config_path("/dev/config.ini") def test_dotdot_blocked(self): with pytest.raises(ValueError, match="traversal"): PathValidator.validate_config_path("../etc/passwd") def test_empty_rejected(self): with pytest.raises(ValueError, match="Invalid config path"): PathValidator.validate_config_path("") def test_none_rejected(self): with pytest.raises(ValueError, match="Invalid config path"): PathValidator.validate_config_path(None) def test_null_bytes_rejected(self): with pytest.raises(ValueError, match="[Nn]ull"): PathValidator.validate_config_path("/tmp/config\x00.json") def test_relative_path_uses_config_root(self, tmp_path): result = PathValidator.validate_config_path( "settings.json", config_root=str(tmp_path) ) assert str(tmp_path) in str(result) def test_invalid_extension_rejected(self, tmp_path): # Create a file with invalid extension test_file = tmp_path / "script.py" test_file.write_text("data") with pytest.raises(ValueError, match="file type"): PathValidator.validate_config_path( "script.py", config_root=str(tmp_path) ) def test_relative_config_path_with_config_root(self, tmp_path): cfg = tmp_path / "cfg.json" cfg.write_text("{}") # validate_config_path uses safe_join("/", path) which requires # stripping the leading slash; pass relative-to-root form result = PathValidator.validate_config_path( "cfg.json", config_root=str(tmp_path) ) assert result.name == "cfg.json" def test_absolute_config_path_rejected_for_invalid_extension( self, tmp_path ): script = tmp_path / "config.py" script.write_text("data") with pytest.raises(ValueError, match="file type|Invalid absolute path"): PathValidator.validate_config_path(str(script)) # --------------------------------------------------------------------------- # SAFE_PATH_PATTERN regex # --------------------------------------------------------------------------- class TestSafePathPattern: """SAFE_PATH_PATTERN regex validation.""" def test_alphanumeric(self): assert PathValidator.SAFE_PATH_PATTERN.match("hello123") def test_dots_and_dashes(self): assert PathValidator.SAFE_PATH_PATTERN.match("file-name.txt") def test_slashes(self): assert PathValidator.SAFE_PATH_PATTERN.match("dir/sub/file.txt") def test_spaces_rejected(self): assert PathValidator.SAFE_PATH_PATTERN.match("hello world") is None def test_special_chars_rejected(self): assert PathValidator.SAFE_PATH_PATTERN.match("file;rm -rf") is None def test_shell_injection_rejected(self): assert PathValidator.SAFE_PATH_PATTERN.match("$(whoami)") is None def test_underscore_allowed(self): assert PathValidator.SAFE_PATH_PATTERN.match("my_file.txt") # --------------------------------------------------------------------------- # CONFIG_EXTENSIONS # --------------------------------------------------------------------------- class TestConfigExtensions: def test_json_included(self): assert ".json" in PathValidator.CONFIG_EXTENSIONS def test_yaml_included(self): assert ".yaml" in PathValidator.CONFIG_EXTENSIONS assert ".yml" in PathValidator.CONFIG_EXTENSIONS def test_toml_included(self): assert ".toml" in PathValidator.CONFIG_EXTENSIONS