mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
feat: keep login selector fallbacks close to auth flow (#855)
This commit is contained in:
@@ -7,7 +7,7 @@ import urllib.parse as urllib_parse
|
||||
from datetime import datetime
|
||||
from gettext import gettext as _
|
||||
from pathlib import Path
|
||||
from typing import Any, Final, cast
|
||||
from typing import Any, Final, Sequence, cast
|
||||
|
||||
import certifi, colorama, nodriver # isort: skip
|
||||
from nodriver.core.connection import ProtocolException
|
||||
@@ -34,10 +34,19 @@ LOG.setLevel(loggers.INFO)
|
||||
|
||||
PUBLISH_MAX_RETRIES:Final[int] = 3
|
||||
_NUMERIC_IDS_RE:Final[re.Pattern[str]] = re.compile(r"^\d+(,\d+)*$")
|
||||
_LOGIN_DETECTION_SELECTORS:Final[list[tuple["By", str]]] = [
|
||||
(By.CLASS_NAME, "mr-medium"),
|
||||
(By.ID, "user-email"),
|
||||
]
|
||||
_LOGIN_DETECTION_SELECTOR_LABELS:Final[tuple[str, ...]] = ("user_info_primary", "user_info_secondary")
|
||||
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
|
||||
def _format_login_detection_selectors(selectors:Sequence[tuple["By", str]]) -> str:
|
||||
return ", ".join(f"{selector_type.name}={selector_value}" for selector_type, selector_value in selectors)
|
||||
|
||||
|
||||
class AdUpdateStrategy(enum.Enum):
|
||||
REPLACE = enum.auto()
|
||||
MODIFY = enum.auto()
|
||||
@@ -1245,38 +1254,39 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
login_check_timeout,
|
||||
effective_timeout,
|
||||
)
|
||||
|
||||
login_selectors = [
|
||||
(By.CLASS_NAME, "mr-medium"),
|
||||
(By.ID, "user-email"),
|
||||
]
|
||||
primary_selector_index = 0
|
||||
tried_login_selectors = _format_login_detection_selectors(_LOGIN_DETECTION_SELECTORS)
|
||||
|
||||
try:
|
||||
user_info, matched_selector = await self.web_text_first_available(
|
||||
login_selectors,
|
||||
_LOGIN_DETECTION_SELECTORS,
|
||||
timeout = login_check_timeout,
|
||||
key = "login_detection",
|
||||
description = "login_detection(selector_group)",
|
||||
)
|
||||
if username in user_info.lower():
|
||||
if matched_selector == primary_selector_index:
|
||||
LOG.debug("Login detected via .mr-medium element")
|
||||
else:
|
||||
LOG.debug("Login detected via #user-email element")
|
||||
matched_selector_label = (
|
||||
_LOGIN_DETECTION_SELECTOR_LABELS[matched_selector]
|
||||
if 0 <= matched_selector < len(_LOGIN_DETECTION_SELECTOR_LABELS)
|
||||
else f"selector_index_{matched_selector}"
|
||||
)
|
||||
LOG.debug("Login detected via login detection selector '%s'", matched_selector_label)
|
||||
return True
|
||||
except TimeoutError:
|
||||
LOG.debug("Timeout waiting for login detection selector group after %.1fs", effective_timeout)
|
||||
|
||||
if not include_probe:
|
||||
LOG.debug("No login detected - neither .mr-medium nor #user-email found with username")
|
||||
LOG.debug("No login detected via configured login detection selectors (%s)", tried_login_selectors)
|
||||
return False
|
||||
|
||||
state = await self._auth_probe_login_state()
|
||||
if state == LoginState.LOGGED_IN:
|
||||
return True
|
||||
|
||||
LOG.debug("No login detected - DOM elements not found and server probe returned %s", state.name)
|
||||
LOG.debug(
|
||||
"No login detected - DOM login detection selectors (%s) did not confirm login and server probe returned %s",
|
||||
tried_login_selectors,
|
||||
state.name,
|
||||
)
|
||||
return False
|
||||
|
||||
async def _fetch_published_ads(self) -> list[dict[str, Any]]:
|
||||
|
||||
@@ -97,11 +97,8 @@ kleinanzeigen_bot/__init__.py:
|
||||
|
||||
is_logged_in:
|
||||
"Starting login detection (timeout: %.1fs base, %.1fs effective with multiplier/backoff)": "Starte Login-Erkennung (Timeout: %.1fs Basis, %.1fs effektiv mit Multiplikator/Backoff)"
|
||||
"Login detected via .mr-medium element": "Login erkannt über .mr-medium Element"
|
||||
"Login detected via #user-email element": "Login erkannt über #user-email Element"
|
||||
"Login detected via login detection selector '%s'": "Login erkannt über Login-Erkennungs-Selektor '%s'"
|
||||
"Timeout waiting for login detection selector group after %.1fs": "Timeout beim Warten auf die Login-Erkennungs-Selektorgruppe nach %.1fs"
|
||||
"No login detected - neither .mr-medium nor #user-email found with username": "Kein Login erkannt - weder .mr-medium noch #user-email mit Benutzername gefunden"
|
||||
"No login detected - DOM elements not found and server probe returned %s": "Kein Login erkannt - DOM-Elemente nicht gefunden und Server-Probe ergab %s"
|
||||
|
||||
handle_after_login_logic:
|
||||
"# Device verification message detected. Please follow the instruction displayed in the Browser.": "# Nachricht zur Geräteverifizierung erkannt. Bitte den Anweisungen im Browser folgen."
|
||||
|
||||
@@ -483,6 +483,63 @@ class TestKleinanzeigenBotAuthentication:
|
||||
assert call_args.kwargs["key"] == "login_detection"
|
||||
assert call_args.kwargs["timeout"] == test_bot._timeout("login_detection")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_logged_in_logs_selector_label_without_raw_selector_literals(
|
||||
self, test_bot:KleinanzeigenBot, caplog:pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Login detection logs should reference stable labels, not raw selector values."""
|
||||
caplog.set_level("DEBUG")
|
||||
|
||||
with (
|
||||
caplog.at_level("DEBUG"),
|
||||
patch.object(test_bot, "web_text_first_available", new_callable = AsyncMock, return_value = ("angemeldet als: dummy_user", 1)),
|
||||
):
|
||||
assert await test_bot.is_logged_in(include_probe = False) is True
|
||||
|
||||
assert "Login detected via login detection selector 'user_info_secondary'" in caplog.text
|
||||
for forbidden in (".mr-medium", "#user-email", "mr-medium", "user-email"):
|
||||
assert forbidden not in caplog.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_logged_in_logs_generic_message_when_selector_group_does_not_match(
|
||||
self, test_bot:KleinanzeigenBot, caplog:pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Missing selector-group match should log the tried selectors when probe is disabled."""
|
||||
caplog.set_level("DEBUG")
|
||||
|
||||
with (
|
||||
caplog.at_level("DEBUG"),
|
||||
patch.object(test_bot, "web_text_first_available", side_effect = TimeoutError),
|
||||
):
|
||||
assert await test_bot.is_logged_in(include_probe = False) is False
|
||||
|
||||
assert any(
|
||||
record.message == "No login detected via configured login detection selectors (CLASS_NAME=mr-medium, ID=user-email)"
|
||||
for record in caplog.records
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_logged_in_logs_raw_selectors_when_probe_reports_logged_out(
|
||||
self, test_bot:KleinanzeigenBot, caplog:pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Probe-based final failure should include the tried raw selectors for debugging."""
|
||||
caplog.set_level("DEBUG")
|
||||
|
||||
with (
|
||||
caplog.at_level("DEBUG"),
|
||||
patch.object(test_bot, "web_text_first_available", side_effect = TimeoutError),
|
||||
patch.object(test_bot, "_auth_probe_login_state", new_callable = AsyncMock, return_value = LoginState.LOGGED_OUT),
|
||||
):
|
||||
assert await test_bot.is_logged_in() is False
|
||||
|
||||
assert any(
|
||||
record.message == (
|
||||
"No login detected - DOM login detection selectors (CLASS_NAME=mr-medium, ID=user-email) "
|
||||
"did not confirm login and server probe returned LOGGED_OUT"
|
||||
)
|
||||
for record in caplog.records
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_state_prefers_dom_over_auth_probe(self, test_bot:KleinanzeigenBot) -> None:
|
||||
with (
|
||||
|
||||
Reference in New Issue
Block a user