Files
kleinanzeigen-bot/tests/unit/test_error_handlers.py
Jens Bergmann 50656ad7e2 feat: Improve test coverage (#515)
* test: implement comprehensive test coverage improvements

This commit improves test coverage across multiple modules, adding unit tests
for core functionality.

Key improvements:

1. WebScrapingMixin:
   - Add comprehensive async error handling tests
   - Add session management tests (browser crash recovery, session expiration)
   - Add element interaction tests (custom wait conditions, timeouts)
   - Add browser configuration tests (extensions, preferences)
   - Add robust awaitable mocking infrastructure
   - Rename integration test file to avoid naming conflicts

2. Error Handlers:
   - Add tests for error message formatting
   - Add tests for error recovery scenarios
   - Add tests for error logging functionality

3. Network Utilities:
   - Add tests for port checking functionality
   - Add tests for network error handling
   - Add tests for connection management

4. Pydantic Models:
   - Add tests for validation cases
   - Add tests for error handling
   - Add tests for complex validation scenarios

Technical details:
- Use TrulyAwaitableMockPage for proper async testing
- Add comprehensive mocking for browser and page objects
- Add proper cleanup in session management tests
- Add browser-specific configuration tests (Chrome/Edge)
- Add proper type hints and docstrings

Files changed:
- Renamed: tests/integration/test_web_scraping_mixin.py → tests/integration/test_web_scraping_mixin_integration.py
- Added: tests/unit/test_error_handlers.py
- Added: tests/unit/test_net.py
- Added: tests/unit/test_pydantics.py
- Added: tests/unit/test_web_scraping_mixin.py

* test: enhance test coverage with additional edge cases and scenarios

This commit extends the test coverage improvements with additional test cases
and edge case handling, focusing on browser configuration, error handling, and
file utilities.

Key improvements:

1. WebScrapingMixin:
   - Add comprehensive browser binary location detection tests
   - Add cross-platform browser path detection (Linux, macOS, Windows)
   - Add browser profile configuration tests
   - Add session state persistence tests
   - Add external process termination handling
   - Add session creation error cleanup tests
   - Improve browser argument configuration tests
   - Add extension loading validation tests

2. Error Handlers:
   - Add debug mode error handling tests
   - Add specific error type tests (AttributeError, ImportError, NameError, TypeError)
   - Improve error message formatting tests
   - Add traceback inclusion verification

3. Pydantic Models:
   - Add comprehensive validation error message tests
   - Add tests for various error codes and contexts
   - Add tests for pluralization in error messages
   - Add tests for empty error list handling
   - Add tests for context handling in validation errors

4. File Utilities:
   - Add comprehensive path resolution tests
   - Add tests for file and directory reference handling
   - Add tests for special path cases
   - Add tests for nonexistent path handling
   - Add tests for absolute and relative path conversion

Technical details:
- Add proper type casting for test fixtures
- Improve test isolation and cleanup
- Add platform-specific browser path detection
- Add proper error context handling
- Add comprehensive error message formatting tests
- Add proper cleanup in session management tests
- Add browser-specific configuration tests
- Add proper path normalization and resolution tests

* fix(test): handle Linux browser paths in web_scraping_mixin test

Update mock_exists to properly detect Linux browser binaries in test_browser_profile_configuration, fixing the "Installed browser could not be detected" error.

* fix(test): handle Windows browser paths in web_scraping_mixin test

Add Windows browser paths to mock_exists function to properly detect browser binaries on Windows platform, fixing the "Specified browser binary does not exist" error.
2025-05-18 19:02:59 +02:00

170 lines
7.2 KiB
Python

# SPDX-FileCopyrightText: © Jens Bergmann and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
"""Tests for the error handlers module.
This module contains tests for the error handling functionality of the kleinanzeigen-bot application.
It tests both the exception handler and signal handler functionality.
"""
import sys
from collections.abc import Generator
from unittest.mock import MagicMock, patch
import pytest
from pydantic import BaseModel, ValidationError
from kleinanzeigen_bot.utils.error_handlers import on_exception, on_sigint
# --------------------------------------------------------------------------- #
# Test fixtures
# --------------------------------------------------------------------------- #
@pytest.fixture
def mock_logger() -> Generator[MagicMock, None, None]:
"""Fixture to mock the logger."""
with patch("kleinanzeigen_bot.utils.error_handlers.LOG") as mock_log:
yield mock_log
@pytest.fixture
def mock_sys_exit() -> Generator[MagicMock, None, None]:
"""Fixture to mock sys.exit to prevent actual program termination."""
with patch("sys.exit") as mock_exit:
yield mock_exit
# --------------------------------------------------------------------------- #
# Test cases
# --------------------------------------------------------------------------- #
class TestExceptionHandler:
"""Test cases for the exception handler."""
def test_keyboard_interrupt(self, mock_logger:MagicMock, mock_sys_exit:MagicMock) -> None:
"""Test that KeyboardInterrupt is handled by the system excepthook."""
with patch("sys.__excepthook__") as mock_excepthook:
on_exception(KeyboardInterrupt, KeyboardInterrupt(), None)
mock_excepthook.assert_called_once()
mock_sys_exit.assert_called_once_with(1)
def test_validation_error(self, mock_logger:MagicMock, mock_sys_exit:MagicMock) -> None:
"""Test that ValidationError is formatted and logged."""
class TestModel(BaseModel):
field:int
try:
TestModel(field = "not an int") # type: ignore[arg-type]
except ValidationError as error:
on_exception(ValidationError, error, None)
mock_logger.error.assert_called_once()
mock_sys_exit.assert_called_once_with(1)
def test_assertion_error(self, mock_logger:MagicMock, mock_sys_exit:MagicMock) -> None:
"""Test that AssertionError is logged directly."""
error = AssertionError("Test error")
on_exception(AssertionError, error, None)
# Accept both with and without trailing newline
logged = mock_logger.error.call_args[0][0]
assert logged.strip() == str(error) or logged.strip() == f"{error.__class__.__name__}: {error}"
mock_sys_exit.assert_called_once_with(1)
def test_unknown_exception(self, mock_logger:MagicMock, mock_sys_exit:MagicMock) -> None:
"""Test that unknown exceptions are logged with type and message."""
error = RuntimeError("Test error")
on_exception(RuntimeError, error, None)
logged = mock_logger.error.call_args[0][0]
assert logged.strip() == f"{error.__class__.__name__}: {error}"
mock_sys_exit.assert_called_once_with(1)
def test_missing_exception_info(self, mock_logger:MagicMock, mock_sys_exit:MagicMock) -> None:
"""Test handling of missing exception information."""
on_exception(None, None, None)
mock_logger.error.assert_called_once()
# sys.exit is not called for missing exception info
mock_sys_exit.assert_not_called()
def test_debug_mode_error(self, mock_logger:MagicMock, mock_sys_exit:MagicMock) -> None:
"""Test error handling in debug mode."""
with patch("kleinanzeigen_bot.utils.error_handlers.loggers.is_debug", return_value = True):
try:
raise ValueError("Test error")
except ValueError as error:
_, _, tb = sys.exc_info()
on_exception(ValueError, error, tb)
mock_logger.error.assert_called_once()
# Verify that traceback was included
logged = mock_logger.error.call_args[0][0]
assert "Traceback" in logged
assert "ValueError: Test error" in logged
mock_sys_exit.assert_called_once_with(1)
def test_attribute_error(self, mock_logger:MagicMock, mock_sys_exit:MagicMock) -> None:
"""Test handling of AttributeError."""
try:
raise AttributeError("Test error")
except AttributeError as error:
_, _, tb = sys.exc_info()
on_exception(AttributeError, error, tb)
mock_logger.error.assert_called_once()
# Verify that traceback was included
logged = mock_logger.error.call_args[0][0]
assert "Traceback" in logged
assert "AttributeError: Test error" in logged
mock_sys_exit.assert_called_once_with(1)
def test_import_error(self, mock_logger:MagicMock, mock_sys_exit:MagicMock) -> None:
"""Test handling of ImportError."""
try:
raise ImportError("Test error")
except ImportError as error:
_, _, tb = sys.exc_info()
on_exception(ImportError, error, tb)
mock_logger.error.assert_called_once()
# Verify that traceback was included
logged = mock_logger.error.call_args[0][0]
assert "Traceback" in logged
assert "ImportError: Test error" in logged
mock_sys_exit.assert_called_once_with(1)
def test_name_error(self, mock_logger:MagicMock, mock_sys_exit:MagicMock) -> None:
"""Test handling of NameError."""
try:
raise NameError("Test error")
except NameError as error:
_, _, tb = sys.exc_info()
on_exception(NameError, error, tb)
mock_logger.error.assert_called_once()
# Verify that traceback was included
logged = mock_logger.error.call_args[0][0]
assert "Traceback" in logged
assert "NameError: Test error" in logged
mock_sys_exit.assert_called_once_with(1)
def test_type_error(self, mock_logger:MagicMock, mock_sys_exit:MagicMock) -> None:
"""Test handling of TypeError."""
try:
raise TypeError("Test error")
except TypeError as error:
_, _, tb = sys.exc_info()
on_exception(TypeError, error, tb)
mock_logger.error.assert_called_once()
# Verify that traceback was included
logged = mock_logger.error.call_args[0][0]
assert "Traceback" in logged
assert "TypeError: Test error" in logged
mock_sys_exit.assert_called_once_with(1)
class TestSignalHandler:
"""Test cases for the signal handler."""
def test_sigint_handler(self, mock_logger:MagicMock, mock_sys_exit:MagicMock) -> None:
"""Test that SIGINT is handled with a warning message."""
on_sigint(2, None) # 2 is SIGINT
mock_logger.warning.assert_called_once_with("Aborted on user request.")
mock_sys_exit.assert_called_once_with(0)