mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 10:31:50 +01:00
feat: introduce smoke test group and fail-fast test orchestration (#572)
This commit is contained in:
8
tests/__init__.py
Normal file
8
tests/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# SPDX-FileCopyrightText: © Jens Bergmann and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
|
||||
# This file makes the tests/ directory a Python package.
|
||||
# It is required so that direct imports like 'from tests.conftest import ...' work correctly,
|
||||
# and to avoid mypy errors about duplicate module names when using such imports.
|
||||
# Pytest does not require this for fixture discovery, but Python and mypy do for package-style imports.
|
||||
@@ -2,13 +2,14 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
import os
|
||||
from typing import Any, Final
|
||||
from typing import Any, Final, cast
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from kleinanzeigen_bot import KleinanzeigenBot
|
||||
from kleinanzeigen_bot.extract import AdExtractor
|
||||
from kleinanzeigen_bot.model.ad_model import Ad
|
||||
from kleinanzeigen_bot.model.config_model import Config
|
||||
from kleinanzeigen_bot.utils import loggers
|
||||
from kleinanzeigen_bot.utils.web_scraping_mixin import Browser
|
||||
@@ -181,3 +182,64 @@ def mock_web_text_responses() -> list[str]:
|
||||
@pytest.fixture(autouse = True)
|
||||
def silence_nodriver_logs() -> None:
|
||||
loggers.get_logger("nodriver").setLevel(loggers.WARNING)
|
||||
|
||||
|
||||
# --- Smoke test fakes and fixtures ---
|
||||
|
||||
class DummyBrowser:
|
||||
def __init__(self) -> None:
|
||||
self.page = DummyPage()
|
||||
self._process_pid = None # Use None to indicate no real process
|
||||
|
||||
def stop(self) -> None:
|
||||
pass # Dummy method to satisfy close_browser_session
|
||||
|
||||
|
||||
class DummyPage:
|
||||
def find_element(self, selector:str) -> "DummyElement":
|
||||
return DummyElement()
|
||||
|
||||
|
||||
class DummyElement:
|
||||
def click(self) -> None:
|
||||
pass
|
||||
|
||||
def type(self, text:str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class SmokeKleinanzeigenBot(KleinanzeigenBot):
|
||||
"""A test subclass that overrides async methods for smoke testing."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
# Use cast to satisfy type checker for browser attribute
|
||||
self.browser = cast(Browser, DummyBrowser())
|
||||
|
||||
def close_browser_session(self) -> None:
|
||||
# Override to avoid psutil.Process logic in tests
|
||||
self.page = None # pyright: ignore[reportAttributeAccessIssue]
|
||||
if self.browser:
|
||||
self.browser.stop()
|
||||
self.browser = None # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
async def login(self) -> None:
|
||||
return None
|
||||
|
||||
async def publish_ads(self, ad_cfgs:list[tuple[str, Ad, dict[str, Any]]]) -> None:
|
||||
return None
|
||||
|
||||
def load_ads(self, *, ignore_inactive:bool = True, exclude_ads_with_id:bool = True) -> list[tuple[str, Ad, dict[str, Any]]]:
|
||||
# Use cast to satisfy type checker for dummy Ad value
|
||||
return [("dummy_file", cast(Ad, None), {})]
|
||||
|
||||
def load_config(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def smoke_bot() -> SmokeKleinanzeigenBot:
|
||||
"""Fixture providing a ready-to-use smoke test bot instance."""
|
||||
bot = SmokeKleinanzeigenBot()
|
||||
bot.command = "publish"
|
||||
return bot
|
||||
|
||||
103
tests/smoke/test_smoke_health.py
Normal file
103
tests/smoke/test_smoke_health.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# SPDX-FileCopyrightText: © Jens Bergmann and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
"""
|
||||
Minimal smoke tests: post-deployment health checks for kleinanzeigen-bot.
|
||||
These tests verify that the most essential components are operational.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import subprocess # noqa: S404
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from kleinanzeigen_bot.model.config_model import Config
|
||||
from kleinanzeigen_bot.utils import i18n
|
||||
from tests.conftest import DummyBrowser, DummyPage, SmokeKleinanzeigenBot
|
||||
|
||||
|
||||
@pytest.mark.smoke
|
||||
def test_app_starts(smoke_bot:SmokeKleinanzeigenBot) -> None:
|
||||
"""Smoke: Bot can be instantiated and started without error."""
|
||||
assert smoke_bot is not None
|
||||
# Optionally call a minimal method if available
|
||||
assert hasattr(smoke_bot, "run") or hasattr(smoke_bot, "login")
|
||||
|
||||
|
||||
@pytest.mark.smoke
|
||||
def test_config_loads() -> None:
|
||||
"""Smoke: Minimal config loads successfully."""
|
||||
minimal_cfg = {
|
||||
"ad_defaults": {"contact": {"name": "dummy", "zipcode": "12345"}},
|
||||
"login": {"username": "dummy", "password": "dummy"},
|
||||
"publishing": {"delete_old_ads": "BEFORE_PUBLISH", "delete_old_ads_by_title": False},
|
||||
}
|
||||
config = Config.model_validate(minimal_cfg)
|
||||
assert config.login.username == "dummy"
|
||||
assert config.login.password == "dummy" # noqa: S105
|
||||
|
||||
|
||||
@pytest.mark.smoke
|
||||
def test_logger_initializes(tmp_path:Path, caplog:pytest.LogCaptureFixture) -> None:
|
||||
"""Smoke: Logger can be initialized and used, robust to pytest log capture."""
|
||||
log_path = tmp_path / "smoke_test.log"
|
||||
logger_name = "smoke_test_logger_unique"
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.propagate = False
|
||||
# Remove all handlers to start clean
|
||||
for h in list(logger.handlers):
|
||||
logger.removeHandler(h)
|
||||
# Create and attach a file handler
|
||||
handle = logging.FileHandler(str(log_path), encoding = "utf-8")
|
||||
handle.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter("%(levelname)s:%(name)s:%(message)s")
|
||||
handle.setFormatter(formatter)
|
||||
logger.addHandler(handle)
|
||||
# Log a message
|
||||
logger.info("Smoke test log message")
|
||||
# Flush and close the handler
|
||||
handle.flush()
|
||||
handle.close()
|
||||
# Remove the handler from the logger
|
||||
logger.removeHandler(handle)
|
||||
assert log_path.exists()
|
||||
with open(log_path, "r", encoding = "utf-8") as f:
|
||||
contents = f.read()
|
||||
assert "Smoke test log message" in contents
|
||||
|
||||
|
||||
@pytest.mark.smoke
|
||||
def test_translation_system_healthy() -> None:
|
||||
"""Smoke: Translation system loads and retrieves a known key."""
|
||||
# Use a known string that should exist in translations (fallback to identity)
|
||||
en = i18n.translate("Login", None)
|
||||
assert isinstance(en, str)
|
||||
assert len(en) > 0
|
||||
# Switch to German and test
|
||||
i18n.set_current_locale(i18n.Locale("de"))
|
||||
de = i18n.translate("Login", None)
|
||||
assert isinstance(de, str)
|
||||
assert len(de) > 0
|
||||
# Reset locale
|
||||
i18n.set_current_locale(i18n.Locale("en"))
|
||||
|
||||
|
||||
@pytest.mark.smoke
|
||||
def test_dummy_browser_session() -> None:
|
||||
"""Smoke: Dummy browser session can be created and closed."""
|
||||
browser = DummyBrowser()
|
||||
page = browser.page
|
||||
assert isinstance(page, DummyPage)
|
||||
browser.stop() # Should not raise
|
||||
|
||||
|
||||
@pytest.mark.smoke
|
||||
def test_cli_entrypoint_help_runs() -> None:
|
||||
"""Smoke: CLI entry point runs with --help and exits cleanly (subprocess)."""
|
||||
cli_module = "kleinanzeigen_bot.__main__"
|
||||
result = subprocess.run([sys.executable, "-m", cli_module, "--help"], check = False, capture_output = True, text = True) # noqa: S603
|
||||
assert result.returncode in {0, 1}, f"CLI exited with unexpected code: {result.returncode}\nstdout: {result.stdout}\nstderr: {result.stderr}"
|
||||
assert "Usage" in result.stdout or "usage" in result.stdout or "help" in result.stdout.lower(), f"No help text in CLI output: {result.stdout}"
|
||||
Reference in New Issue
Block a user