diff --git a/tests/routes/test_preview_routes.py b/tests/routes/test_preview_routes.py index 9ec798ce..639c3b5b 100644 --- a/tests/routes/test_preview_routes.py +++ b/tests/routes/test_preview_routes.py @@ -1,5 +1,7 @@ +import os import urllib.parse from pathlib import Path +from unittest.mock import patch import pytest from aiohttp import web @@ -110,3 +112,80 @@ async def test_config_updates_preview_roots_after_switch(tmp_path): assert preview_url.startswith("/api/lm/previews?path=") decoded = urllib.parse.unquote(preview_url.split("path=", 1)[1]) assert decoded.replace("\\", "/").endswith("model.webp") + + +def test_is_preview_path_allowed_case_insensitive_on_windows(tmp_path): + """Test that preview path validation is case-insensitive on Windows. + + On Windows, drive letters and paths are case-insensitive. This test verifies + that paths like 'a:/folder/file' match roots stored as 'A:/folder'. + + See: https://github.com/willmiao/ComfyUI-Lora-Manager/issues/772 + See: https://github.com/willmiao/ComfyUI-Lora-Manager/issues/774 + """ + # Create actual files for the test + library_root = tmp_path / "loras" + library_root.mkdir() + preview_file = library_root / "model.preview.jpeg" + preview_file.write_bytes(b"preview") + + config = Config() + + # Simulate Windows behavior by mocking os.path.normcase to lowercase paths + # and os.sep to backslash, regardless of the actual platform + def windows_normcase(path): + return path.lower().replace("/", "\\") + + with patch("py.config.os.path.normcase", side_effect=windows_normcase), \ + patch("py.config.os.sep", "\\"): + + # Manually set _preview_root_paths with uppercase drive letter style path + uppercase_root = Path(str(library_root).upper()) + config._preview_root_paths = {uppercase_root} + + # Test: lowercase version of the path should still be allowed + lowercase_path = str(preview_file).lower() + assert config.is_preview_path_allowed(lowercase_path), \ + f"Path '{lowercase_path}' should be allowed when root is '{uppercase_root}'" + + # Test: mixed case should also work + mixed_case_path = str(preview_file).swapcase() + assert config.is_preview_path_allowed(mixed_case_path), \ + f"Path '{mixed_case_path}' should be allowed when root is '{uppercase_root}'" + + # Test: path outside root should still be rejected + outside_path = str(tmp_path / "other" / "file.jpeg") + assert not config.is_preview_path_allowed(outside_path), \ + f"Path '{outside_path}' should NOT be allowed" + + +def test_is_preview_path_allowed_rejects_prefix_without_separator(tmp_path): + """Test that 'A:/folder' does not match 'A:/folderextra/file'. + + This ensures we check for the path separator after the root to avoid + false positives with paths that share a common prefix. + """ + library_root = tmp_path / "loras" + library_root.mkdir() + + # Create a sibling folder that starts with the same prefix + sibling_root = tmp_path / "loras_extra" + sibling_root.mkdir() + sibling_file = sibling_root / "model.jpeg" + sibling_file.write_bytes(b"x") + + config = Config() + config.apply_library_settings( + { + "folder_paths": { + "loras": [str(library_root)], + "checkpoints": [], + "unet": [], + "embeddings": [], + } + } + ) + + # The sibling path should NOT be allowed even though it shares a prefix + assert not config.is_preview_path_allowed(str(sibling_file)), \ + f"Path in '{sibling_root}' should NOT be allowed when root is '{library_root}'"