""" Tests targeting specific uncovered lines in path_validator.py. Covers: - Line 140: Malformed Windows path on win32 - Line 153: safe_join returns None for unix absolute path - Lines 157-162: Windows absolute path handling (drive letter parsing) - Lines 185-186: Windows system directory restrictions - Line 274: validate_model_path when validate_safe_path returns None - Line 308: validate_data_path when validate_safe_path returns None - Lines 375-385: Absolute config path validation (suffix + existence) - Line 400: Relative config path when validate_safe_path returns None """ from pathlib import Path from unittest.mock import patch import pytest from local_deep_research.security.path_validator import PathValidator # --------------------------------------------------------------------------- # Line 140: Malformed Windows path on win32 # The function does `import sys` locally (line 112), so we patch `sys.platform` # on the actual sys module. # --------------------------------------------------------------------------- class TestMalformedWindowsPath: """Line 140: raise ValueError('Malformed Windows path input')""" @patch("sys.platform", "win32") def test_malformed_windows_path_slash_colon(self): """'/C:/Windows' on win32 triggers the malformed path guard.""" with pytest.raises(ValueError, match="Malformed Windows path input"): PathValidator.validate_local_filesystem_path("/C:/Windows") @patch("sys.platform", "win32") def test_malformed_windows_path_backslash_colon(self): """'\\C:\\Windows' on win32 triggers the malformed path guard.""" with pytest.raises(ValueError, match="Malformed Windows path input"): PathValidator.validate_local_filesystem_path("\\C:\\Windows") # --------------------------------------------------------------------------- # Line 153: safe_join returns None for unix absolute path # --------------------------------------------------------------------------- class TestSafeJoinReturnsNoneUnix: """Line 153: raise ValueError('Invalid path - failed security validation')""" @patch( "local_deep_research.security.path_validator.safe_join", return_value=None, ) def test_safe_join_none_for_absolute_path(self, mock_safe_join): with pytest.raises(ValueError, match="failed security validation"): PathValidator.validate_local_filesystem_path("/home/user/docs") # --------------------------------------------------------------------------- # Lines 157-162: Windows absolute path handling (drive letter parsing) # On Linux, a path like 'C:\Users\test' is not absolute (no leading /), # so it enters the drive-letter branch at line 155. # --------------------------------------------------------------------------- class TestWindowsDriveLetterParsing: """Lines 157-162: Windows drive-letter absolute path branch.""" @patch("local_deep_research.security.path_validator.safe_join") def test_windows_drive_letter_path_success(self, mock_safe_join): """Drive-letter path where safe_join succeeds (lines 157-162).""" mock_safe_join.return_value = "/tmp/fake_resolved" result = PathValidator.validate_local_filesystem_path("C:\\Users\\test") assert result is not None @patch( "local_deep_research.security.path_validator.safe_join", return_value=None, ) def test_windows_drive_letter_safe_join_none(self, mock_safe_join): """Drive-letter path where safe_join returns None (line 160-161).""" with pytest.raises(ValueError, match="failed security validation"): PathValidator.validate_local_filesystem_path("C:\\Users\\test") # --------------------------------------------------------------------------- # Lines 185-186: Windows system directory restrictions # When sys.platform == "win32" and restricted_dirs is None, extra Windows # system dirs are added. We mock safe_join and Path.resolve so that the # validated path falls inside one of those restricted dirs. # --------------------------------------------------------------------------- class TestWindowsSystemDirRestrictions: """Lines 184-193: Windows restricted directories added on win32.""" @patch("sys.platform", "win32") @patch("local_deep_research.security.path_validator.safe_join") def test_windows_restricted_dirs_block_access(self, mock_safe_join): """On win32, C:\\Windows is restricted by default.""" # The path 'C:\Windows\System32\cmd.exe' has len>2, [1]==':', # so it enters the drive-letter branch. # It does NOT start with / or \, so the malformed guard (line 135-140) # is skipped. fake = Path("C:\\Windows\\System32\\cmd.exe") mock_safe_join.return_value = str(fake) with patch.object(Path, "resolve", return_value=fake): with patch.object(Path, "is_relative_to", return_value=True): with pytest.raises( ValueError, match="Cannot access system directories" ): PathValidator.validate_local_filesystem_path( "C:\\Windows\\System32\\cmd.exe" ) # --------------------------------------------------------------------------- # Line 274: validate_model_path - validate_safe_path returns None # --------------------------------------------------------------------------- class TestValidateModelPathNone: """Line 274: raise ValueError('Invalid model path')""" @patch.object(PathValidator, "validate_safe_path", return_value=None) def test_model_path_returns_none(self, mock_vsp): with pytest.raises(ValueError, match="Invalid model path"): PathValidator.validate_model_path( "bad_model.bin", model_root="/tmp" ) # --------------------------------------------------------------------------- # Line 308: validate_data_path - validate_safe_path returns None # --------------------------------------------------------------------------- class TestValidateDataPathNone: """Line 308: raise ValueError('Invalid data path')""" @patch.object(PathValidator, "validate_safe_path", return_value=None) def test_data_path_returns_none(self, mock_vsp): with pytest.raises(ValueError, match="Invalid data path"): PathValidator.validate_data_path("bad_file.dat", data_root="/tmp") # --------------------------------------------------------------------------- # Lines 375-385: Absolute config path validation # safe_join("/", config_path) returns None when config_path starts with "/", # so on Unix these lines are unreachable through normal flow. # We mock safe_join to return a value so we can exercise lines 375-385. # --------------------------------------------------------------------------- class TestAbsoluteConfigPath: """Lines 375-385: absolute config path suffix and existence checks.""" @patch("local_deep_research.security.path_validator.safe_join") def test_absolute_config_bad_extension(self, mock_safe_join): """Line 378-379: suffix not in CONFIG_EXTENSIONS.""" mock_safe_join.return_value = "/tmp/config.exe" with pytest.raises(ValueError, match="Invalid config file type"): PathValidator.validate_config_path("/tmp/config.exe") @patch("local_deep_research.security.path_validator.safe_join") def test_absolute_config_does_not_exist(self, mock_safe_join, tmp_path): """Line 382-383: valid extension but file does not exist.""" nonexistent = str(tmp_path / "missing.json") mock_safe_join.return_value = nonexistent with pytest.raises(ValueError, match="Config file not found"): PathValidator.validate_config_path(nonexistent) @patch("local_deep_research.security.path_validator.safe_join") def test_absolute_config_valid(self, mock_safe_join, tmp_path): """Lines 375-385: happy path - valid extension, file exists.""" cfg = tmp_path / "settings.yaml" cfg.write_text("key: value") cfg_str = str(cfg) mock_safe_join.return_value = cfg_str result = PathValidator.validate_config_path(cfg_str) assert result == Path(cfg_str) # --------------------------------------------------------------------------- # Line 400: Relative config path - validate_safe_path returns None # --------------------------------------------------------------------------- class TestRelativeConfigPathNone: """Line 400: raise ValueError('Invalid config path: ...')""" @patch.object(PathValidator, "validate_safe_path", return_value=None) def test_relative_config_path_none(self, mock_vsp): with pytest.raises(ValueError, match="Invalid config path"): PathValidator.validate_config_path( "my_config.yaml", config_root="/tmp" )