diff --git a/src/kleinanzeigen_bot/__init__.py b/src/kleinanzeigen_bot/__init__.py index 4140314..9c397a5 100644 --- a/src/kleinanzeigen_bot/__init__.py +++ b/src/kleinanzeigen_bot/__init__.py @@ -582,7 +582,8 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904 self.config_file_path, default_config, header = ("# yaml-language-server: $schema=https://raw.githubusercontent.com/Second-Hand-Friends/kleinanzeigen-bot/main/schemas/config.schema.json"), - exclude = {"ad_defaults": {"description"}}, + exclude = { + "ad_defaults": {"description"}}, ) def load_config(self) -> None: diff --git a/src/kleinanzeigen_bot/utils/web_scraping_mixin.py b/src/kleinanzeigen_bot/utils/web_scraping_mixin.py index 522975a..279e75d 100644 --- a/src/kleinanzeigen_bot/utils/web_scraping_mixin.py +++ b/src/kleinanzeigen_bot/utils/web_scraping_mixin.py @@ -4,7 +4,7 @@ import asyncio, enum, inspect, json, os, platform, secrets, shutil, subprocess, urllib.request # isort: skip # noqa: S404 from collections.abc import Awaitable, Callable, Coroutine, Iterable from gettext import gettext as _ -from pathlib import Path +from pathlib import Path, PureWindowsPath from typing import Any, Final, Optional, cast try: @@ -441,7 +441,11 @@ class WebScrapingMixin: else: LOG.error("(fail) Browser binary not found: %s", self.browser_config.binary_location) else: - browser_path = self.get_compatible_browser() + try: + browser_path = self.get_compatible_browser() + except AssertionError as exc: + LOG.debug("Browser auto-detection failed: %s", exc) + browser_path = None if browser_path: LOG.info("(ok) Auto-detected browser: %s", browser_path) # Set the binary location for Chrome version detection @@ -579,15 +583,31 @@ class WebScrapingMixin: ] case "Windows": + def win_path(*parts:str) -> str: + return str(PureWindowsPath(*parts)) + + program_files = os.environ.get("PROGRAMFILES", "C:\\Program Files") + program_files_x86 = os.environ.get("PROGRAMFILES(X86)", "C:\\Program Files (x86)") + local_app_data = os.environ.get("LOCALAPPDATA") + if not local_app_data: + user_profile = os.environ.get("USERPROFILE") + if user_profile: + local_app_data = win_path(user_profile, "AppData", "Local") + browser_paths = [ - os.environ.get("PROGRAMFILES", "C:\\Program Files") + r"\Microsoft\Edge\Application\msedge.exe", - os.environ.get("PROGRAMFILES(X86)", "C:\\Program Files (x86)") + r"\Microsoft\Edge\Application\msedge.exe", - os.environ["PROGRAMFILES"] + r"\Chromium\Application\chrome.exe", - os.environ["PROGRAMFILES(X86)"] + r"\Chromium\Application\chrome.exe", - os.environ["LOCALAPPDATA"] + r"\Chromium\Application\chrome.exe", - os.environ["PROGRAMFILES"] + r"\Chrome\Application\chrome.exe", - os.environ["PROGRAMFILES(X86)"] + r"\Chrome\Application\chrome.exe", - os.environ["LOCALAPPDATA"] + r"\Chrome\Application\chrome.exe", + win_path(local_app_data, "Google", "Chrome", "Application", "chrome.exe") if local_app_data else None, + win_path(program_files, "Google", "Chrome", "Application", "chrome.exe"), + win_path(program_files_x86, "Google", "Chrome", "Application", "chrome.exe"), + win_path(local_app_data, "Microsoft", "Edge", "Application", "msedge.exe") if local_app_data else None, + win_path(program_files, "Microsoft", "Edge", "Application", "msedge.exe"), + win_path(program_files_x86, "Microsoft", "Edge", "Application", "msedge.exe"), + win_path(local_app_data, "Chromium", "Application", "chrome.exe") if local_app_data else None, + win_path(program_files, "Chromium", "Application", "chrome.exe"), + win_path(program_files_x86, "Chromium", "Application", "chrome.exe"), + # Intentional fallback for portable/custom distributions installed under a bare "Chrome" directory. + win_path(program_files, "Chrome", "Application", "chrome.exe"), + win_path(program_files_x86, "Chrome", "Application", "chrome.exe"), + win_path(local_app_data, "Chrome", "Application", "chrome.exe") if local_app_data else None, shutil.which("msedge.exe"), shutil.which("chromium.exe"), shutil.which("chrome.exe"), diff --git a/tests/unit/test_web_scraping_mixin.py b/tests/unit/test_web_scraping_mixin.py index 9fab531..9e7020e 100644 --- a/tests/unit/test_web_scraping_mixin.py +++ b/tests/unit/test_web_scraping_mixin.py @@ -878,6 +878,10 @@ class TestWebScrapingBrowserConfiguration: edge_path, chrome_path, # Windows paths + "C:\\Users\\runneradmin\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe", + "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", + "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", + "C:\\Users\\runneradmin\\AppData\\Local\\Microsoft\\Edge\\Application\\msedge.exe", "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe", "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", "C:\\Program Files\\Chromium\\Application\\chrome.exe", @@ -1085,10 +1089,32 @@ class TestWebScrapingBrowserConfiguration: # Test Windows with environment variables not set monkeypatch.setattr(platform, "system", lambda: "Windows") - # Set default values for environment variables monkeypatch.setenv("PROGRAMFILES", "C:\\Program Files") monkeypatch.setenv("PROGRAMFILES(X86)", "C:\\Program Files (x86)") monkeypatch.setenv("LOCALAPPDATA", "C:\\Users\\TestUser\\AppData\\Local") + + local_chrome_path = "C:\\Users\\TestUser\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe" + monkeypatch.setattr(os.path, "isfile", lambda p: p == local_chrome_path) + assert scraper.get_compatible_browser() == local_chrome_path + + local_edge_path = "C:\\Users\\TestUser\\AppData\\Local\\Microsoft\\Edge\\Application\\msedge.exe" + monkeypatch.setattr(os.path, "isfile", lambda p: p == local_edge_path) + assert scraper.get_compatible_browser() == local_edge_path + + local_chromium_path = "C:\\Users\\TestUser\\AppData\\Local\\Chromium\\Application\\chrome.exe" + monkeypatch.setattr(os.path, "isfile", lambda p: p == local_chromium_path) + assert scraper.get_compatible_browser() == local_chromium_path + + monkeypatch.delenv("LOCALAPPDATA", raising = False) + monkeypatch.setenv("USERPROFILE", "C:\\Users\\FallbackUser") + fallback_local_chrome_path = "C:\\Users\\FallbackUser\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe" + monkeypatch.setattr(os.path, "isfile", lambda p: p == fallback_local_chrome_path) + assert scraper.get_compatible_browser() == fallback_local_chrome_path + + monkeypatch.delenv("PROGRAMFILES", raising = False) + monkeypatch.delenv("PROGRAMFILES(X86)", raising = False) + monkeypatch.delenv("LOCALAPPDATA", raising = False) + monkeypatch.delenv("USERPROFILE", raising = False) monkeypatch.setattr(os.path, "isfile", lambda p: False) with pytest.raises(AssertionError, match = "Installed browser could not be detected"): scraper.get_compatible_browser() @@ -1350,7 +1376,7 @@ class TestWebScrapingDiagnostics: def test_diagnose_browser_issues_auto_detect_failure(self, scraper_with_config:WebScrapingMixin, caplog:pytest.LogCaptureFixture) -> None: """Test diagnostic when auto-detecting browser fails.""" - with patch.object(scraper_with_config, "get_compatible_browser", return_value = None): + with patch.object(scraper_with_config, "get_compatible_browser", side_effect = AssertionError("No browser found")): scraper_with_config.browser_config.binary_location = None scraper_with_config.diagnose_browser_issues() @@ -1748,9 +1774,9 @@ class TestWebScrapingDiagnostics: with patch("platform.system", return_value = "Linux"), \ patch("kleinanzeigen_bot.utils.web_scraping_mixin._is_admin", return_value = False), \ patch("psutil.process_iter", return_value = []), \ - patch.object(scraper_with_config, "get_compatible_browser", side_effect = AssertionError("No browser found")), \ - pytest.raises(AssertionError, match = "No browser found"): + patch.object(scraper_with_config, "get_compatible_browser", side_effect = AssertionError("No browser found")): scraper_with_config.diagnose_browser_issues() + assert "(fail) No compatible browser found" in caplog.text def test_diagnose_browser_issues_user_data_dir_permissions_issue( self, scraper_with_config:WebScrapingMixin, caplog:pytest.LogCaptureFixture, tmp_path:Path