diff --git a/README.md b/README.md index b1e0c94..d3f6a24 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,7 @@ timeouts: page_load: 15.0 # Timeout for web_open page loads captcha_detection: 2.0 # Timeout for captcha iframe detection sms_verification: 4.0 # Timeout for SMS verification banners + email_verification: 4.0 # Timeout for email verification banners gdpr_prompt: 10.0 # Timeout when handling GDPR dialogs login_detection: 10.0 # Timeout for DOM-based login detection fallback (auth probe is tried first) publishing_result: 300.0 # Timeout for publishing status checks diff --git a/schemas/config.schema.json b/schemas/config.schema.json index b8d8d92..573dda4 100644 --- a/schemas/config.schema.json +++ b/schemas/config.schema.json @@ -518,6 +518,13 @@ "title": "Sms Verification", "type": "number" }, + "email_verification": { + "default": 4.0, + "description": "Timeout for eMail verification prompts.", + "minimum": 0.1, + "title": "Email Verification", + "type": "number" + }, "gdpr_prompt": { "default": 10.0, "description": "Timeout for GDPR/consent dialogs.", diff --git a/src/kleinanzeigen_bot/__init__.py b/src/kleinanzeigen_bot/__init__.py index 50a5cba..20ad5ad 100644 --- a/src/kleinanzeigen_bot/__init__.py +++ b/src/kleinanzeigen_bot/__init__.py @@ -863,11 +863,22 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904 LOG.warning("############################################") LOG.warning("# Device verification message detected. Please follow the instruction displayed in the Browser.") LOG.warning("############################################") - await ainput("Press ENTER when done...") + await ainput(_("Press ENTER when done...")) except TimeoutError: # No SMS verification prompt detected. pass + try: + email_timeout = self._timeout("email_verification") + await self.web_find(By.TEXT, "Um dein Konto zu schützen haben wir dir eine E-Mail geschickt", timeout = email_timeout) + LOG.warning("############################################") + LOG.warning("# Device verification message detected. Please follow the instruction displayed in the Browser.") + LOG.warning("############################################") + await ainput(_("Press ENTER when done...")) + except TimeoutError: + # No email verification prompt detected. + pass + try: LOG.info("Handling GDPR disclaimer...") gdpr_timeout = self._timeout("gdpr_prompt") diff --git a/src/kleinanzeigen_bot/model/config_model.py b/src/kleinanzeigen_bot/model/config_model.py index f7ba421..1881842 100644 --- a/src/kleinanzeigen_bot/model/config_model.py +++ b/src/kleinanzeigen_bot/model/config_model.py @@ -153,6 +153,7 @@ class TimeoutConfig(ContextualModel): page_load:float = Field(default = 15.0, ge = 1.0, description = "Page load timeout for web_open.") captcha_detection:float = Field(default = 2.0, ge = 0.1, description = "Timeout for captcha iframe detection.") sms_verification:float = Field(default = 4.0, ge = 0.1, description = "Timeout for SMS verification prompts.") + email_verification:float = Field(default = 4.0, ge = 0.1, description = "Timeout for email verification prompts.") gdpr_prompt:float = Field(default = 10.0, ge = 1.0, description = "Timeout for GDPR/consent dialogs.") login_detection:float = Field(default = 10.0, ge = 1.0, description = "Timeout for detecting existing login session via DOM elements.") publishing_result:float = Field(default = 300.0, ge = 10.0, description = "Timeout for publishing result checks.") @@ -218,7 +219,7 @@ class DiagnosticsConfig(ContextualModel): def _validate_glob_pattern(v:str) -> str: if not v.strip(): - raise ValueError("must be a non-empty, non-blank glob pattern") + raise ValueError(_("must be a non-empty, non-blank glob pattern")) return v diff --git a/src/kleinanzeigen_bot/resources/translations.de.yaml b/src/kleinanzeigen_bot/resources/translations.de.yaml index 392c404..9dde85c 100644 --- a/src/kleinanzeigen_bot/resources/translations.de.yaml +++ b/src/kleinanzeigen_bot/resources/translations.de.yaml @@ -603,6 +603,8 @@ kleinanzeigen_bot/model/config_model.py: "amount must be specified when auto_price_reduction is enabled": "amount muss angegeben werden, wenn auto_price_reduction aktiviert ist" "min_price must be specified when auto_price_reduction is enabled": "min_price muss angegeben werden, wenn auto_price_reduction aktiviert ist" "Percentage reduction amount must not exceed %s": "Prozentuale Reduktionsmenge darf %s nicht überschreiten" + _validate_glob_pattern: + "must be a non-empty, non-blank glob pattern": "muss ein nicht-leeres Glob-Muster sein" _validate_pause_requires_capture: "pause_on_login_detection_failure requires login_detection_capture to be enabled": "pause_on_login_detection_failure erfordert, dass login_detection_capture aktiviert ist" diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py index b66405a..1956bac 100644 --- a/tests/unit/test_init.py +++ b/tests/unit/test_init.py @@ -509,17 +509,21 @@ class TestKleinanzeigenBotAuthentication: # First login attempt: # 1. Captcha iframe found (in check_and_wait_for_captcha) # 2. Phone verification not found (in handle_after_login_logic) - # 3. GDPR banner not found (in handle_after_login_logic) + # 3. Email verification not found (in handle_after_login_logic) + # 4. GDPR banner not found (in handle_after_login_logic) # Second login attempt: - # 4. Captcha iframe found (in check_and_wait_for_captcha) - # 5. Phone verification not found (in handle_after_login_logic) - # 6. GDPR banner not found (in handle_after_login_logic) + # 5. Captcha iframe found (in check_and_wait_for_captcha) + # 6. Phone verification not found (in handle_after_login_logic) + # 7. Email verification not found (in handle_after_login_logic) + # 8. GDPR banner not found (in handle_after_login_logic) mock_find.side_effect = [ AsyncMock(), # Captcha iframe (first login) TimeoutError(), # Phone verification (first login) + TimeoutError(), # Email verification (first login) TimeoutError(), # GDPR banner (first login) AsyncMock(), # Captcha iframe (second login) TimeoutError(), # Phone verification (second login) + TimeoutError(), # Email verification (second login) TimeoutError(), # GDPR banner (second login) ] mock_ainput.return_value = "" @@ -529,7 +533,7 @@ class TestKleinanzeigenBotAuthentication: await test_bot.login() # Verify the complete flow - assert mock_find.call_count == 6 # Exactly 6 web_find calls + assert mock_find.call_count == 8 # Exactly 8 web_find calls assert mock_ainput.call_count == 2 # Two captcha prompts assert mock_input.call_count == 6 # Two login attempts with username, clear password, and set password assert mock_click.call_count == 2 # Two submit button clicks @@ -583,13 +587,13 @@ class TestKleinanzeigenBotAuthentication: patch("kleinanzeigen_bot.ainput", new_callable = AsyncMock) as mock_ainput, ): # Test case 1: No special handling needed - mock_find.side_effect = [TimeoutError(), TimeoutError()] # No phone verification, no GDPR + mock_find.side_effect = [TimeoutError(), TimeoutError(), TimeoutError()] # No phone verification, no email verification, no GDPR mock_click.return_value = AsyncMock() mock_ainput.return_value = "" await test_bot.handle_after_login_logic() - assert mock_find.call_count == 2 + assert mock_find.call_count == 3 assert mock_click.call_count == 0 assert mock_ainput.call_count == 0 @@ -597,11 +601,11 @@ class TestKleinanzeigenBotAuthentication: mock_find.reset_mock() mock_click.reset_mock() mock_ainput.reset_mock() - mock_find.side_effect = [AsyncMock(), TimeoutError()] # Phone verification found, no GDPR + mock_find.side_effect = [AsyncMock(), TimeoutError(), TimeoutError()] # Phone verification found, no email verification, no GDPR await test_bot.handle_after_login_logic() - assert mock_find.call_count == 2 + assert mock_find.call_count == 3 assert mock_click.call_count == 0 # No click needed, just wait for user assert mock_ainput.call_count == 1 # Wait for user to complete verification @@ -609,11 +613,11 @@ class TestKleinanzeigenBotAuthentication: mock_find.reset_mock() mock_click.reset_mock() mock_ainput.reset_mock() - mock_find.side_effect = [TimeoutError(), AsyncMock()] # No phone verification, GDPR found + mock_find.side_effect = [TimeoutError(), TimeoutError(), AsyncMock()] # No phone verification, no email verification, GDPR found await test_bot.handle_after_login_logic() - assert mock_find.call_count == 2 + assert mock_find.call_count == 3 assert mock_click.call_count == 2 # Click to accept GDPR and continue assert mock_ainput.call_count == 0