""" Tests for uncovered code paths in PathValidator. Targets: - validate_config_path: absolute path branch, restricted prefixes, null bytes - sanitize_for_filesystem_ops: non-absolute path, safe_join failure - validate_model_path: non-existent file, not-a-file, model_root creation - validate_local_filesystem_path: control characters, tilde expansion, relative path """ import os from pathlib import Path import pytest from local_deep_research.security.path_validator import PathValidator class TestValidateConfigPathAbsolute: """Tests for validate_config_path with absolute paths.""" def test_absolute_path_rejected_by_safe_join(self, tmp_path): """Absolute Unix paths are rejected by safe_join in validate_config_path. werkzeug safe_join("/", "/absolute/path") returns None because the second argument starts with "/" which is treated as escaping the base. This covers the `if safe_path is None: raise ValueError` branch. """ config_file = tmp_path / "test.json" config_file.write_text("{}") with pytest.raises(ValueError, match="Invalid"): PathValidator.validate_config_path(str(config_file)) def test_config_path_null_bytes(self): """Config path with null bytes raises ValueError.""" with pytest.raises(ValueError, match="Null bytes"): PathValidator.validate_config_path("/some/path\x00.json") def test_config_path_empty_string(self): """Empty config path raises ValueError.""" with pytest.raises(ValueError, match="Invalid config path input"): PathValidator.validate_config_path("") def test_config_path_non_string(self): """Non-string config path raises ValueError.""" with pytest.raises(ValueError, match="Invalid config path input"): PathValidator.validate_config_path(None) # type: ignore def test_config_path_traversal_attempt(self): """Config path with .. raises ValueError.""" with pytest.raises(ValueError, match="traversal"): PathValidator.validate_config_path("../../../etc/passwd") class TestConfigPathRestrictedPrefixes: """Tests for restricted system directory prefixes in config path.""" @pytest.mark.parametrize( "path", [ "etc/passwd", "/etc/passwd", "proc/self/status", "sys/kernel", "dev/null", ], ) def test_restricted_prefixes_blocked(self, path): """Config paths targeting system directories are blocked.""" with pytest.raises(ValueError, match="restricted system directory"): PathValidator.validate_config_path(path) class TestConfigPathRelative: """Tests for validate_config_path with relative paths.""" def test_relative_config_path_with_valid_extension(self, tmp_path): """Relative config path resolves within config_root.""" config_file = tmp_path / "settings.yaml" config_file.write_text("key: value") result = PathValidator.validate_config_path( "settings.yaml", config_root=str(tmp_path) ) assert result == config_file def test_relative_config_path_invalid_extension(self, tmp_path): """Relative config path with wrong extension raises ValueError.""" with pytest.raises(ValueError, match="Invalid file type"): PathValidator.validate_config_path( "script.sh", config_root=str(tmp_path) ) class TestSanitizeForFilesystemOps: """Tests for sanitize_for_filesystem_ops.""" def test_valid_absolute_path(self, tmp_path): """Valid absolute path is returned unchanged.""" result = PathValidator.sanitize_for_filesystem_ops(tmp_path) assert result == tmp_path def test_non_absolute_path_raises(self): """Relative path raises ValueError.""" with pytest.raises(ValueError, match="Path must be absolute"): PathValidator.sanitize_for_filesystem_ops(Path("relative/path")) class TestValidateModelPath: """Tests for validate_model_path edge cases.""" def test_model_path_nonexistent_file(self, tmp_path): """Model path to nonexistent file raises ValueError.""" with pytest.raises(ValueError, match="Model file not found"): PathValidator.validate_model_path( "nonexistent.bin", model_root=str(tmp_path) ) def test_model_path_is_directory(self, tmp_path): """Model path pointing to directory raises ValueError.""" subdir = tmp_path / "model_dir" subdir.mkdir() with pytest.raises(ValueError, match="not a file"): PathValidator.validate_model_path( "model_dir", model_root=str(tmp_path) ) def test_model_path_valid_file(self, tmp_path): """Valid model file is returned.""" model_file = tmp_path / "model.bin" model_file.write_bytes(b"fake model data") result = PathValidator.validate_model_path( "model.bin", model_root=str(tmp_path) ) assert result == model_file def test_model_path_creates_root_directory(self, tmp_path): """Model root directory is created if it doesn't exist.""" new_root = tmp_path / "new_model_root" # The directory doesn't exist yet but will be created # Then the file won't exist, so it should raise with pytest.raises(ValueError, match="Model file not found"): PathValidator.validate_model_path( "model.bin", model_root=str(new_root) ) # But the directory should have been created assert new_root.exists() class TestValidateLocalFilesystemPath: """Tests for validate_local_filesystem_path edge cases.""" def test_control_characters_rejected(self): """Paths with control characters are rejected.""" with pytest.raises(ValueError, match="Control characters"): PathValidator.validate_local_filesystem_path("/tmp/test\x01file") def test_tilde_expansion(self): """Tilde is expanded to home directory.""" # Pass empty restricted_dirs to avoid /root being blocked in CI result = PathValidator.validate_local_filesystem_path( "~", restricted_dirs=[] ) assert result == Path.home().resolve() def test_tilde_with_subpath(self): """Tilde with subpath is expanded correctly.""" # Pass empty restricted_dirs to avoid /root being blocked in CI result = PathValidator.validate_local_filesystem_path( "~/Documents", restricted_dirs=[] ) expected = (Path.home() / "Documents").resolve() assert result == expected def test_relative_path_resolved_to_cwd(self): """Relative path is resolved relative to CWD.""" result = PathValidator.validate_local_filesystem_path("some_dir") expected = (Path(os.getcwd()) / "some_dir").resolve() assert result == expected def test_restricted_directories_blocked(self): """System directories like /etc are blocked.""" with pytest.raises( ValueError, match="Cannot access system directories" ): PathValidator.validate_local_filesystem_path("/etc/passwd") def test_custom_restricted_dirs(self, tmp_path): """Custom restricted directories are respected.""" restricted = tmp_path / "restricted" restricted.mkdir() with pytest.raises( ValueError, match="Cannot access system directories" ): PathValidator.validate_local_filesystem_path( str(restricted / "file.txt"), restricted_dirs=[restricted], ) def test_path_traversal_blocked(self): """Path traversal with .. is blocked.""" with pytest.raises(ValueError, match="Path traversal"): PathValidator.validate_local_filesystem_path("/tmp/../etc/passwd") def test_null_bytes_blocked(self): """Null bytes in path are blocked.""" with pytest.raises(ValueError, match="Null bytes"): PathValidator.validate_local_filesystem_path("/tmp/file\x00.txt") class TestValidateDataPath: """Tests for validate_data_path.""" def test_valid_data_path(self, tmp_path): """Valid relative data path is accepted.""" result = PathValidator.validate_data_path( "subdir/file.txt", str(tmp_path) ) assert result.parent.name == "subdir" assert result.name == "file.txt" def test_data_path_traversal_blocked(self, tmp_path): """Data path traversal is blocked.""" with pytest.raises(ValueError): PathValidator.validate_data_path("../../etc/passwd", str(tmp_path))