feat: chrome version detection clean (#607)

This commit is contained in:
Jens Bergmann
2025-08-18 13:19:50 +02:00
committed by GitHub
parent df24a675a9
commit 332926519d
8 changed files with 1304 additions and 33 deletions

View 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"] == []

View File

@@ -1092,9 +1092,6 @@ class TestWebScrapingDiagnostics:
scraper_with_config.diagnose_browser_issues()
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(
self, scraper_with_config:WebScrapingMixin, caplog:pytest.LogCaptureFixture, tmp_path:Path
@@ -1110,8 +1107,6 @@ class TestWebScrapingDiagnostics:
scraper_with_config.diagnose_browser_issues()
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:
"""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.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(
self, scraper_with_config:WebScrapingMixin, caplog:pytest.LogCaptureFixture, tmp_path:Path
) -> None:
@@ -1347,9 +1337,6 @@ class TestWebScrapingDiagnostics:
patch.object(scraper_with_config, "get_compatible_browser", return_value = "/usr/bin/chrome"):
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(
self, scraper_with_config:WebScrapingMixin, caplog:pytest.LogCaptureFixture
) -> None:

View 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