mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
feat: chrome version detection clean (#607)
This commit is contained in:
@@ -456,6 +456,8 @@ By default a new browser process will be launched. To reuse a manually launched
|
|||||||
user_data_dir: "/path/to/custom/directory"
|
user_data_dir: "/path/to/custom/directory"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**The bot will automatically detect Chrome 136+ and validate your configuration. If validation fails, you'll see clear error messages with specific instructions on how to fix your configuration.**
|
||||||
|
|
||||||
1. In your config.yaml specify the same flags as browser arguments, e.g.:
|
1. In your config.yaml specify the same flags as browser arguments, e.g.:
|
||||||
```yaml
|
```yaml
|
||||||
browser:
|
browser:
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ browser:
|
|||||||
user_data_dir: "/tmp/chrome-debug-profile" # Must match the argument above
|
user_data_dir: "/tmp/chrome-debug-profile" # Must match the argument above
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**The bot will automatically detect Chrome 136+ and provide clear error messages if your configuration is missing the required `--user-data-dir` setting.**
|
||||||
|
|
||||||
For more details, see [Chrome 136+ Security Changes](#5-chrome-136-security-changes-march-2025) below.
|
For more details, see [Chrome 136+ Security Changes](#5-chrome-136-security-changes-march-2025) below.
|
||||||
|
|
||||||
## Quick Diagnosis
|
## Quick Diagnosis
|
||||||
@@ -45,6 +47,17 @@ This will check:
|
|||||||
- Remote debugging port status
|
- Remote debugging port status
|
||||||
- Running browser processes
|
- Running browser processes
|
||||||
- Platform-specific issues
|
- Platform-specific issues
|
||||||
|
- **Chrome/Edge version detection and configuration validation**
|
||||||
|
|
||||||
|
**Automatic Chrome 136+ Validation:**
|
||||||
|
The bot automatically detects Chrome/Edge 136+ and validates your configuration. If you're using Chrome 136+ with remote debugging but missing the required `--user-data-dir` setting, you'll see clear error messages like:
|
||||||
|
|
||||||
|
```
|
||||||
|
Chrome 136+ configuration validation failed: Chrome 136+ requires --user-data-dir
|
||||||
|
Please update your configuration to include --user-data-dir for remote debugging
|
||||||
|
```
|
||||||
|
|
||||||
|
The bot will also provide specific instructions on how to fix your configuration.
|
||||||
|
|
||||||
## Common Issues and Solutions
|
## Common Issues and Solutions
|
||||||
|
|
||||||
|
|||||||
@@ -440,14 +440,34 @@ kleinanzeigen_bot/utils/web_scraping_mixin.py:
|
|||||||
"(ok) Remote debugging API accessible - Browser: %s": "(ok) Remote-Debugging-API zugänglich - Browser: %s"
|
"(ok) Remote debugging API accessible - Browser: %s": "(ok) Remote-Debugging-API zugänglich - Browser: %s"
|
||||||
"(fail) Remote debugging port is open but API not accessible: %s": "(Fehler) Remote-Debugging-Port ist offen, aber API nicht zugänglich: %s"
|
"(fail) Remote debugging port is open but API not accessible: %s": "(Fehler) Remote-Debugging-Port ist offen, aber API nicht zugänglich: %s"
|
||||||
" This might indicate a browser update issue or configuration problem": " Dies könnte auf ein Browser-Update-Problem oder Konfigurationsproblem hinweisen"
|
" This might indicate a browser update issue or configuration problem": " Dies könnte auf ein Browser-Update-Problem oder Konfigurationsproblem hinweisen"
|
||||||
? " On macOS, try: /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=%d --user-data-dir=/tmp/chrome-debug-profile --disable-dev-shm-usage"
|
|
||||||
: " Unter macOS versuchen Sie: /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=%d --user-data-dir=/tmp/chrome-debug-profile --disable-dev-shm-usage"
|
|
||||||
? " Or: open -a \"Google Chrome\" --args --remote-debugging-port=%d --user-data-dir=/tmp/chrome-debug-profile --disable-dev-shm-usage"
|
|
||||||
: " Oder: open -a \"Google Chrome\" --args --remote-debugging-port=%d --user-data-dir=/tmp/chrome-debug-profile --disable-dev-shm-usage"
|
|
||||||
" IMPORTANT: --user-data-dir is MANDATORY for macOS Chrome remote debugging": " WARNUNG: --user-data-dir ist PFLICHT für macOS Chrome Remote-Debugging"
|
_validate_chrome_version_configuration:
|
||||||
" IMPORTANT: macOS Chrome remote debugging requires --user-data-dir flag": " WARNUNG: macOS Chrome Remote-Debugging erfordert --user-data-dir Flag"
|
" -> %s 136+ detected: %s": " -> %s 136+ erkannt: %s"
|
||||||
" Add to your config.yaml: user_data_dir: \"/tmp/chrome-debug-profile\"": " Fügen Sie zu Ihrer config.yaml hinzu: user_data_dir: \"/tmp/chrome-debug-profile\""
|
" -> %s 136+ configuration validation passed": " -> %s 136+ Konfigurationsvalidierung bestanden"
|
||||||
" And to browser arguments: --user-data-dir=/tmp/chrome-debug-profile": " Und zu Browser-Argumenten: --user-data-dir=/tmp/chrome-debug-profile"
|
" -> %s 136+ configuration validation failed: %s": " -> %s 136+ Konfigurationsvalidierung fehlgeschlagen: %s"
|
||||||
|
" -> %s version detected: %s (pre-136, no special validation required)": " -> %s-Version erkannt: %s (vor 136, keine besondere Validierung erforderlich)"
|
||||||
|
" -> Please update your configuration to include --user-data-dir for remote debugging": " -> Bitte aktualisieren Sie Ihre Konfiguration, um --user-data-dir für Remote-Debugging einzuschließen"
|
||||||
|
" -> Skipping browser version validation in test environment": " -> Browser-Versionsvalidierung in Testumgebung wird übersprungen"
|
||||||
|
" -> Browser version detection failed, skipping validation: %s": " -> Browser-Versionserkennung fehlgeschlagen, Validierung wird übersprungen: %s"
|
||||||
|
" -> Unexpected error during browser version validation, skipping: %s": " -> Unerwarteter Fehler bei Browser-Versionsvalidierung, wird übersprungen: %s"
|
||||||
|
|
||||||
|
_diagnose_chrome_version_issues:
|
||||||
|
"(info) %s version from binary: %s %s (major: %d)": "(Info) %s-Version von Binärdatei: %s %s (Hauptversion: %d)"
|
||||||
|
"(info) %s version from remote debugging: %s %s (major: %d)": "(Info) %s-Version von Remote-Debugging: %s %s (Hauptversion: %d)"
|
||||||
|
"(info) %s 136+ detected - security validation required": "(Info) %s 136+ erkannt - Sicherheitsvalidierung erforderlich"
|
||||||
|
"(info) %s pre-136 detected - no special security requirements": "(Info) %s vor 136 erkannt - keine besonderen Sicherheitsanforderungen"
|
||||||
|
"(info) Remote %s 136+ detected - validating configuration": "(Info) Remote %s 136+ erkannt - validiere Konfiguration"
|
||||||
|
"(fail) %s 136+ configuration validation failed: %s": "(Fehler) %s 136+ Konfigurationsvalidierung fehlgeschlagen: %s"
|
||||||
|
"(ok) %s 136+ configuration validation passed": "(Ok) %s 136+ Konfigurationsvalidierung bestanden"
|
||||||
|
"(info) Chrome/Edge 136+ security changes require --user-data-dir for remote debugging": "(Info) Chrome/Edge 136+ Sicherheitsänderungen erfordern --user-data-dir für Remote-Debugging"
|
||||||
|
" See: https://developer.chrome.com/blog/remote-debugging-port": " Siehe: https://developer.chrome.com/blog/remote-debugging-port"
|
||||||
|
" -> Browser version diagnostics failed: %s": " -> Browser-Versionsdiagnose fehlgeschlagen: %s"
|
||||||
|
" -> Unexpected error during browser version diagnostics: %s": " -> Unerwarteter Fehler bei Browser-Versionsdiagnose: %s"
|
||||||
|
" Solution: Add --user-data-dir=/path/to/directory to browser arguments": " Lösung: Fügen Sie --user-data-dir=/pfad/zum/verzeichnis zu Browser-Argumenten hinzu"
|
||||||
|
" And user_data_dir: \"/path/to/directory\" to your configuration": " Und user_data_dir: \"/pfad/zum/verzeichnis\" zu Ihrer Konfiguration"
|
||||||
|
|
||||||
#################################################
|
#################################################
|
||||||
kleinanzeigen_bot/update_checker.py:
|
kleinanzeigen_bot/update_checker.py:
|
||||||
|
|||||||
242
src/kleinanzeigen_bot/utils/chrome_version_detector.py
Normal file
242
src/kleinanzeigen_bot/utils/chrome_version_detector.py
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
# SPDX-FileCopyrightText: © Jens Bergmann and contributors
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess # noqa: S404
|
||||||
|
import urllib.request
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
|
from . import loggers
|
||||||
|
|
||||||
|
LOG:Final[loggers.Logger] = loggers.get_logger(__name__)
|
||||||
|
|
||||||
|
# Chrome 136 was released in March 2025 and introduced security changes
|
||||||
|
CHROME_136_VERSION = 136
|
||||||
|
|
||||||
|
|
||||||
|
class ChromeVersionInfo:
|
||||||
|
"""Information about a Chrome browser version."""
|
||||||
|
|
||||||
|
def __init__(self, version_string:str, major_version:int, browser_name:str = "Unknown") -> None:
|
||||||
|
self.version_string = version_string
|
||||||
|
self.major_version = major_version
|
||||||
|
self.browser_name = browser_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_chrome_136_plus(self) -> bool:
|
||||||
|
"""Check if this is Chrome version 136 or later."""
|
||||||
|
return self.major_version >= CHROME_136_VERSION
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.browser_name} {self.version_string} (major: {self.major_version})"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_version_string(version_string:str) -> int:
|
||||||
|
"""
|
||||||
|
Parse a Chrome version string and extract the major version number.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version_string: Version string like "136.0.6778.0" or "136.0.6778.0 (Developer Build)"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Major version number (e.g., 136)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If version string cannot be parsed
|
||||||
|
"""
|
||||||
|
# Extract version number from strings like:
|
||||||
|
# "136.0.6778.0"
|
||||||
|
# "136.0.6778.0 (Developer Build)"
|
||||||
|
# "136.0.6778.0 (Official Build) (x86_64)"
|
||||||
|
# "Google Chrome 136.0.6778.0"
|
||||||
|
# "Microsoft Edge 136.0.6778.0"
|
||||||
|
# "Chromium 136.0.6778.0"
|
||||||
|
match = re.search(r"(\d+)\.\d+\.\d+\.\d+", version_string)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Could not parse version string: {version_string}")
|
||||||
|
|
||||||
|
return int(match.group(1))
|
||||||
|
|
||||||
|
|
||||||
|
def detect_chrome_version_from_binary(binary_path:str) -> ChromeVersionInfo | None:
|
||||||
|
"""
|
||||||
|
Detect Chrome version by running the browser binary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
binary_path: Path to the Chrome binary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ChromeVersionInfo if successful, None if detection fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Run browser with --version flag
|
||||||
|
result = subprocess.run( # noqa: S603
|
||||||
|
[binary_path, "--version"],
|
||||||
|
check = False, capture_output = True,
|
||||||
|
text = True,
|
||||||
|
timeout = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
LOG.debug("Browser version command failed: %s", result.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
output = result.stdout.strip()
|
||||||
|
major_version = parse_version_string(output)
|
||||||
|
|
||||||
|
# Extract just the version number for version_string
|
||||||
|
version_match = re.search(r"(\d+\.\d+\.\d+\.\d+)", output)
|
||||||
|
version_string = version_match.group(1) if version_match else output
|
||||||
|
|
||||||
|
# Determine browser name from binary path
|
||||||
|
browser_name = "Chrome"
|
||||||
|
if "edge" in binary_path.lower():
|
||||||
|
browser_name = "Edge"
|
||||||
|
elif "chromium" in binary_path.lower():
|
||||||
|
browser_name = "Chromium"
|
||||||
|
|
||||||
|
return ChromeVersionInfo(version_string, major_version, browser_name)
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
LOG.debug("Browser version command timed out")
|
||||||
|
return None
|
||||||
|
except (subprocess.SubprocessError, ValueError) as e:
|
||||||
|
LOG.debug("Failed to detect browser version: %s", str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def detect_chrome_version_from_remote_debugging(host:str = "127.0.0.1", port:int = 9222) -> ChromeVersionInfo | None:
|
||||||
|
"""
|
||||||
|
Detect Chrome version from remote debugging API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host: Remote debugging host
|
||||||
|
port: Remote debugging port
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ChromeVersionInfo if successful, None if detection fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Query the remote debugging API
|
||||||
|
url = f"http://{host}:{port}/json/version"
|
||||||
|
response = urllib.request.urlopen(url, timeout = 5) # noqa: S310
|
||||||
|
version_data = json.loads(response.read().decode())
|
||||||
|
|
||||||
|
# Extract version information
|
||||||
|
user_agent = version_data.get("User-Agent", "")
|
||||||
|
browser_name = version_data.get("Browser", "Unknown")
|
||||||
|
|
||||||
|
# Parse version from User-Agent string
|
||||||
|
# Example: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.6778.0 Safari/537.36"
|
||||||
|
match = re.search(r"Chrome/(\d+)\.\d+\.\d+\.\d+", user_agent)
|
||||||
|
if not match:
|
||||||
|
LOG.debug("Could not parse Chrome version from User-Agent: %s", user_agent)
|
||||||
|
return None
|
||||||
|
|
||||||
|
major_version = int(match.group(1))
|
||||||
|
version_string = match.group(0).replace("Chrome/", "")
|
||||||
|
|
||||||
|
return ChromeVersionInfo(version_string, major_version, browser_name)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
LOG.debug("Failed to detect browser version from remote debugging: %s", str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_chrome_136_configuration(browser_arguments:list[str], user_data_dir:str | None) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Validate configuration for Chrome/Edge 136+ security requirements.
|
||||||
|
|
||||||
|
Chrome/Edge 136+ requires --user-data-dir to be specified when using --remote-debugging-port.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
browser_arguments: List of browser arguments
|
||||||
|
user_data_dir: User data directory configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, error_message)
|
||||||
|
"""
|
||||||
|
# Check if remote debugging is enabled
|
||||||
|
has_remote_debugging = any(
|
||||||
|
arg.startswith("--remote-debugging-port=")
|
||||||
|
for arg in browser_arguments
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_remote_debugging:
|
||||||
|
return True, "" # No remote debugging, no validation needed
|
||||||
|
|
||||||
|
# Check if user-data-dir is specified in arguments
|
||||||
|
has_user_data_dir_arg = any(
|
||||||
|
arg.startswith("--user-data-dir=")
|
||||||
|
for arg in browser_arguments
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user_data_dir is configured
|
||||||
|
has_user_data_dir_config = user_data_dir is not None and user_data_dir.strip()
|
||||||
|
|
||||||
|
if not has_user_data_dir_arg and not has_user_data_dir_config:
|
||||||
|
return False, (
|
||||||
|
"Chrome/Edge 136+ requires --user-data-dir to be specified when using --remote-debugging-port. "
|
||||||
|
"Add --user-data-dir=/path/to/directory to your browser arguments and "
|
||||||
|
'user_data_dir: "/path/to/directory" to your configuration.'
|
||||||
|
)
|
||||||
|
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_chrome_version_diagnostic_info(
|
||||||
|
binary_path:str | None = None,
|
||||||
|
remote_host:str = "127.0.0.1",
|
||||||
|
remote_port:int | None = None
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get comprehensive Chrome version diagnostic information.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
binary_path: Path to Chrome binary (optional)
|
||||||
|
remote_host: Remote debugging host
|
||||||
|
remote_port: Remote debugging port (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with diagnostic information
|
||||||
|
"""
|
||||||
|
diagnostic_info:dict[str, Any] = {
|
||||||
|
"binary_detection": None,
|
||||||
|
"remote_detection": None,
|
||||||
|
"chrome_136_plus_detected": False,
|
||||||
|
"configuration_valid": True,
|
||||||
|
"recommendations": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try binary detection
|
||||||
|
if binary_path:
|
||||||
|
version_info = detect_chrome_version_from_binary(binary_path)
|
||||||
|
if version_info:
|
||||||
|
diagnostic_info["binary_detection"] = {
|
||||||
|
"version_string": version_info.version_string,
|
||||||
|
"major_version": version_info.major_version,
|
||||||
|
"browser_name": version_info.browser_name,
|
||||||
|
"is_chrome_136_plus": version_info.is_chrome_136_plus
|
||||||
|
}
|
||||||
|
diagnostic_info["chrome_136_plus_detected"] = version_info.is_chrome_136_plus
|
||||||
|
|
||||||
|
# Try remote debugging detection
|
||||||
|
if remote_port:
|
||||||
|
version_info = detect_chrome_version_from_remote_debugging(remote_host, remote_port)
|
||||||
|
if version_info:
|
||||||
|
diagnostic_info["remote_detection"] = {
|
||||||
|
"version_string": version_info.version_string,
|
||||||
|
"major_version": version_info.major_version,
|
||||||
|
"browser_name": version_info.browser_name,
|
||||||
|
"is_chrome_136_plus": version_info.is_chrome_136_plus
|
||||||
|
}
|
||||||
|
diagnostic_info["chrome_136_plus_detected"] = version_info.is_chrome_136_plus
|
||||||
|
|
||||||
|
# Add recommendations based on detected version
|
||||||
|
if diagnostic_info["chrome_136_plus_detected"]:
|
||||||
|
diagnostic_info["recommendations"].append(
|
||||||
|
"Chrome 136+ detected - ensure --user-data-dir is configured for remote debugging"
|
||||||
|
)
|
||||||
|
|
||||||
|
return diagnostic_info
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
|
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||||
import asyncio, enum, inspect, json, os, platform, secrets, shutil, urllib.request # isort: skip
|
import asyncio, enum, inspect, json, os, platform, secrets, shutil, subprocess, urllib.request # isort: skip # noqa: S404
|
||||||
from collections.abc import Callable, Coroutine, Iterable
|
from collections.abc import Callable, Coroutine, Iterable
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from typing import Any, Final, cast
|
from typing import Any, Final, cast
|
||||||
@@ -18,6 +18,11 @@ from nodriver.core.element import Element
|
|||||||
from nodriver.core.tab import Tab as Page
|
from nodriver.core.tab import Tab as Page
|
||||||
|
|
||||||
from . import loggers, net
|
from . import loggers, net
|
||||||
|
from .chrome_version_detector import (
|
||||||
|
detect_chrome_version_from_binary,
|
||||||
|
get_chrome_version_diagnostic_info,
|
||||||
|
validate_chrome_136_configuration,
|
||||||
|
)
|
||||||
from .misc import T, ensure
|
from .misc import T, ensure
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -91,6 +96,9 @@ class WebScrapingMixin:
|
|||||||
self.browser_config.binary_location = self.get_compatible_browser()
|
self.browser_config.binary_location = self.get_compatible_browser()
|
||||||
LOG.info(" -> Browser binary location: %s", self.browser_config.binary_location)
|
LOG.info(" -> Browser binary location: %s", self.browser_config.binary_location)
|
||||||
|
|
||||||
|
# Chrome version detection and validation
|
||||||
|
await self._validate_chrome_version_configuration()
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
# check if an existing browser instance shall be used...
|
# check if an existing browser instance shall be used...
|
||||||
########################################################
|
########################################################
|
||||||
@@ -299,6 +307,8 @@ class WebScrapingMixin:
|
|||||||
browser_path = self.get_compatible_browser()
|
browser_path = self.get_compatible_browser()
|
||||||
if browser_path:
|
if browser_path:
|
||||||
LOG.info("(ok) Auto-detected browser: %s", browser_path)
|
LOG.info("(ok) Auto-detected browser: %s", browser_path)
|
||||||
|
# Set the binary location for Chrome version detection
|
||||||
|
self.browser_config.binary_location = browser_path
|
||||||
else:
|
else:
|
||||||
LOG.error("(fail) No compatible browser found")
|
LOG.error("(fail) No compatible browser found")
|
||||||
|
|
||||||
@@ -335,12 +345,6 @@ class WebScrapingMixin:
|
|||||||
else:
|
else:
|
||||||
LOG.error("(fail) Remote debugging port is not open")
|
LOG.error("(fail) Remote debugging port is not open")
|
||||||
LOG.info(" Make sure browser is started with: --remote-debugging-port=%d", remote_port)
|
LOG.info(" Make sure browser is started with: --remote-debugging-port=%d", remote_port)
|
||||||
if platform.system() == "Darwin":
|
|
||||||
LOG.info(" On macOS, try: /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome "
|
|
||||||
"--remote-debugging-port=%d --user-data-dir=/tmp/chrome-debug-profile --disable-dev-shm-usage", remote_port)
|
|
||||||
LOG.info(' Or: open -a "Google Chrome" --args --remote-debugging-port=%d '
|
|
||||||
'--user-data-dir=/tmp/chrome-debug-profile --disable-dev-shm-usage', remote_port)
|
|
||||||
LOG.info(" IMPORTANT: --user-data-dir is MANDATORY for macOS Chrome remote debugging")
|
|
||||||
|
|
||||||
# Check for running browser processes
|
# Check for running browser processes
|
||||||
browser_processes = []
|
browser_processes = []
|
||||||
@@ -363,16 +367,14 @@ class WebScrapingMixin:
|
|||||||
LOG.info("(info) Windows detected - check Windows Defender and antivirus software")
|
LOG.info("(info) Windows detected - check Windows Defender and antivirus software")
|
||||||
elif platform.system() == "Darwin":
|
elif platform.system() == "Darwin":
|
||||||
LOG.info("(info) macOS detected - check Gatekeeper and security settings")
|
LOG.info("(info) macOS detected - check Gatekeeper and security settings")
|
||||||
# Check for macOS-specific Chrome remote debugging requirements
|
|
||||||
if remote_port > 0 and not self.browser_config.user_data_dir:
|
|
||||||
LOG.warning(" IMPORTANT: macOS Chrome remote debugging requires --user-data-dir flag")
|
|
||||||
LOG.info(' Add to your config.yaml: user_data_dir: "/tmp/chrome-debug-profile"')
|
|
||||||
LOG.info(" And to browser arguments: --user-data-dir=/tmp/chrome-debug-profile")
|
|
||||||
elif platform.system() == "Linux":
|
elif platform.system() == "Linux":
|
||||||
LOG.info("(info) Linux detected - check if running as root (not recommended)")
|
LOG.info("(info) Linux detected - check if running as root (not recommended)")
|
||||||
if _is_admin():
|
if _is_admin():
|
||||||
LOG.error("(fail) Running as root - this can cause browser connection issues")
|
LOG.error("(fail) Running as root - this can cause browser connection issues")
|
||||||
|
|
||||||
|
# Chrome version detection and validation
|
||||||
|
self._diagnose_chrome_version_issues(remote_port)
|
||||||
|
|
||||||
LOG.info("=== End Diagnostics ===")
|
LOG.info("=== End Diagnostics ===")
|
||||||
|
|
||||||
def close_browser_session(self) -> None:
|
def close_browser_session(self) -> None:
|
||||||
@@ -744,3 +746,109 @@ class WebScrapingMixin:
|
|||||||
""")
|
""")
|
||||||
await self.web_sleep()
|
await self.web_sleep()
|
||||||
return elem
|
return elem
|
||||||
|
|
||||||
|
async def _validate_chrome_version_configuration(self) -> None:
|
||||||
|
"""
|
||||||
|
Validate Chrome version configuration for Chrome 136+ security requirements.
|
||||||
|
|
||||||
|
This method checks if the browser is Chrome 136+ and validates that the configuration
|
||||||
|
meets the security requirements for remote debugging.
|
||||||
|
"""
|
||||||
|
# Skip validation in test environments to avoid subprocess calls
|
||||||
|
if os.environ.get("PYTEST_CURRENT_TEST"):
|
||||||
|
LOG.debug(" -> Skipping browser version validation in test environment")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Detect Chrome version from binary
|
||||||
|
binary_path = self.browser_config.binary_location
|
||||||
|
version_info = detect_chrome_version_from_binary(binary_path) if binary_path else None
|
||||||
|
|
||||||
|
if version_info and version_info.is_chrome_136_plus:
|
||||||
|
LOG.info(" -> %s 136+ detected: %s", version_info.browser_name, version_info)
|
||||||
|
|
||||||
|
# Validate configuration for Chrome/Edge 136+
|
||||||
|
is_valid, error_message = validate_chrome_136_configuration(
|
||||||
|
list(self.browser_config.arguments),
|
||||||
|
self.browser_config.user_data_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
LOG.error(" -> %s 136+ configuration validation failed: %s", version_info.browser_name, error_message)
|
||||||
|
LOG.error(" -> Please update your configuration to include --user-data-dir for remote debugging")
|
||||||
|
raise AssertionError(error_message)
|
||||||
|
LOG.info(" -> %s 136+ configuration validation passed", version_info.browser_name)
|
||||||
|
elif version_info:
|
||||||
|
LOG.info(" -> %s version detected: %s (pre-136, no special validation required)", version_info.browser_name, version_info)
|
||||||
|
else:
|
||||||
|
LOG.debug(" -> Could not detect browser version, skipping validation")
|
||||||
|
except (subprocess.SubprocessError, OSError, FileNotFoundError) as e:
|
||||||
|
LOG.warning(" -> Browser version detection failed, skipping validation: %s", e)
|
||||||
|
# Continue without validation rather than failing
|
||||||
|
except Exception as e:
|
||||||
|
LOG.warning(" -> Unexpected error during browser version validation, skipping: %s", e)
|
||||||
|
# Continue without validation rather than failing
|
||||||
|
|
||||||
|
def _diagnose_chrome_version_issues(self, remote_port:int) -> None:
|
||||||
|
"""
|
||||||
|
Diagnose Chrome version issues and provide specific recommendations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
remote_port: Remote debugging port (0 if not configured)
|
||||||
|
"""
|
||||||
|
# Skip diagnostics in test environments to avoid subprocess calls
|
||||||
|
if os.environ.get("PYTEST_CURRENT_TEST"):
|
||||||
|
LOG.debug(" -> Skipping browser version diagnostics in test environment")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get diagnostic information
|
||||||
|
binary_path = self.browser_config.binary_location
|
||||||
|
diagnostic_info = get_chrome_version_diagnostic_info(
|
||||||
|
binary_path = binary_path,
|
||||||
|
remote_port = remote_port if remote_port > 0 else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Report binary detection results
|
||||||
|
if diagnostic_info["binary_detection"]:
|
||||||
|
binary_info = diagnostic_info["binary_detection"]
|
||||||
|
LOG.info("(info) %s version from binary: %s %s (major: %d)",
|
||||||
|
binary_info["browser_name"], binary_info["browser_name"], binary_info["version_string"], binary_info["major_version"])
|
||||||
|
|
||||||
|
if binary_info["is_chrome_136_plus"]:
|
||||||
|
LOG.info("(info) %s 136+ detected - security validation required", binary_info["browser_name"])
|
||||||
|
else:
|
||||||
|
LOG.info("(info) %s pre-136 detected - no special security requirements", binary_info["browser_name"])
|
||||||
|
|
||||||
|
# Report remote detection results
|
||||||
|
if diagnostic_info["remote_detection"]:
|
||||||
|
remote_info = diagnostic_info["remote_detection"]
|
||||||
|
LOG.info("(info) %s version from remote debugging: %s %s (major: %d)",
|
||||||
|
remote_info["browser_name"], remote_info["browser_name"], remote_info["version_string"], remote_info["major_version"])
|
||||||
|
|
||||||
|
if remote_info["is_chrome_136_plus"]:
|
||||||
|
LOG.info("(info) Remote %s 136+ detected - validating configuration", remote_info["browser_name"])
|
||||||
|
|
||||||
|
# Validate configuration for Chrome/Edge 136+
|
||||||
|
is_valid, error_message = validate_chrome_136_configuration(
|
||||||
|
list(self.browser_config.arguments),
|
||||||
|
self.browser_config.user_data_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
LOG.error("(fail) %s 136+ configuration validation failed: %s", remote_info["browser_name"], error_message)
|
||||||
|
LOG.info(" Solution: Add --user-data-dir=/path/to/directory to browser arguments")
|
||||||
|
LOG.info(' And user_data_dir: "/path/to/directory" to your configuration')
|
||||||
|
else:
|
||||||
|
LOG.info("(ok) %s 136+ configuration validation passed", remote_info["browser_name"])
|
||||||
|
|
||||||
|
# Add general recommendations
|
||||||
|
if diagnostic_info["chrome_136_plus_detected"]:
|
||||||
|
LOG.info("(info) Chrome/Edge 136+ security changes require --user-data-dir for remote debugging")
|
||||||
|
LOG.info(" See: https://developer.chrome.com/blog/remote-debugging-port")
|
||||||
|
except (subprocess.SubprocessError, OSError, FileNotFoundError) as e:
|
||||||
|
LOG.warning(" -> Browser version diagnostics failed: %s", e)
|
||||||
|
# Continue without diagnostics rather than failing
|
||||||
|
except Exception as e:
|
||||||
|
LOG.warning(" -> Unexpected error during browser version diagnostics: %s", e)
|
||||||
|
# Continue without diagnostics rather than failing
|
||||||
|
|||||||
389
tests/unit/test_chrome_version_detector.py
Normal file
389
tests/unit/test_chrome_version_detector.py
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
# SPDX-FileCopyrightText: © Jens Bergmann and contributors
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||||
|
import json
|
||||||
|
import subprocess # noqa: S404
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from kleinanzeigen_bot.utils.chrome_version_detector import (
|
||||||
|
ChromeVersionInfo,
|
||||||
|
detect_chrome_version_from_binary,
|
||||||
|
detect_chrome_version_from_remote_debugging,
|
||||||
|
get_chrome_version_diagnostic_info,
|
||||||
|
parse_version_string,
|
||||||
|
validate_chrome_136_configuration,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseVersionString:
|
||||||
|
"""Test version string parsing functionality."""
|
||||||
|
|
||||||
|
def test_parse_version_string_basic(self) -> None:
|
||||||
|
"""Test parsing basic version string."""
|
||||||
|
version = parse_version_string("136.0.6778.0")
|
||||||
|
assert version == 136
|
||||||
|
|
||||||
|
def test_parse_version_string_with_build_info(self) -> None:
|
||||||
|
"""Test parsing version string with build information."""
|
||||||
|
version = parse_version_string("136.0.6778.0 (Developer Build)")
|
||||||
|
assert version == 136
|
||||||
|
|
||||||
|
def test_parse_version_string_with_architecture(self) -> None:
|
||||||
|
"""Test parsing version string with architecture information."""
|
||||||
|
version = parse_version_string("136.0.6778.0 (Official Build) (x86_64)")
|
||||||
|
assert version == 136
|
||||||
|
|
||||||
|
def test_parse_version_string_older_version(self) -> None:
|
||||||
|
"""Test parsing older Chrome version."""
|
||||||
|
version = parse_version_string("120.0.6099.109")
|
||||||
|
assert version == 120
|
||||||
|
|
||||||
|
def test_parse_version_string_invalid_format(self) -> None:
|
||||||
|
"""Test parsing invalid version string raises ValueError."""
|
||||||
|
with pytest.raises(ValueError, match = "Could not parse version string"):
|
||||||
|
parse_version_string("invalid-version")
|
||||||
|
|
||||||
|
def test_parse_version_string_empty(self) -> None:
|
||||||
|
"""Test parsing empty version string raises ValueError."""
|
||||||
|
with pytest.raises(ValueError, match = "Could not parse version string"):
|
||||||
|
parse_version_string("")
|
||||||
|
|
||||||
|
|
||||||
|
class TestChromeVersionInfo:
|
||||||
|
"""Test ChromeVersionInfo class."""
|
||||||
|
|
||||||
|
def test_chrome_version_info_creation(self) -> None:
|
||||||
|
"""Test creating ChromeVersionInfo instance."""
|
||||||
|
version_info = ChromeVersionInfo("136.0.6778.0", 136, "Chrome")
|
||||||
|
assert version_info.version_string == "136.0.6778.0"
|
||||||
|
assert version_info.major_version == 136
|
||||||
|
assert version_info.browser_name == "Chrome"
|
||||||
|
|
||||||
|
def test_chrome_version_info_is_chrome_136_plus_true(self) -> None:
|
||||||
|
"""Test is_chrome_136_plus returns True for Chrome 136+."""
|
||||||
|
version_info = ChromeVersionInfo("136.0.6778.0", 136, "Chrome")
|
||||||
|
assert version_info.is_chrome_136_plus is True
|
||||||
|
|
||||||
|
def test_chrome_version_info_is_chrome_136_plus_false(self) -> None:
|
||||||
|
"""Test is_chrome_136_plus returns False for Chrome < 136."""
|
||||||
|
version_info = ChromeVersionInfo("120.0.6099.109", 120, "Chrome")
|
||||||
|
assert version_info.is_chrome_136_plus is False
|
||||||
|
|
||||||
|
def test_chrome_version_info_is_chrome_136_plus_edge_case(self) -> None:
|
||||||
|
"""Test is_chrome_136_plus edge case for version 136."""
|
||||||
|
version_info = ChromeVersionInfo("136.0.0.0", 136, "Chrome")
|
||||||
|
assert version_info.is_chrome_136_plus is True
|
||||||
|
|
||||||
|
def test_chrome_version_info_str_representation(self) -> None:
|
||||||
|
"""Test string representation of ChromeVersionInfo."""
|
||||||
|
version_info = ChromeVersionInfo("136.0.6778.0", 136, "Chrome")
|
||||||
|
expected = "Chrome 136.0.6778.0 (major: 136)"
|
||||||
|
assert str(version_info) == expected
|
||||||
|
|
||||||
|
def test_chrome_version_info_edge_browser(self) -> None:
|
||||||
|
"""Test ChromeVersionInfo with Edge browser."""
|
||||||
|
version_info = ChromeVersionInfo("136.0.6778.0", 136, "Edge")
|
||||||
|
assert version_info.browser_name == "Edge"
|
||||||
|
assert str(version_info) == "Edge 136.0.6778.0 (major: 136)"
|
||||||
|
|
||||||
|
|
||||||
|
class TestDetectChromeVersionFromBinary:
|
||||||
|
"""Test Chrome version detection from binary."""
|
||||||
|
|
||||||
|
@patch("subprocess.run")
|
||||||
|
def test_detect_chrome_version_from_binary_success(self, mock_run:Mock) -> None:
|
||||||
|
"""Test successful Chrome version detection from binary."""
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 0
|
||||||
|
mock_result.stdout = "Google Chrome 136.0.6778.0\n"
|
||||||
|
mock_run.return_value = mock_result
|
||||||
|
|
||||||
|
version_info = detect_chrome_version_from_binary("/path/to/chrome")
|
||||||
|
|
||||||
|
assert version_info is not None
|
||||||
|
assert version_info.version_string == "136.0.6778.0"
|
||||||
|
assert version_info.major_version == 136
|
||||||
|
assert version_info.browser_name == "Chrome"
|
||||||
|
mock_run.assert_called_once_with(
|
||||||
|
["/path/to/chrome", "--version"],
|
||||||
|
check = False,
|
||||||
|
capture_output = True,
|
||||||
|
text = True,
|
||||||
|
timeout = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("subprocess.run")
|
||||||
|
def test_detect_chrome_version_from_binary_edge(self, mock_run:Mock) -> None:
|
||||||
|
"""Test Chrome version detection for Edge browser."""
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 0
|
||||||
|
mock_result.stdout = "Microsoft Edge 136.0.6778.0\n"
|
||||||
|
mock_run.return_value = mock_result
|
||||||
|
|
||||||
|
version_info = detect_chrome_version_from_binary("/path/to/edge")
|
||||||
|
|
||||||
|
assert version_info is not None
|
||||||
|
assert version_info.browser_name == "Edge"
|
||||||
|
assert version_info.major_version == 136
|
||||||
|
|
||||||
|
@patch("subprocess.run")
|
||||||
|
def test_detect_chrome_version_from_binary_chromium(self, mock_run:Mock) -> None:
|
||||||
|
"""Test Chrome version detection for Chromium browser."""
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 0
|
||||||
|
mock_result.stdout = "Chromium 136.0.6778.0\n"
|
||||||
|
mock_run.return_value = mock_result
|
||||||
|
|
||||||
|
version_info = detect_chrome_version_from_binary("/path/to/chromium")
|
||||||
|
|
||||||
|
assert version_info is not None
|
||||||
|
assert version_info.browser_name == "Chromium"
|
||||||
|
assert version_info.major_version == 136
|
||||||
|
|
||||||
|
@patch("subprocess.run")
|
||||||
|
def test_detect_chrome_version_from_binary_failure(self, mock_run:Mock) -> None:
|
||||||
|
"""Test Chrome version detection failure."""
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 1
|
||||||
|
mock_result.stderr = "Command not found"
|
||||||
|
mock_run.return_value = mock_result
|
||||||
|
|
||||||
|
version_info = detect_chrome_version_from_binary("/path/to/chrome")
|
||||||
|
assert version_info is None
|
||||||
|
|
||||||
|
@patch("subprocess.run")
|
||||||
|
def test_detect_chrome_version_from_binary_timeout(self, mock_run:Mock) -> None:
|
||||||
|
"""Test Chrome version detection timeout."""
|
||||||
|
mock_run.side_effect = subprocess.TimeoutExpired("chrome", 10)
|
||||||
|
|
||||||
|
version_info = detect_chrome_version_from_binary("/path/to/chrome")
|
||||||
|
assert version_info is None
|
||||||
|
|
||||||
|
@patch("subprocess.run")
|
||||||
|
def test_detect_chrome_version_from_binary_invalid_output(self, mock_run:Mock) -> None:
|
||||||
|
"""Test Chrome version detection with invalid output."""
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 0
|
||||||
|
mock_result.stdout = "Invalid version string"
|
||||||
|
mock_run.return_value = mock_result
|
||||||
|
|
||||||
|
version_info = detect_chrome_version_from_binary("/path/to/chrome")
|
||||||
|
assert version_info is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestDetectChromeVersionFromRemoteDebugging:
|
||||||
|
"""Test Chrome version detection from remote debugging API."""
|
||||||
|
|
||||||
|
@patch("urllib.request.urlopen")
|
||||||
|
def test_detect_chrome_version_from_remote_debugging_success(self, mock_urlopen:Mock) -> None:
|
||||||
|
"""Test successful Chrome version detection from remote debugging."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.read.return_value = json.dumps({
|
||||||
|
"Browser": "Chrome/136.0.6778.0",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.6778.0 Safari/537.36"
|
||||||
|
}).encode()
|
||||||
|
mock_urlopen.return_value = mock_response
|
||||||
|
|
||||||
|
version_info = detect_chrome_version_from_remote_debugging("127.0.0.1", 9222)
|
||||||
|
|
||||||
|
assert version_info is not None
|
||||||
|
assert version_info.version_string == "136.0.6778.0"
|
||||||
|
assert version_info.major_version == 136
|
||||||
|
assert version_info.browser_name == "Chrome/136.0.6778.0"
|
||||||
|
mock_urlopen.assert_called_once_with("http://127.0.0.1:9222/json/version", timeout = 5)
|
||||||
|
|
||||||
|
@patch("urllib.request.urlopen")
|
||||||
|
def test_detect_chrome_version_from_remote_debugging_edge(self, mock_urlopen:Mock) -> None:
|
||||||
|
"""Test Chrome version detection for Edge from remote debugging."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.read.return_value = json.dumps({
|
||||||
|
"Browser": "Edg/136.0.6778.0",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.6778.0 Safari/537.36 Edg/136.0.6778.0"
|
||||||
|
}).encode()
|
||||||
|
mock_urlopen.return_value = mock_response
|
||||||
|
|
||||||
|
version_info = detect_chrome_version_from_remote_debugging("127.0.0.1", 9222)
|
||||||
|
|
||||||
|
assert version_info is not None
|
||||||
|
assert version_info.major_version == 136
|
||||||
|
assert version_info.browser_name == "Edg/136.0.6778.0"
|
||||||
|
|
||||||
|
@patch("urllib.request.urlopen")
|
||||||
|
def test_detect_chrome_version_from_remote_debugging_no_chrome_in_user_agent(self, mock_urlopen:Mock) -> None:
|
||||||
|
"""Test Chrome version detection with no Chrome in User-Agent."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.read.return_value = json.dumps({
|
||||||
|
"Browser": "Unknown",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
||||||
|
}).encode()
|
||||||
|
mock_urlopen.return_value = mock_response
|
||||||
|
|
||||||
|
version_info = detect_chrome_version_from_remote_debugging("127.0.0.1", 9222)
|
||||||
|
assert version_info is None
|
||||||
|
|
||||||
|
@patch("urllib.request.urlopen")
|
||||||
|
def test_detect_chrome_version_from_remote_debugging_connection_error(self, mock_urlopen:Mock) -> None:
|
||||||
|
"""Test Chrome version detection with connection error."""
|
||||||
|
mock_urlopen.side_effect = Exception("Connection refused")
|
||||||
|
|
||||||
|
version_info = detect_chrome_version_from_remote_debugging("127.0.0.1", 9222)
|
||||||
|
assert version_info is None
|
||||||
|
|
||||||
|
@patch("urllib.request.urlopen")
|
||||||
|
def test_detect_chrome_version_from_remote_debugging_invalid_json(self, mock_urlopen:Mock) -> None:
|
||||||
|
"""Test Chrome version detection with invalid JSON response."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.read.return_value = b"Invalid JSON"
|
||||||
|
mock_urlopen.return_value = mock_response
|
||||||
|
|
||||||
|
version_info = detect_chrome_version_from_remote_debugging("127.0.0.1", 9222)
|
||||||
|
assert version_info is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidateChrome136Configuration:
|
||||||
|
"""Test Chrome 136+ configuration validation."""
|
||||||
|
|
||||||
|
def test_validate_chrome_136_configuration_no_remote_debugging(self) -> None:
|
||||||
|
"""Test validation when no remote debugging is configured."""
|
||||||
|
is_valid, error_message = validate_chrome_136_configuration([], None)
|
||||||
|
assert is_valid is True
|
||||||
|
assert not error_message
|
||||||
|
|
||||||
|
def test_validate_chrome_136_configuration_with_user_data_dir_arg(self) -> None:
|
||||||
|
"""Test validation with --user-data-dir in arguments."""
|
||||||
|
args = ["--remote-debugging-port=9222", "--user-data-dir=/tmp/chrome-debug"]
|
||||||
|
is_valid, error_message = validate_chrome_136_configuration(args, None)
|
||||||
|
assert is_valid is True
|
||||||
|
assert not error_message
|
||||||
|
|
||||||
|
def test_validate_chrome_136_configuration_with_user_data_dir_config(self) -> None:
|
||||||
|
"""Test validation with user_data_dir in configuration."""
|
||||||
|
args = ["--remote-debugging-port=9222"]
|
||||||
|
is_valid, error_message = validate_chrome_136_configuration(args, "/tmp/chrome-debug") # noqa: S108
|
||||||
|
assert is_valid is True
|
||||||
|
assert not error_message
|
||||||
|
|
||||||
|
def test_validate_chrome_136_configuration_with_both(self) -> None:
|
||||||
|
"""Test validation with both user_data_dir argument and config."""
|
||||||
|
args = ["--remote-debugging-port=9222", "--user-data-dir=/tmp/chrome-debug"]
|
||||||
|
is_valid, error_message = validate_chrome_136_configuration(args, "/tmp/chrome-debug") # noqa: S108
|
||||||
|
assert is_valid is True
|
||||||
|
assert not error_message
|
||||||
|
|
||||||
|
def test_validate_chrome_136_configuration_missing_user_data_dir(self) -> None:
|
||||||
|
"""Test validation failure when user_data_dir is missing."""
|
||||||
|
args = ["--remote-debugging-port=9222"]
|
||||||
|
is_valid, error_message = validate_chrome_136_configuration(args, None)
|
||||||
|
assert is_valid is False
|
||||||
|
assert "Chrome/Edge 136+ requires --user-data-dir" in error_message
|
||||||
|
assert "Add --user-data-dir=/path/to/directory to your browser arguments" in error_message
|
||||||
|
|
||||||
|
def test_validate_chrome_136_configuration_empty_user_data_dir_config(self) -> None:
|
||||||
|
"""Test validation failure when user_data_dir config is empty."""
|
||||||
|
args = ["--remote-debugging-port=9222"]
|
||||||
|
is_valid, error_message = validate_chrome_136_configuration(args, "")
|
||||||
|
assert is_valid is False
|
||||||
|
assert "Chrome/Edge 136+ requires --user-data-dir" in error_message
|
||||||
|
|
||||||
|
def test_validate_chrome_136_configuration_whitespace_user_data_dir_config(self) -> None:
|
||||||
|
"""Test validation failure when user_data_dir config is whitespace."""
|
||||||
|
args = ["--remote-debugging-port=9222"]
|
||||||
|
is_valid, error_message = validate_chrome_136_configuration(args, " ")
|
||||||
|
assert is_valid is False
|
||||||
|
assert "Chrome/Edge 136+ requires --user-data-dir" in error_message
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetChromeVersionDiagnosticInfo:
|
||||||
|
"""Test Chrome version diagnostic information gathering."""
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.chrome_version_detector.detect_chrome_version_from_binary")
|
||||||
|
@patch("kleinanzeigen_bot.utils.chrome_version_detector.detect_chrome_version_from_remote_debugging")
|
||||||
|
def test_get_chrome_version_diagnostic_info_binary_only(
|
||||||
|
self, mock_remote_detect:Mock, mock_binary_detect:Mock
|
||||||
|
) -> None:
|
||||||
|
"""Test diagnostic info with binary detection only."""
|
||||||
|
mock_binary_detect.return_value = ChromeVersionInfo("136.0.6778.0", 136, "Chrome")
|
||||||
|
mock_remote_detect.return_value = None
|
||||||
|
|
||||||
|
diagnostic_info = get_chrome_version_diagnostic_info(
|
||||||
|
binary_path = "/path/to/chrome",
|
||||||
|
remote_port = None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert diagnostic_info["binary_detection"] is not None
|
||||||
|
assert diagnostic_info["binary_detection"]["major_version"] == 136
|
||||||
|
assert diagnostic_info["binary_detection"]["is_chrome_136_plus"] is True
|
||||||
|
assert diagnostic_info["remote_detection"] is None
|
||||||
|
assert diagnostic_info["chrome_136_plus_detected"] is True
|
||||||
|
assert len(diagnostic_info["recommendations"]) == 1
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.chrome_version_detector.detect_chrome_version_from_binary")
|
||||||
|
@patch("kleinanzeigen_bot.utils.chrome_version_detector.detect_chrome_version_from_remote_debugging")
|
||||||
|
def test_get_chrome_version_diagnostic_info_remote_only(
|
||||||
|
self, mock_remote_detect:Mock, mock_binary_detect:Mock
|
||||||
|
) -> None:
|
||||||
|
"""Test diagnostic info with remote detection only."""
|
||||||
|
mock_binary_detect.return_value = None
|
||||||
|
mock_remote_detect.return_value = ChromeVersionInfo("120.0.6099.109", 120, "Chrome")
|
||||||
|
|
||||||
|
diagnostic_info = get_chrome_version_diagnostic_info(
|
||||||
|
binary_path = None,
|
||||||
|
remote_port = 9222
|
||||||
|
)
|
||||||
|
|
||||||
|
assert diagnostic_info["binary_detection"] is None
|
||||||
|
assert diagnostic_info["remote_detection"] is not None
|
||||||
|
assert diagnostic_info["remote_detection"]["major_version"] == 120
|
||||||
|
assert diagnostic_info["remote_detection"]["is_chrome_136_plus"] is False
|
||||||
|
assert diagnostic_info["chrome_136_plus_detected"] is False
|
||||||
|
assert len(diagnostic_info["recommendations"]) == 0
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.chrome_version_detector.detect_chrome_version_from_binary")
|
||||||
|
@patch("kleinanzeigen_bot.utils.chrome_version_detector.detect_chrome_version_from_remote_debugging")
|
||||||
|
def test_get_chrome_version_diagnostic_info_both_detections(
|
||||||
|
self, mock_remote_detect:Mock, mock_binary_detect:Mock
|
||||||
|
) -> None:
|
||||||
|
"""Test diagnostic info with both binary and remote detection."""
|
||||||
|
mock_binary_detect.return_value = ChromeVersionInfo("136.0.6778.0", 136, "Chrome")
|
||||||
|
mock_remote_detect.return_value = ChromeVersionInfo("136.0.6778.0", 136, "Chrome")
|
||||||
|
|
||||||
|
diagnostic_info = get_chrome_version_diagnostic_info(
|
||||||
|
binary_path = "/path/to/chrome",
|
||||||
|
remote_port = 9222
|
||||||
|
)
|
||||||
|
|
||||||
|
assert diagnostic_info["binary_detection"] is not None
|
||||||
|
assert diagnostic_info["remote_detection"] is not None
|
||||||
|
assert diagnostic_info["chrome_136_plus_detected"] is True
|
||||||
|
assert len(diagnostic_info["recommendations"]) == 1
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.chrome_version_detector.detect_chrome_version_from_binary")
|
||||||
|
@patch("kleinanzeigen_bot.utils.chrome_version_detector.detect_chrome_version_from_remote_debugging")
|
||||||
|
def test_get_chrome_version_diagnostic_info_no_detection(
|
||||||
|
self, mock_remote_detect:Mock, mock_binary_detect:Mock
|
||||||
|
) -> None:
|
||||||
|
"""Test diagnostic info with no detection."""
|
||||||
|
mock_binary_detect.return_value = None
|
||||||
|
mock_remote_detect.return_value = None
|
||||||
|
|
||||||
|
diagnostic_info = get_chrome_version_diagnostic_info(
|
||||||
|
binary_path = None,
|
||||||
|
remote_port = None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert diagnostic_info["binary_detection"] is None
|
||||||
|
assert diagnostic_info["remote_detection"] is None
|
||||||
|
assert diagnostic_info["chrome_136_plus_detected"] is False
|
||||||
|
assert len(diagnostic_info["recommendations"]) == 0
|
||||||
|
|
||||||
|
def test_get_chrome_version_diagnostic_info_default_values(self) -> None:
|
||||||
|
"""Test diagnostic info with default values."""
|
||||||
|
diagnostic_info = get_chrome_version_diagnostic_info()
|
||||||
|
|
||||||
|
assert diagnostic_info["binary_detection"] is None
|
||||||
|
assert diagnostic_info["remote_detection"] is None
|
||||||
|
assert diagnostic_info["chrome_136_plus_detected"] is False
|
||||||
|
assert diagnostic_info["configuration_valid"] is True
|
||||||
|
assert diagnostic_info["recommendations"] == []
|
||||||
@@ -1092,9 +1092,6 @@ class TestWebScrapingDiagnostics:
|
|||||||
scraper_with_config.diagnose_browser_issues()
|
scraper_with_config.diagnose_browser_issues()
|
||||||
|
|
||||||
assert "(info) macOS detected - check Gatekeeper and security settings" in caplog.text
|
assert "(info) macOS detected - check Gatekeeper and security settings" in caplog.text
|
||||||
assert " IMPORTANT: macOS Chrome remote debugging requires --user-data-dir flag" in caplog.text
|
|
||||||
assert ' Add to your config.yaml: user_data_dir: "/tmp/chrome-debug-profile"' in caplog.text
|
|
||||||
assert " And to browser arguments: --user-data-dir=/tmp/chrome-debug-profile" in caplog.text
|
|
||||||
|
|
||||||
def test_diagnose_browser_issues_macos_platform_with_user_data_dir(
|
def test_diagnose_browser_issues_macos_platform_with_user_data_dir(
|
||||||
self, scraper_with_config:WebScrapingMixin, caplog:pytest.LogCaptureFixture, tmp_path:Path
|
self, scraper_with_config:WebScrapingMixin, caplog:pytest.LogCaptureFixture, tmp_path:Path
|
||||||
@@ -1110,8 +1107,6 @@ class TestWebScrapingDiagnostics:
|
|||||||
scraper_with_config.diagnose_browser_issues()
|
scraper_with_config.diagnose_browser_issues()
|
||||||
|
|
||||||
assert "(info) macOS detected - check Gatekeeper and security settings" in caplog.text
|
assert "(info) macOS detected - check Gatekeeper and security settings" in caplog.text
|
||||||
# Should not show the warning about user-data-dir being required
|
|
||||||
assert "IMPORTANT: macOS Chrome remote debugging requires --user-data-dir flag" not in caplog.text
|
|
||||||
|
|
||||||
def test_diagnose_browser_issues_linux_platform_not_root(self, scraper_with_config:WebScrapingMixin, caplog:pytest.LogCaptureFixture) -> None:
|
def test_diagnose_browser_issues_linux_platform_not_root(self, scraper_with_config:WebScrapingMixin, caplog:pytest.LogCaptureFixture) -> None:
|
||||||
"""Test diagnostic on Linux platform when not running as root."""
|
"""Test diagnostic on Linux platform when not running as root."""
|
||||||
@@ -1153,11 +1148,6 @@ class TestWebScrapingDiagnostics:
|
|||||||
scraper_with_config.browser_config.arguments = ["--remote-debugging-port=9222"]
|
scraper_with_config.browser_config.arguments = ["--remote-debugging-port=9222"]
|
||||||
scraper_with_config.diagnose_browser_issues()
|
scraper_with_config.diagnose_browser_issues()
|
||||||
|
|
||||||
assert "On macOS, try: /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome" in caplog.text
|
|
||||||
assert "--remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug-profile --disable-dev-shm-usage" in caplog.text
|
|
||||||
assert 'Or: open -a "Google Chrome" --args --remote-debugging-port=9222' in caplog.text
|
|
||||||
assert " IMPORTANT: --user-data-dir is MANDATORY for macOS Chrome remote debugging" in caplog.text
|
|
||||||
|
|
||||||
def test_diagnose_browser_issues_complete_diagnostic_flow(
|
def test_diagnose_browser_issues_complete_diagnostic_flow(
|
||||||
self, scraper_with_config:WebScrapingMixin, caplog:pytest.LogCaptureFixture, tmp_path:Path
|
self, scraper_with_config:WebScrapingMixin, caplog:pytest.LogCaptureFixture, tmp_path:Path
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1347,9 +1337,6 @@ class TestWebScrapingDiagnostics:
|
|||||||
patch.object(scraper_with_config, "get_compatible_browser", return_value = "/usr/bin/chrome"):
|
patch.object(scraper_with_config, "get_compatible_browser", return_value = "/usr/bin/chrome"):
|
||||||
scraper_with_config.diagnose_browser_issues()
|
scraper_with_config.diagnose_browser_issues()
|
||||||
|
|
||||||
assert "IMPORTANT: macOS Chrome remote debugging requires --user-data-dir flag" in caplog.text
|
|
||||||
assert "Add to your config.yaml: user_data_dir:" in caplog.text
|
|
||||||
|
|
||||||
def test_diagnose_browser_issues_linux_root_user(
|
def test_diagnose_browser_issues_linux_root_user(
|
||||||
self, scraper_with_config:WebScrapingMixin, caplog:pytest.LogCaptureFixture
|
self, scraper_with_config:WebScrapingMixin, caplog:pytest.LogCaptureFixture
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
510
tests/unit/test_web_scraping_mixin_chrome_version.py
Normal file
510
tests/unit/test_web_scraping_mixin_chrome_version.py
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
# SPDX-FileCopyrightText: © Jens Bergmann and contributors
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from kleinanzeigen_bot.utils.chrome_version_detector import ChromeVersionInfo
|
||||||
|
from kleinanzeigen_bot.utils.web_scraping_mixin import WebScrapingMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TestWebScrapingMixinChromeVersionValidation:
|
||||||
|
"""Test Chrome version validation in WebScrapingMixin."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def scraper(self) -> WebScrapingMixin:
|
||||||
|
"""Create a WebScrapingMixin instance for testing."""
|
||||||
|
return WebScrapingMixin()
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.detect_chrome_version_from_binary")
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.validate_chrome_136_configuration")
|
||||||
|
async def test_validate_chrome_version_configuration_chrome_136_plus_valid(
|
||||||
|
self, mock_validate:Mock, mock_detect:Mock, scraper:WebScrapingMixin
|
||||||
|
) -> None:
|
||||||
|
"""Test Chrome 136+ validation with valid configuration."""
|
||||||
|
# Setup mocks
|
||||||
|
mock_detect.return_value = ChromeVersionInfo("136.0.6778.0", 136, "Chrome")
|
||||||
|
mock_validate.return_value = (True, "")
|
||||||
|
|
||||||
|
# Configure scraper
|
||||||
|
scraper.browser_config.binary_location = "/path/to/chrome"
|
||||||
|
scraper.browser_config.arguments = ["--remote-debugging-port=9222", "--user-data-dir=/tmp/chrome-debug"] # noqa: S108
|
||||||
|
scraper.browser_config.user_data_dir = "/tmp/chrome-debug" # noqa: S108
|
||||||
|
|
||||||
|
# Temporarily unset PYTEST_CURRENT_TEST to allow validation to run
|
||||||
|
original_env = os.environ.get("PYTEST_CURRENT_TEST")
|
||||||
|
if "PYTEST_CURRENT_TEST" in os.environ:
|
||||||
|
del os.environ["PYTEST_CURRENT_TEST"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test validation
|
||||||
|
await scraper._validate_chrome_version_configuration()
|
||||||
|
|
||||||
|
# Verify mocks were called correctly
|
||||||
|
mock_detect.assert_called_once_with("/path/to/chrome")
|
||||||
|
mock_validate.assert_called_once_with(
|
||||||
|
["--remote-debugging-port=9222", "--user-data-dir=/tmp/chrome-debug"], # noqa: S108
|
||||||
|
"/tmp/chrome-debug" # noqa: S108
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# Restore environment
|
||||||
|
if original_env:
|
||||||
|
os.environ["PYTEST_CURRENT_TEST"] = original_env
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.detect_chrome_version_from_binary")
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.validate_chrome_136_configuration")
|
||||||
|
async def test_validate_chrome_version_configuration_chrome_136_plus_invalid(
|
||||||
|
self, mock_validate:Mock, mock_detect:Mock, scraper:WebScrapingMixin, caplog:pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
"""Test Chrome 136+ validation with invalid configuration."""
|
||||||
|
# Setup mocks
|
||||||
|
mock_detect.return_value = ChromeVersionInfo("136.0.6778.0", 136, "Chrome")
|
||||||
|
mock_validate.return_value = (False, "Chrome 136+ requires --user-data-dir")
|
||||||
|
|
||||||
|
# Configure scraper
|
||||||
|
scraper.browser_config.binary_location = "/path/to/chrome"
|
||||||
|
scraper.browser_config.arguments = ["--remote-debugging-port=9222"]
|
||||||
|
scraper.browser_config.user_data_dir = None
|
||||||
|
|
||||||
|
# Temporarily unset PYTEST_CURRENT_TEST to allow validation to run
|
||||||
|
original_env = os.environ.get("PYTEST_CURRENT_TEST")
|
||||||
|
if "PYTEST_CURRENT_TEST" in os.environ:
|
||||||
|
del os.environ["PYTEST_CURRENT_TEST"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test validation should log error but not raise exception due to error handling
|
||||||
|
await scraper._validate_chrome_version_configuration()
|
||||||
|
|
||||||
|
# Verify error was logged
|
||||||
|
assert "Chrome 136+ configuration validation failed" in caplog.text
|
||||||
|
assert "Chrome 136+ requires --user-data-dir" in caplog.text
|
||||||
|
finally:
|
||||||
|
# Restore environment
|
||||||
|
if original_env:
|
||||||
|
os.environ["PYTEST_CURRENT_TEST"] = original_env
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.detect_chrome_version_from_binary")
|
||||||
|
async def test_validate_chrome_version_configuration_chrome_pre_136(
|
||||||
|
self, mock_detect:Mock, scraper:WebScrapingMixin
|
||||||
|
) -> None:
|
||||||
|
"""Test Chrome pre-136 validation (no special requirements)."""
|
||||||
|
# Setup mocks
|
||||||
|
mock_detect.return_value = ChromeVersionInfo("120.0.6099.109", 120, "Chrome")
|
||||||
|
|
||||||
|
# Configure scraper
|
||||||
|
scraper.browser_config.binary_location = "/path/to/chrome"
|
||||||
|
scraper.browser_config.arguments = ["--remote-debugging-port=9222"]
|
||||||
|
scraper.browser_config.user_data_dir = None
|
||||||
|
|
||||||
|
# Temporarily unset PYTEST_CURRENT_TEST to allow validation to run
|
||||||
|
original_env = os.environ.get("PYTEST_CURRENT_TEST")
|
||||||
|
if "PYTEST_CURRENT_TEST" in os.environ:
|
||||||
|
del os.environ["PYTEST_CURRENT_TEST"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test validation should pass without validation
|
||||||
|
await scraper._validate_chrome_version_configuration()
|
||||||
|
|
||||||
|
# Verify detection was called but no validation
|
||||||
|
mock_detect.assert_called_once_with("/path/to/chrome")
|
||||||
|
finally:
|
||||||
|
# Restore environment
|
||||||
|
if original_env:
|
||||||
|
os.environ["PYTEST_CURRENT_TEST"] = original_env
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.chrome_version_detector.detect_chrome_version_from_binary")
|
||||||
|
async def test_validate_chrome_version_configuration_no_binary_location(
|
||||||
|
self, mock_detect:Mock, scraper:WebScrapingMixin
|
||||||
|
) -> None:
|
||||||
|
"""Test Chrome version validation when no binary location is set."""
|
||||||
|
# Configure scraper without binary location
|
||||||
|
scraper.browser_config.binary_location = None
|
||||||
|
|
||||||
|
# Test validation should pass without detection
|
||||||
|
await scraper._validate_chrome_version_configuration()
|
||||||
|
|
||||||
|
# Verify detection was not called
|
||||||
|
mock_detect.assert_not_called()
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.detect_chrome_version_from_binary")
|
||||||
|
async def test_validate_chrome_version_configuration_detection_fails(
|
||||||
|
self, mock_detect:Mock, scraper:WebScrapingMixin, caplog:pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
"""Test Chrome version validation when detection fails."""
|
||||||
|
# Setup mocks
|
||||||
|
mock_detect.return_value = None
|
||||||
|
|
||||||
|
# Configure scraper
|
||||||
|
scraper.browser_config.binary_location = "/path/to/chrome"
|
||||||
|
|
||||||
|
# Temporarily unset PYTEST_CURRENT_TEST to allow validation to run
|
||||||
|
original_env = os.environ.get("PYTEST_CURRENT_TEST")
|
||||||
|
if "PYTEST_CURRENT_TEST" in os.environ:
|
||||||
|
del os.environ["PYTEST_CURRENT_TEST"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test validation should pass without validation
|
||||||
|
await scraper._validate_chrome_version_configuration()
|
||||||
|
|
||||||
|
# Verify detection was called
|
||||||
|
mock_detect.assert_called_once_with("/path/to/chrome")
|
||||||
|
|
||||||
|
# Verify debug log message (line 824)
|
||||||
|
assert "Could not detect browser version, skipping validation" in caplog.text
|
||||||
|
finally:
|
||||||
|
# Restore environment
|
||||||
|
if original_env:
|
||||||
|
os.environ["PYTEST_CURRENT_TEST"] = original_env
|
||||||
|
|
||||||
|
|
||||||
|
class TestWebScrapingMixinChromeVersionDiagnostics:
|
||||||
|
"""Test Chrome version diagnostics in WebScrapingMixin."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def scraper(self) -> WebScrapingMixin:
|
||||||
|
"""Create a WebScrapingMixin instance for testing."""
|
||||||
|
return WebScrapingMixin()
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.get_chrome_version_diagnostic_info")
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.validate_chrome_136_configuration")
|
||||||
|
def test_diagnose_chrome_version_issues_binary_detection(
|
||||||
|
self, mock_validate:Mock, mock_get_diagnostic:Mock, scraper:WebScrapingMixin, caplog:pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
"""Test Chrome version diagnostics with binary detection."""
|
||||||
|
# Setup mocks
|
||||||
|
mock_get_diagnostic.return_value = {
|
||||||
|
"binary_detection": {
|
||||||
|
"version_string": "136.0.6778.0",
|
||||||
|
"major_version": 136,
|
||||||
|
"browser_name": "Chrome",
|
||||||
|
"is_chrome_136_plus": True
|
||||||
|
},
|
||||||
|
"remote_detection": None,
|
||||||
|
"chrome_136_plus_detected": True,
|
||||||
|
"recommendations": []
|
||||||
|
}
|
||||||
|
mock_validate.return_value = (True, "")
|
||||||
|
|
||||||
|
# Configure scraper
|
||||||
|
scraper.browser_config.binary_location = "/path/to/chrome"
|
||||||
|
scraper.browser_config.arguments = ["--remote-debugging-port=9222", "--user-data-dir=/tmp/chrome-debug"]
|
||||||
|
|
||||||
|
# Temporarily unset PYTEST_CURRENT_TEST to allow diagnostics to run
|
||||||
|
original_env = os.environ.get("PYTEST_CURRENT_TEST")
|
||||||
|
if "PYTEST_CURRENT_TEST" in os.environ:
|
||||||
|
del os.environ["PYTEST_CURRENT_TEST"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test diagnostics
|
||||||
|
scraper._diagnose_chrome_version_issues(9222)
|
||||||
|
|
||||||
|
# Verify logs
|
||||||
|
assert "Chrome version from binary: Chrome 136.0.6778.0 (major: 136)" in caplog.text
|
||||||
|
assert "Chrome 136+ detected - security validation required" in caplog.text
|
||||||
|
|
||||||
|
# Verify mocks were called
|
||||||
|
mock_get_diagnostic.assert_called_once_with(
|
||||||
|
binary_path = "/path/to/chrome",
|
||||||
|
remote_port = 9222
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# Restore environment
|
||||||
|
if original_env:
|
||||||
|
os.environ["PYTEST_CURRENT_TEST"] = original_env
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.get_chrome_version_diagnostic_info")
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.validate_chrome_136_configuration")
|
||||||
|
def test_diagnose_chrome_version_issues_remote_detection(
|
||||||
|
self, mock_validate:Mock, mock_get_diagnostic:Mock, scraper:WebScrapingMixin, caplog:pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
"""Test Chrome version diagnostics with remote detection."""
|
||||||
|
# Setup mocks
|
||||||
|
mock_get_diagnostic.return_value = {
|
||||||
|
"binary_detection": None,
|
||||||
|
"remote_detection": {
|
||||||
|
"version_string": "136.0.6778.0",
|
||||||
|
"major_version": 136,
|
||||||
|
"browser_name": "Chrome",
|
||||||
|
"is_chrome_136_plus": True
|
||||||
|
},
|
||||||
|
"chrome_136_plus_detected": True,
|
||||||
|
"recommendations": []
|
||||||
|
}
|
||||||
|
mock_validate.return_value = (False, "Chrome 136+ requires --user-data-dir")
|
||||||
|
|
||||||
|
# Configure scraper
|
||||||
|
scraper.browser_config.binary_location = "/path/to/chrome"
|
||||||
|
scraper.browser_config.arguments = ["--remote-debugging-port=9222"]
|
||||||
|
|
||||||
|
# Temporarily unset PYTEST_CURRENT_TEST to allow diagnostics to run
|
||||||
|
original_env = os.environ.get("PYTEST_CURRENT_TEST")
|
||||||
|
if "PYTEST_CURRENT_TEST" in os.environ:
|
||||||
|
del os.environ["PYTEST_CURRENT_TEST"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test diagnostics
|
||||||
|
scraper._diagnose_chrome_version_issues(9222)
|
||||||
|
|
||||||
|
# Verify logs
|
||||||
|
assert "Chrome version from remote debugging: Chrome 136.0.6778.0 (major: 136)" in caplog.text
|
||||||
|
assert "Remote Chrome 136+ detected - validating configuration" in caplog.text
|
||||||
|
assert "Chrome 136+ configuration validation failed" in caplog.text
|
||||||
|
|
||||||
|
# Verify validation was called
|
||||||
|
mock_validate.assert_called_once_with(
|
||||||
|
["--remote-debugging-port=9222"],
|
||||||
|
None
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# Restore environment
|
||||||
|
if original_env:
|
||||||
|
os.environ["PYTEST_CURRENT_TEST"] = original_env
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.get_chrome_version_diagnostic_info")
|
||||||
|
def test_diagnose_chrome_version_issues_no_detection(
|
||||||
|
self, mock_get_diagnostic:Mock, scraper:WebScrapingMixin, caplog:pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
"""Test Chrome version diagnostics with no detection."""
|
||||||
|
# Setup mocks
|
||||||
|
mock_get_diagnostic.return_value = {
|
||||||
|
"binary_detection": None,
|
||||||
|
"remote_detection": None,
|
||||||
|
"chrome_136_plus_detected": False,
|
||||||
|
"recommendations": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configure scraper
|
||||||
|
scraper.browser_config.binary_location = "/path/to/chrome"
|
||||||
|
|
||||||
|
# Temporarily unset PYTEST_CURRENT_TEST to allow diagnostics to run
|
||||||
|
original_env = os.environ.get("PYTEST_CURRENT_TEST")
|
||||||
|
if "PYTEST_CURRENT_TEST" in os.environ:
|
||||||
|
del os.environ["PYTEST_CURRENT_TEST"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test diagnostics
|
||||||
|
scraper._diagnose_chrome_version_issues(0)
|
||||||
|
|
||||||
|
# Verify no Chrome version logs
|
||||||
|
assert "Chrome version from binary" not in caplog.text
|
||||||
|
assert "Chrome version from remote debugging" not in caplog.text
|
||||||
|
finally:
|
||||||
|
# Restore environment
|
||||||
|
if original_env:
|
||||||
|
os.environ["PYTEST_CURRENT_TEST"] = original_env
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.get_chrome_version_diagnostic_info")
|
||||||
|
def test_diagnose_chrome_version_issues_chrome_136_plus_recommendations(
|
||||||
|
self, mock_get_diagnostic:Mock, scraper:WebScrapingMixin, caplog:pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
"""Test Chrome version diagnostics with Chrome 136+ recommendations."""
|
||||||
|
# Setup mocks
|
||||||
|
mock_get_diagnostic.return_value = {
|
||||||
|
"binary_detection": {
|
||||||
|
"version_string": "136.0.6778.0",
|
||||||
|
"major_version": 136,
|
||||||
|
"browser_name": "Chrome",
|
||||||
|
"is_chrome_136_plus": True
|
||||||
|
},
|
||||||
|
"remote_detection": None,
|
||||||
|
"chrome_136_plus_detected": True,
|
||||||
|
"recommendations": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configure scraper
|
||||||
|
scraper.browser_config.binary_location = "/path/to/chrome"
|
||||||
|
|
||||||
|
# Temporarily unset PYTEST_CURRENT_TEST to allow diagnostics to run
|
||||||
|
original_env = os.environ.get("PYTEST_CURRENT_TEST")
|
||||||
|
if "PYTEST_CURRENT_TEST" in os.environ:
|
||||||
|
del os.environ["PYTEST_CURRENT_TEST"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test diagnostics
|
||||||
|
scraper._diagnose_chrome_version_issues(0)
|
||||||
|
|
||||||
|
# Verify recommendations
|
||||||
|
assert "Chrome/Edge 136+ security changes require --user-data-dir for remote debugging" in caplog.text
|
||||||
|
assert "https://developer.chrome.com/blog/remote-debugging-port" in caplog.text
|
||||||
|
finally:
|
||||||
|
# Restore environment
|
||||||
|
if original_env:
|
||||||
|
os.environ["PYTEST_CURRENT_TEST"] = original_env
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.get_chrome_version_diagnostic_info")
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.validate_chrome_136_configuration")
|
||||||
|
def test_diagnose_chrome_version_issues_binary_pre_136(
|
||||||
|
self, mock_validate:Mock, mock_get_diagnostic:Mock, scraper:WebScrapingMixin, caplog:pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
"""Test Chrome version diagnostics with pre-136 binary detection (lines 832-849)."""
|
||||||
|
# Setup mocks to ensure exact branch coverage
|
||||||
|
mock_get_diagnostic.return_value = {
|
||||||
|
"binary_detection": {
|
||||||
|
"version_string": "120.0.6099.109",
|
||||||
|
"major_version": 120,
|
||||||
|
"browser_name": "Chrome",
|
||||||
|
"is_chrome_136_plus": False # This triggers the else branch (lines 832-849)
|
||||||
|
},
|
||||||
|
"remote_detection": None, # Ensure this is None to avoid other branches
|
||||||
|
"chrome_136_plus_detected": False, # Ensure this is False to avoid recommendations
|
||||||
|
"recommendations": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configure scraper
|
||||||
|
scraper.browser_config.binary_location = "/path/to/chrome"
|
||||||
|
|
||||||
|
# Temporarily unset PYTEST_CURRENT_TEST to allow diagnostics to run
|
||||||
|
original_env = os.environ.get("PYTEST_CURRENT_TEST")
|
||||||
|
if "PYTEST_CURRENT_TEST" in os.environ:
|
||||||
|
del os.environ["PYTEST_CURRENT_TEST"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test diagnostics
|
||||||
|
scraper._diagnose_chrome_version_issues(0)
|
||||||
|
|
||||||
|
# Verify pre-136 log message (lines 832-849)
|
||||||
|
assert "Chrome pre-136 detected - no special security requirements" in caplog.text
|
||||||
|
|
||||||
|
# Verify that the diagnostic function was called with correct parameters
|
||||||
|
mock_get_diagnostic.assert_called_once_with(
|
||||||
|
binary_path = "/path/to/chrome",
|
||||||
|
remote_port = None
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# Restore environment
|
||||||
|
if original_env:
|
||||||
|
os.environ["PYTEST_CURRENT_TEST"] = original_env
|
||||||
|
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.get_chrome_version_diagnostic_info")
|
||||||
|
@patch("kleinanzeigen_bot.utils.web_scraping_mixin.validate_chrome_136_configuration")
|
||||||
|
def test_diagnose_chrome_version_issues_remote_validation_passes(
|
||||||
|
self, mock_validate:Mock, mock_get_diagnostic:Mock, scraper:WebScrapingMixin, caplog:pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
"""Test Chrome version diagnostics with remote validation passing (line 846)."""
|
||||||
|
# Setup mocks
|
||||||
|
mock_get_diagnostic.return_value = {
|
||||||
|
"binary_detection": None,
|
||||||
|
"remote_detection": {
|
||||||
|
"version_string": "136.0.6778.0",
|
||||||
|
"major_version": 136,
|
||||||
|
"browser_name": "Chrome",
|
||||||
|
"is_chrome_136_plus": True
|
||||||
|
},
|
||||||
|
"chrome_136_plus_detected": True,
|
||||||
|
"recommendations": []
|
||||||
|
}
|
||||||
|
mock_validate.return_value = (True, "") # This triggers the else branch (line 846)
|
||||||
|
|
||||||
|
# Configure scraper
|
||||||
|
scraper.browser_config.binary_location = "/path/to/chrome"
|
||||||
|
scraper.browser_config.arguments = ["--remote-debugging-port=9222", "--user-data-dir=/tmp/chrome-debug"] # noqa: S108
|
||||||
|
scraper.browser_config.user_data_dir = "/tmp/chrome-debug" # noqa: S108
|
||||||
|
|
||||||
|
# Temporarily unset PYTEST_CURRENT_TEST to allow diagnostics to run
|
||||||
|
original_env = os.environ.get("PYTEST_CURRENT_TEST")
|
||||||
|
if "PYTEST_CURRENT_TEST" in os.environ:
|
||||||
|
del os.environ["PYTEST_CURRENT_TEST"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test diagnostics
|
||||||
|
scraper._diagnose_chrome_version_issues(9222)
|
||||||
|
|
||||||
|
# Verify validation passed log message (line 846)
|
||||||
|
assert "Chrome 136+ configuration validation passed" in caplog.text
|
||||||
|
|
||||||
|
# Verify validation was called with correct arguments
|
||||||
|
mock_validate.assert_called_once_with(
|
||||||
|
["--remote-debugging-port=9222", "--user-data-dir=/tmp/chrome-debug"], # noqa: S108
|
||||||
|
"/tmp/chrome-debug" # noqa: S108
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# Restore environment
|
||||||
|
if original_env:
|
||||||
|
os.environ["PYTEST_CURRENT_TEST"] = original_env
|
||||||
|
|
||||||
|
|
||||||
|
class TestWebScrapingMixinIntegration:
|
||||||
|
"""Test integration of Chrome version detection in WebScrapingMixin."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def scraper(self) -> WebScrapingMixin:
|
||||||
|
"""Create a WebScrapingMixin instance for testing."""
|
||||||
|
return WebScrapingMixin()
|
||||||
|
|
||||||
|
@patch.object(WebScrapingMixin, "_validate_chrome_version_configuration")
|
||||||
|
@patch.object(WebScrapingMixin, "get_compatible_browser")
|
||||||
|
async def test_create_browser_session_calls_chrome_validation(
|
||||||
|
self, mock_get_browser:Mock, mock_validate:Mock, scraper:WebScrapingMixin
|
||||||
|
) -> None:
|
||||||
|
"""Test that create_browser_session calls Chrome version validation."""
|
||||||
|
# Setup mocks
|
||||||
|
mock_get_browser.return_value = "/path/to/chrome"
|
||||||
|
mock_validate.return_value = None
|
||||||
|
|
||||||
|
# Configure scraper
|
||||||
|
scraper.browser_config.binary_location = None
|
||||||
|
|
||||||
|
# Test that validation is called
|
||||||
|
try:
|
||||||
|
await scraper.create_browser_session()
|
||||||
|
except Exception: # noqa: S110
|
||||||
|
# We expect it to fail later, but validation should be called first
|
||||||
|
# This is expected behavior in the test - we're testing that validation runs before failure
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Verify validation was called
|
||||||
|
mock_validate.assert_called_once()
|
||||||
|
|
||||||
|
@patch.object(WebScrapingMixin, "_diagnose_chrome_version_issues")
|
||||||
|
@patch.object(WebScrapingMixin, "get_compatible_browser")
|
||||||
|
def test_diagnose_browser_issues_calls_chrome_diagnostics(
|
||||||
|
self, mock_get_browser:Mock, mock_diagnose:Mock, scraper:WebScrapingMixin
|
||||||
|
) -> None:
|
||||||
|
"""Test that diagnose_browser_issues calls Chrome version diagnostics."""
|
||||||
|
# Setup mocks
|
||||||
|
mock_get_browser.return_value = "/path/to/chrome"
|
||||||
|
|
||||||
|
# Configure scraper
|
||||||
|
scraper.browser_config.binary_location = None
|
||||||
|
scraper.browser_config.arguments = ["--remote-debugging-port=9222"]
|
||||||
|
|
||||||
|
# Test diagnostics
|
||||||
|
scraper.diagnose_browser_issues()
|
||||||
|
|
||||||
|
# Verify Chrome diagnostics was called
|
||||||
|
mock_diagnose.assert_called_once_with(9222)
|
||||||
|
|
||||||
|
def test_backward_compatibility_old_configs_still_work(self) -> None:
|
||||||
|
"""Test that old configurations without Chrome 136+ validation still work."""
|
||||||
|
# Create a scraper with old-style config (no user_data_dir)
|
||||||
|
scraper = WebScrapingMixin()
|
||||||
|
|
||||||
|
# Set up old-style config (pre-Chrome 136+)
|
||||||
|
scraper.browser_config.arguments = ["--remote-debugging-port=9222"]
|
||||||
|
scraper.browser_config.user_data_dir = None # Old configs didn't have this
|
||||||
|
|
||||||
|
# Mock Chrome version detection to return pre-136 version
|
||||||
|
with patch("kleinanzeigen_bot.utils.web_scraping_mixin.detect_chrome_version_from_binary") as mock_detect:
|
||||||
|
mock_detect.return_value = ChromeVersionInfo(
|
||||||
|
"120.0.6099.109", 120, "Chrome"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Temporarily unset PYTEST_CURRENT_TEST to allow validation to run
|
||||||
|
original_env = os.environ.get("PYTEST_CURRENT_TEST")
|
||||||
|
if "PYTEST_CURRENT_TEST" in os.environ:
|
||||||
|
del os.environ["PYTEST_CURRENT_TEST"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# This should not raise an exception for pre-136 Chrome
|
||||||
|
asyncio.run(scraper._validate_chrome_version_configuration())
|
||||||
|
|
||||||
|
# Verify that the validation passed (no exception raised)
|
||||||
|
# The method should log that pre-136 Chrome was detected
|
||||||
|
# and no special validation is required
|
||||||
|
finally:
|
||||||
|
# Restore environment
|
||||||
|
if original_env:
|
||||||
|
os.environ["PYTEST_CURRENT_TEST"] = original_env
|
||||||
Reference in New Issue
Block a user