mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
feat: capture publish failure diagnostics with screenshot and logs (#802)
This commit is contained in:
@@ -87,7 +87,7 @@ def test_timeout_config_resolve_falls_back_to_default() -> None:
|
||||
def test_diagnostics_pause_requires_capture_validation() -> None:
|
||||
"""
|
||||
Unit: DiagnosticsConfig validator ensures pause_on_login_detection_failure
|
||||
requires login_detection_capture to be enabled.
|
||||
requires capture_on.login_detection to be enabled.
|
||||
"""
|
||||
minimal_cfg = {
|
||||
"ad_defaults": {"contact": {"name": "dummy", "zipcode": "12345"}},
|
||||
@@ -95,12 +95,98 @@ def test_diagnostics_pause_requires_capture_validation() -> None:
|
||||
"publishing": {"delete_old_ads": "BEFORE_PUBLISH", "delete_old_ads_by_title": False},
|
||||
}
|
||||
|
||||
valid_cfg = {**minimal_cfg, "diagnostics": {"login_detection_capture": True, "pause_on_login_detection_failure": True}}
|
||||
valid_cfg = {**minimal_cfg, "diagnostics": {"capture_on": {"login_detection": True}, "pause_on_login_detection_failure": True}}
|
||||
config = Config.model_validate(valid_cfg)
|
||||
assert config.diagnostics is not None
|
||||
assert config.diagnostics.pause_on_login_detection_failure is True
|
||||
assert config.diagnostics.login_detection_capture is True
|
||||
assert config.diagnostics.capture_on.login_detection is True
|
||||
|
||||
invalid_cfg = {**minimal_cfg, "diagnostics": {"login_detection_capture": False, "pause_on_login_detection_failure": True}}
|
||||
with pytest.raises(ValueError, match = "pause_on_login_detection_failure requires login_detection_capture to be enabled"):
|
||||
invalid_cfg = {**minimal_cfg, "diagnostics": {"capture_on": {"login_detection": False}, "pause_on_login_detection_failure": True}}
|
||||
with pytest.raises(ValueError, match = "pause_on_login_detection_failure requires capture_on.login_detection to be enabled"):
|
||||
Config.model_validate(invalid_cfg)
|
||||
|
||||
|
||||
def test_diagnostics_legacy_login_detection_capture_migration_when_capture_on_exists() -> None:
|
||||
"""
|
||||
Unit: Test that legacy login_detection_capture is removed but doesn't overwrite explicit capture_on.login_detection.
|
||||
"""
|
||||
minimal_cfg = {
|
||||
"ad_defaults": {"contact": {"name": "dummy", "zipcode": "12345"}},
|
||||
"login": {"username": "dummy", "password": "dummy"}, # noqa: S105
|
||||
}
|
||||
|
||||
# When capture_on.login_detection is explicitly set to False, legacy True should be ignored
|
||||
cfg_with_explicit = {
|
||||
**minimal_cfg,
|
||||
"diagnostics": {
|
||||
"login_detection_capture": True, # legacy key
|
||||
"capture_on": {"login_detection": False}, # explicit new key set to False
|
||||
},
|
||||
}
|
||||
config = Config.model_validate(cfg_with_explicit)
|
||||
assert config.diagnostics is not None
|
||||
assert config.diagnostics.capture_on.login_detection is False # explicit value preserved
|
||||
|
||||
|
||||
def test_diagnostics_legacy_publish_error_capture_migration_when_capture_on_exists() -> None:
|
||||
"""
|
||||
Unit: Test that legacy publish_error_capture is removed but doesn't overwrite explicit capture_on.publish.
|
||||
"""
|
||||
minimal_cfg = {
|
||||
"ad_defaults": {"contact": {"name": "dummy", "zipcode": "12345"}},
|
||||
"login": {"username": "dummy", "password": "dummy"}, # noqa: S105
|
||||
}
|
||||
|
||||
# When capture_on.publish is explicitly set to False, legacy True should be ignored
|
||||
cfg_with_explicit = {
|
||||
**minimal_cfg,
|
||||
"diagnostics": {
|
||||
"publish_error_capture": True, # legacy key
|
||||
"capture_on": {"publish": False}, # explicit new key set to False
|
||||
},
|
||||
}
|
||||
config = Config.model_validate(cfg_with_explicit)
|
||||
assert config.diagnostics is not None
|
||||
assert config.diagnostics.capture_on.publish is False # explicit value preserved
|
||||
|
||||
|
||||
def test_diagnostics_legacy_login_detection_capture_migration_when_capture_on_is_none() -> None:
|
||||
"""
|
||||
Unit: Test that legacy login_detection_capture is migrated when capture_on is None.
|
||||
"""
|
||||
minimal_cfg = {
|
||||
"ad_defaults": {"contact": {"name": "dummy", "zipcode": "12345"}},
|
||||
"login": {"username": "dummy", "password": "dummy"}, # noqa: S105
|
||||
}
|
||||
|
||||
cfg_with_null_capture_on = {
|
||||
**minimal_cfg,
|
||||
"diagnostics": {
|
||||
"login_detection_capture": True, # legacy key
|
||||
"capture_on": None, # capture_on is explicitly None
|
||||
},
|
||||
}
|
||||
config = Config.model_validate(cfg_with_null_capture_on)
|
||||
assert config.diagnostics is not None
|
||||
assert config.diagnostics.capture_on.login_detection is True # legacy value migrated
|
||||
|
||||
|
||||
def test_diagnostics_legacy_publish_error_capture_migration_when_capture_on_is_none() -> None:
|
||||
"""
|
||||
Unit: Test that legacy publish_error_capture is migrated when capture_on is None.
|
||||
"""
|
||||
minimal_cfg = {
|
||||
"ad_defaults": {"contact": {"name": "dummy", "zipcode": "12345"}},
|
||||
"login": {"username": "dummy", "password": "dummy"}, # noqa: S105
|
||||
}
|
||||
|
||||
cfg_with_null_capture_on = {
|
||||
**minimal_cfg,
|
||||
"diagnostics": {
|
||||
"publish_error_capture": True, # legacy key
|
||||
"capture_on": None, # capture_on is explicitly None
|
||||
},
|
||||
}
|
||||
config = Config.model_validate(cfg_with_null_capture_on)
|
||||
assert config.diagnostics is not None
|
||||
assert config.diagnostics.capture_on.publish is True # legacy value migrated
|
||||
|
||||
224
tests/unit/test_diagnostics.py
Normal file
224
tests/unit/test_diagnostics.py
Normal file
@@ -0,0 +1,224 @@
|
||||
# SPDX-FileCopyrightText: © 2025 Sebastian Thomschke and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from kleinanzeigen_bot.utils import diagnostics as diagnostics_module
|
||||
from kleinanzeigen_bot.utils.diagnostics import capture_diagnostics
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDiagnosticsCapture:
|
||||
"""Tests for diagnostics capture functionality."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_diagnostics_creates_output_dir(self, tmp_path:Path) -> None:
|
||||
"""Test that capture_diagnostics creates output directory."""
|
||||
mock_page = AsyncMock()
|
||||
|
||||
output_dir = tmp_path / "diagnostics"
|
||||
_ = await capture_diagnostics(
|
||||
output_dir = output_dir,
|
||||
base_prefix = "test",
|
||||
page = mock_page,
|
||||
)
|
||||
|
||||
# Verify directory was created
|
||||
assert output_dir.exists()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_diagnostics_creates_screenshot(self, tmp_path:Path) -> None:
|
||||
"""Test that capture_diagnostics creates screenshot file."""
|
||||
mock_page = AsyncMock()
|
||||
mock_page.save_screenshot = AsyncMock()
|
||||
|
||||
output_dir = tmp_path / "diagnostics"
|
||||
result = await capture_diagnostics(
|
||||
output_dir = output_dir,
|
||||
base_prefix = "test",
|
||||
page = mock_page,
|
||||
)
|
||||
|
||||
# Verify screenshot file was created and page method was called
|
||||
assert len(result.saved_artifacts) == 1
|
||||
assert result.saved_artifacts[0].suffix == ".png"
|
||||
mock_page.save_screenshot.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_diagnostics_creates_html(self, tmp_path:Path) -> None:
|
||||
"""Test that capture_diagnostics creates HTML file."""
|
||||
mock_page = AsyncMock()
|
||||
mock_page.get_content = AsyncMock(return_value = "<html></html>")
|
||||
|
||||
output_dir = tmp_path / "diagnostics"
|
||||
result = await capture_diagnostics(
|
||||
output_dir = output_dir,
|
||||
base_prefix = "test",
|
||||
page = mock_page,
|
||||
)
|
||||
|
||||
# Verify HTML file was created along with screenshot
|
||||
assert len(result.saved_artifacts) == 2
|
||||
assert any(a.suffix == ".html" for a in result.saved_artifacts)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_diagnostics_creates_json(self, tmp_path:Path) -> None:
|
||||
"""Test that capture_diagnostics creates JSON file."""
|
||||
mock_page = AsyncMock()
|
||||
mock_page.get_content = AsyncMock(return_value = "<html></html>")
|
||||
|
||||
output_dir = tmp_path / "diagnostics"
|
||||
result = await capture_diagnostics(
|
||||
output_dir = output_dir,
|
||||
base_prefix = "test",
|
||||
page = mock_page,
|
||||
json_payload = {"test": "data"},
|
||||
)
|
||||
|
||||
# Verify JSON file was created along with HTML and screenshot
|
||||
assert len(result.saved_artifacts) == 3
|
||||
assert any(a.suffix == ".json" for a in result.saved_artifacts)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_diagnostics_copies_log_file(self, tmp_path:Path) -> None:
|
||||
"""Test that capture_diagnostics copies log file when enabled."""
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("test log content")
|
||||
|
||||
output_dir = tmp_path / "diagnostics"
|
||||
result = await capture_diagnostics(
|
||||
output_dir = output_dir,
|
||||
base_prefix = "test",
|
||||
page = None, # No page to avoid screenshot
|
||||
log_file_path = str(log_file),
|
||||
copy_log = True,
|
||||
)
|
||||
|
||||
# Verify log was copied
|
||||
assert len(result.saved_artifacts) == 1
|
||||
assert result.saved_artifacts[0].suffix == ".log"
|
||||
|
||||
def test_copy_log_sync_returns_false_when_file_not_found(self, tmp_path:Path) -> None:
|
||||
"""Test _copy_log_sync returns False when log file does not exist."""
|
||||
non_existent_log = tmp_path / "non_existent.log"
|
||||
log_path = tmp_path / "output.log"
|
||||
|
||||
result = diagnostics_module._copy_log_sync(str(non_existent_log), log_path)
|
||||
|
||||
assert result is False
|
||||
assert not log_path.exists()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_diagnostics_handles_screenshot_exception(self, tmp_path:Path, caplog:pytest.LogCaptureFixture) -> None:
|
||||
"""Test that capture_diagnostics handles screenshot capture exceptions gracefully."""
|
||||
mock_page = AsyncMock()
|
||||
mock_page.save_screenshot = AsyncMock(side_effect = Exception("Screenshot failed"))
|
||||
|
||||
output_dir = tmp_path / "diagnostics"
|
||||
result = await capture_diagnostics(
|
||||
output_dir = output_dir,
|
||||
base_prefix = "test",
|
||||
page = mock_page,
|
||||
)
|
||||
|
||||
# Verify no artifacts were saved due to exception
|
||||
assert len(result.saved_artifacts) == 0
|
||||
assert "Diagnostics screenshot capture failed" in caplog.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_diagnostics_handles_json_exception(self, tmp_path:Path, caplog:pytest.LogCaptureFixture, monkeypatch:pytest.MonkeyPatch) -> None:
|
||||
"""Test that capture_diagnostics handles JSON write exceptions gracefully."""
|
||||
mock_page = AsyncMock()
|
||||
mock_page.get_content = AsyncMock(return_value = "<html></html>")
|
||||
|
||||
output_dir = tmp_path / "diagnostics"
|
||||
|
||||
# Mock _write_json_sync to raise an exception
|
||||
monkeypatch.setattr(diagnostics_module, "_write_json_sync", MagicMock(side_effect = Exception("JSON write failed")))
|
||||
|
||||
result = await capture_diagnostics(
|
||||
output_dir = output_dir,
|
||||
base_prefix = "test",
|
||||
page = mock_page,
|
||||
json_payload = {"test": "data"},
|
||||
)
|
||||
|
||||
# Verify screenshot and HTML were saved, but JSON failed
|
||||
assert len(result.saved_artifacts) == 2
|
||||
assert any(a.suffix == ".png" for a in result.saved_artifacts)
|
||||
assert any(a.suffix == ".html" for a in result.saved_artifacts)
|
||||
assert not any(a.suffix == ".json" for a in result.saved_artifacts)
|
||||
assert "Diagnostics JSON capture failed" in caplog.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_diagnostics_handles_log_copy_exception(
|
||||
self, tmp_path:Path, caplog:pytest.LogCaptureFixture, monkeypatch:pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test that capture_diagnostics handles log copy exceptions gracefully."""
|
||||
# Create a log file
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("test log content")
|
||||
|
||||
output_dir = tmp_path / "diagnostics"
|
||||
|
||||
# Mock _copy_log_sync to raise an exception
|
||||
original_copy_log = diagnostics_module._copy_log_sync
|
||||
monkeypatch.setattr(diagnostics_module, "_copy_log_sync", MagicMock(side_effect = Exception("Copy failed")))
|
||||
|
||||
try:
|
||||
result = await capture_diagnostics(
|
||||
output_dir = output_dir,
|
||||
base_prefix = "test",
|
||||
page = None,
|
||||
log_file_path = str(log_file),
|
||||
copy_log = True,
|
||||
)
|
||||
|
||||
# Verify no artifacts were saved due to exception
|
||||
assert len(result.saved_artifacts) == 0
|
||||
assert "Diagnostics log copy failed" in caplog.text
|
||||
finally:
|
||||
monkeypatch.setattr(diagnostics_module, "_copy_log_sync", original_copy_log)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_diagnostics_logs_warning_when_all_captures_fail(
|
||||
self, tmp_path:Path, caplog:pytest.LogCaptureFixture, monkeypatch:pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Test warning is logged when capture is requested but all fail."""
|
||||
mock_page = AsyncMock()
|
||||
mock_page.save_screenshot = AsyncMock(side_effect = Exception("Screenshot failed"))
|
||||
mock_page.get_content = AsyncMock(side_effect = Exception("HTML failed"))
|
||||
|
||||
# Mock JSON write to also fail
|
||||
monkeypatch.setattr(diagnostics_module, "_write_json_sync", MagicMock(side_effect = Exception("JSON write failed")))
|
||||
|
||||
output_dir = tmp_path / "diagnostics"
|
||||
result = await capture_diagnostics(
|
||||
output_dir = output_dir,
|
||||
base_prefix = "test",
|
||||
page = mock_page,
|
||||
json_payload = {"test": "data"},
|
||||
)
|
||||
|
||||
# Verify no artifacts were saved
|
||||
assert len(result.saved_artifacts) == 0
|
||||
assert "Diagnostics capture attempted but no artifacts were saved" in caplog.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_diagnostics_logs_debug_when_no_capture_requested(self, tmp_path:Path, caplog:pytest.LogCaptureFixture) -> None:
|
||||
"""Test debug is logged when no diagnostics capture is requested."""
|
||||
output_dir = tmp_path / "diagnostics"
|
||||
|
||||
with caplog.at_level("DEBUG"):
|
||||
_ = await capture_diagnostics(
|
||||
output_dir = output_dir,
|
||||
base_prefix = "test",
|
||||
page = None,
|
||||
json_payload = None,
|
||||
copy_log = False,
|
||||
)
|
||||
|
||||
assert "No diagnostics capture requested" in caplog.text
|
||||
@@ -1,7 +1,7 @@
|
||||
# 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 copy, io, json, logging, os, tempfile # isort: skip
|
||||
import copy, fnmatch, io, json, logging, os, tempfile # isort: skip
|
||||
from collections.abc import Generator
|
||||
from contextlib import redirect_stdout
|
||||
from datetime import timedelta
|
||||
@@ -12,7 +12,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from kleinanzeigen_bot import LOG, AdUpdateStrategy, KleinanzeigenBot, LoginState, misc
|
||||
from kleinanzeigen_bot import LOG, PUBLISH_MAX_RETRIES, AdUpdateStrategy, KleinanzeigenBot, LoginState, misc
|
||||
from kleinanzeigen_bot._version import __version__
|
||||
from kleinanzeigen_bot.model.ad_model import Ad
|
||||
from kleinanzeigen_bot.model.config_model import AdDefaults, Config, DiagnosticsConfig, PublishingConfig
|
||||
@@ -388,7 +388,7 @@ class TestKleinanzeigenBotAuthentication:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_state_unknown_captures_diagnostics_when_enabled(self, test_bot:KleinanzeigenBot, tmp_path:Path) -> None:
|
||||
test_bot.config.diagnostics = DiagnosticsConfig.model_validate({"login_detection_capture": True, "output_dir": str(tmp_path)})
|
||||
test_bot.config.diagnostics = DiagnosticsConfig.model_validate({"capture_on": {"login_detection": True}, "output_dir": str(tmp_path)})
|
||||
|
||||
page = MagicMock()
|
||||
page.save_screenshot = AsyncMock()
|
||||
@@ -406,7 +406,7 @@ class TestKleinanzeigenBotAuthentication:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_state_unknown_does_not_capture_diagnostics_when_disabled(self, test_bot:KleinanzeigenBot, tmp_path:Path) -> None:
|
||||
test_bot.config.diagnostics = DiagnosticsConfig.model_validate({"login_detection_capture": False, "output_dir": str(tmp_path)})
|
||||
test_bot.config.diagnostics = DiagnosticsConfig.model_validate({"capture_on": {"login_detection": False}, "output_dir": str(tmp_path)})
|
||||
|
||||
page = MagicMock()
|
||||
page.save_screenshot = AsyncMock()
|
||||
@@ -425,7 +425,7 @@ class TestKleinanzeigenBotAuthentication:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_state_unknown_pauses_for_inspection_when_enabled_and_interactive(self, test_bot:KleinanzeigenBot, tmp_path:Path) -> None:
|
||||
test_bot.config.diagnostics = DiagnosticsConfig.model_validate(
|
||||
{"login_detection_capture": True, "pause_on_login_detection_failure": True, "output_dir": str(tmp_path)}
|
||||
{"capture_on": {"login_detection": True}, "pause_on_login_detection_failure": True, "output_dir": str(tmp_path)}
|
||||
)
|
||||
|
||||
page = MagicMock()
|
||||
@@ -453,7 +453,7 @@ class TestKleinanzeigenBotAuthentication:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_login_state_unknown_does_not_pause_when_non_interactive(self, test_bot:KleinanzeigenBot, tmp_path:Path) -> None:
|
||||
test_bot.config.diagnostics = DiagnosticsConfig.model_validate(
|
||||
{"login_detection_capture": True, "pause_on_login_detection_failure": True, "output_dir": str(tmp_path)}
|
||||
{"capture_on": {"login_detection": True}, "pause_on_login_detection_failure": True, "output_dir": str(tmp_path)}
|
||||
)
|
||||
|
||||
page = MagicMock()
|
||||
@@ -624,6 +624,139 @@ class TestKleinanzeigenBotAuthentication:
|
||||
assert mock_ainput.call_count == 0
|
||||
|
||||
|
||||
class TestKleinanzeigenBotDiagnostics:
|
||||
@pytest.fixture
|
||||
def diagnostics_ad_config(self) -> dict[str, Any]:
|
||||
return {
|
||||
"active": True,
|
||||
"type": "OFFER",
|
||||
"title": "Test ad title",
|
||||
"description": "Test description",
|
||||
"category": "161/176/sonstige",
|
||||
"price_type": "NEGOTIABLE",
|
||||
"shipping_type": "PICKUP",
|
||||
"sell_directly": False,
|
||||
"contact": {
|
||||
"name": "Tester",
|
||||
"zipcode": "12345",
|
||||
},
|
||||
"republication_interval": 7,
|
||||
}
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_ads_captures_diagnostics_on_failures(
|
||||
self,
|
||||
test_bot:KleinanzeigenBot,
|
||||
tmp_path:Path,
|
||||
diagnostics_ad_config:dict[str, Any],
|
||||
) -> None:
|
||||
"""Ensure publish failures capture diagnostics artifacts."""
|
||||
log_file_path = tmp_path / "test.log"
|
||||
log_file_path.write_text("Test log content\n", encoding = "utf-8")
|
||||
test_bot.log_file_path = str(log_file_path)
|
||||
|
||||
test_bot.config.diagnostics = DiagnosticsConfig.model_validate({"capture_on": {"publish": True}, "output_dir": str(tmp_path)})
|
||||
|
||||
page = MagicMock()
|
||||
page.save_screenshot = AsyncMock()
|
||||
page.get_content = AsyncMock(return_value = "<html></html>")
|
||||
page.sleep = AsyncMock()
|
||||
page.url = "https://example.com/fail"
|
||||
test_bot.page = page
|
||||
|
||||
ad_cfg = Ad.model_validate(diagnostics_ad_config)
|
||||
ad_cfg_orig = copy.deepcopy(diagnostics_ad_config)
|
||||
ad_file = str(tmp_path / "ad_000001_Test.yml")
|
||||
|
||||
with (
|
||||
patch.object(test_bot, "web_request", new_callable = AsyncMock, return_value = {"content": json.dumps({"ads": []})}),
|
||||
patch.object(test_bot, "publish_ad", new_callable = AsyncMock, side_effect = TimeoutError("boom")),
|
||||
):
|
||||
await test_bot.publish_ads([(ad_file, ad_cfg, ad_cfg_orig)])
|
||||
|
||||
expected_retries = PUBLISH_MAX_RETRIES
|
||||
assert page.save_screenshot.await_count == expected_retries
|
||||
assert page.get_content.await_count == expected_retries
|
||||
entries = os.listdir(tmp_path)
|
||||
html_files = [name for name in entries if fnmatch.fnmatch(name, "publish_error_*_attempt*_ad_000001_Test.html")]
|
||||
json_files = [name for name in entries if fnmatch.fnmatch(name, "publish_error_*_attempt*_ad_000001_Test.json")]
|
||||
assert len(html_files) == expected_retries
|
||||
assert len(json_files) == expected_retries
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_ads_captures_log_copy_when_enabled(
|
||||
self,
|
||||
test_bot:KleinanzeigenBot,
|
||||
tmp_path:Path,
|
||||
diagnostics_ad_config:dict[str, Any],
|
||||
) -> None:
|
||||
"""Ensure publish failures copy log file when capture_log_copy is enabled."""
|
||||
log_file_path = tmp_path / "test.log"
|
||||
log_file_path.write_text("Test log content\n", encoding = "utf-8")
|
||||
test_bot.log_file_path = str(log_file_path)
|
||||
|
||||
test_bot.config.diagnostics = DiagnosticsConfig.model_validate({"capture_on": {"publish": True}, "capture_log_copy": True, "output_dir": str(tmp_path)})
|
||||
|
||||
page = MagicMock()
|
||||
page.save_screenshot = AsyncMock()
|
||||
page.get_content = AsyncMock(return_value = "<html></html>")
|
||||
page.sleep = AsyncMock()
|
||||
page.url = "https://example.com/fail"
|
||||
test_bot.page = page
|
||||
|
||||
ad_cfg = Ad.model_validate(diagnostics_ad_config)
|
||||
ad_cfg_orig = copy.deepcopy(diagnostics_ad_config)
|
||||
ad_file = str(tmp_path / "ad_000001_Test.yml")
|
||||
|
||||
with (
|
||||
patch.object(test_bot, "web_request", new_callable = AsyncMock, return_value = {"content": json.dumps({"ads": []})}),
|
||||
patch.object(test_bot, "publish_ad", new_callable = AsyncMock, side_effect = TimeoutError("boom")),
|
||||
):
|
||||
await test_bot.publish_ads([(ad_file, ad_cfg, ad_cfg_orig)])
|
||||
|
||||
entries = os.listdir(tmp_path)
|
||||
log_files = [name for name in entries if fnmatch.fnmatch(name, "publish_error_*_attempt*_ad_000001_Test.log")]
|
||||
assert len(log_files) == PUBLISH_MAX_RETRIES
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_ads_does_not_capture_diagnostics_when_disabled(
|
||||
self,
|
||||
test_bot:KleinanzeigenBot,
|
||||
tmp_path:Path,
|
||||
diagnostics_ad_config:dict[str, Any],
|
||||
) -> None:
|
||||
"""Ensure diagnostics are not captured when disabled."""
|
||||
test_bot.config.diagnostics = DiagnosticsConfig.model_validate({"capture_on": {"publish": False}, "output_dir": str(tmp_path)})
|
||||
|
||||
page = MagicMock()
|
||||
page.save_screenshot = AsyncMock()
|
||||
page.get_content = AsyncMock(return_value = "<html></html>")
|
||||
page.sleep = AsyncMock()
|
||||
page.url = "https://example.com/fail"
|
||||
test_bot.page = page
|
||||
|
||||
ad_cfg = Ad.model_validate(diagnostics_ad_config)
|
||||
ad_cfg_orig = copy.deepcopy(diagnostics_ad_config)
|
||||
ad_file = str(tmp_path / "ad_000001_Test.yml")
|
||||
|
||||
with (
|
||||
patch.object(test_bot, "web_request", new_callable = AsyncMock, return_value = {"content": json.dumps({"ads": []})}),
|
||||
patch.object(test_bot, "publish_ad", new_callable = AsyncMock, side_effect = TimeoutError("boom")),
|
||||
):
|
||||
await test_bot.publish_ads([(ad_file, ad_cfg, ad_cfg_orig)])
|
||||
|
||||
page.save_screenshot.assert_not_called()
|
||||
page.get_content.assert_not_called()
|
||||
entries = os.listdir(tmp_path)
|
||||
html_files = [name for name in entries if fnmatch.fnmatch(name, "publish_error_*_attempt*_ad_000001_Test.html")]
|
||||
json_files = [name for name in entries if fnmatch.fnmatch(name, "publish_error_*_attempt*_ad_000001_Test.json")]
|
||||
assert not html_files
|
||||
assert not json_files
|
||||
|
||||
|
||||
class TestKleinanzeigenBotLocalization:
|
||||
"""Tests for localization and help text."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user