Files
BanGUI/backend/tests/test_startup_integration.py
Lukas e86ab6dad1 10) Implement explicit startup DAG for resource initialization
- Created StartupDAG class to orchestrate startup stages with explicit dependencies
- Defined 6 startup stages: WORKER_MODE → DATABASE → GEO_CACHE → HTTP_SESSION → SCHEDULER → TASKS
- Each stage has prerequisites, error handling, and rollback support
- Refactored startup_shared_resources() to use the DAG
- Added StartupContext for resource tracking and failure management
- Partial failures automatically roll back all completed resources in reverse order
- Added health checks to verify all resources initialized successfully
- Comprehensive test coverage: 15 DAG unit tests + 3 integration tests + 6 existing tests
- Documented startup DAG in Architekture.md with detailed stage descriptions and failure modes

This replaces implicit ordering with explicit dependency tracking, making lifecycle
changes safe and failure modes predictable. Hidden order dependencies no longer exist.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-28 08:08:05 +02:00

189 lines
7.0 KiB
Python

"""Integration tests for the complete startup flow with StartupDAG."""
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import FastAPI
from app.config import Settings
from app.startup import startup_shared_resources
def _create_test_settings(tmpdir: str) -> Settings:
"""Create a minimal Settings object for testing."""
return Settings(
database_path=str(Path(tmpdir) / "bangui.db"),
fail2ban_socket="/var/run/fail2ban/fail2ban.sock",
session_secret="test-secret-12345678901234567890",
fail2ban_config_dir="/etc/fail2ban",
geoip_db_path="/usr/share/GeoIP/GeoLite2-Country.mmdb",
geoip_allow_http_fallback=False,
log_level="info",
)
@pytest.mark.asyncio
async def test_startup_shared_resources_complete_flow() -> None:
"""Test that startup_shared_resources successfully initializes all resources via DAG."""
# Create a test app
app = FastAPI()
app.state = MagicMock()
# Create minimal settings for testing
with tempfile.TemporaryDirectory() as tmpdir:
settings = _create_test_settings(tmpdir)
# Mock external dependencies that would require actual fail2ban/MaxMind
with patch("app.startup.open_db") as mock_open_db, patch(
"app.startup.init_db"
) as mock_init_db, patch(
"app.startup.setup_service.is_setup_complete"
) as mock_is_setup_complete, patch(
"app.startup.set_setup_complete_cache"
) as mock_set_setup_complete, patch(
"app.startup.GeoCache"
) as mock_geo_cache_class, patch(
"app.startup.ensure_jail_configs"
) as mock_ensure_jail, patch(
"app.startup.health_check.register"
) as mock_health_check_register, patch(
"app.startup.blocklist_import.register"
) as mock_blocklist_import_register, patch(
"app.startup.geo_cache_cleanup.register"
) as mock_geo_cache_cleanup_register, patch(
"app.startup.geo_cache_flush.register"
) as mock_geo_cache_flush_register, patch(
"app.startup.geo_re_resolve.register"
) as mock_geo_re_resolve_register, patch(
"app.startup.history_sync.register"
) as mock_history_sync_register, patch(
"app.startup.session_cleanup.register"
) as mock_session_cleanup_register:
# Setup mock database
mock_db = AsyncMock()
mock_db.close = AsyncMock()
mock_open_db.return_value = mock_db
# Setup mock services
mock_init_db.return_value = None
mock_is_setup_complete.return_value = False
mock_set_setup_complete.return_value = None
# Setup mock GeoCache
mock_geo_cache = MagicMock()
mock_geo_cache.load_cache_from_db = AsyncMock()
mock_geo_cache.count_unresolved = AsyncMock(return_value=0)
mock_geo_cache.init_geoip = MagicMock()
mock_geo_cache_class.return_value = mock_geo_cache
# Setup mock blocklist import (async function)
mock_blocklist_import_register.return_value = None
# Call startup_shared_resources
http_session, scheduler = await startup_shared_resources(app, settings)
# Verify all stages completed successfully
assert http_session is not None
assert scheduler is not None
assert scheduler.running
# Verify resources were initialized
assert app.state.geo_cache is mock_geo_cache
# Verify all task registration functions were called
mock_health_check_register.assert_called_once()
mock_blocklist_import_register.assert_called_once()
mock_geo_cache_cleanup_register.assert_called_once()
mock_geo_cache_flush_register.assert_called_once()
mock_geo_re_resolve_register.assert_called_once()
mock_history_sync_register.assert_called_once()
mock_session_cleanup_register.assert_called_once()
# Cleanup
await http_session.close()
scheduler.shutdown(wait=False)
@pytest.mark.asyncio
async def test_startup_shared_resources_rollback_on_database_failure() -> None:
"""Test that startup_shared_resources rolls back all resources if database init fails."""
app = FastAPI()
app.state = MagicMock()
with tempfile.TemporaryDirectory() as tmpdir:
settings = _create_test_settings(tmpdir)
with patch("app.startup.open_db") as mock_open_db, patch(
"app.startup.init_db"
) as mock_init_db:
# Setup mock database to fail
mock_db = AsyncMock()
mock_db.close = AsyncMock()
mock_open_db.return_value = mock_db
mock_init_db.side_effect = RuntimeError("Database initialization failed")
# startup_shared_resources should raise the database error
with pytest.raises(RuntimeError, match="Database initialization failed"):
await startup_shared_resources(app, settings)
# Verify cleanup was attempted
mock_db.close.assert_called()
@pytest.mark.asyncio
async def test_startup_shared_resources_scheduler_starts() -> None:
"""Test that the scheduler is started during startup."""
app = FastAPI()
app.state = MagicMock()
with tempfile.TemporaryDirectory() as tmpdir:
settings = _create_test_settings(tmpdir)
with patch("app.startup.open_db") as mock_open_db, patch(
"app.startup.init_db"
), patch("app.startup.setup_service.is_setup_complete") as mock_is_setup, patch(
"app.startup.set_setup_complete_cache"
), patch(
"app.startup.GeoCache"
) as mock_geo_cache_class, patch(
"app.startup.ensure_jail_configs"
), patch(
"app.startup.health_check.register"
), patch(
"app.startup.blocklist_import.register"
), patch(
"app.startup.geo_cache_cleanup.register"
), patch(
"app.startup.geo_cache_flush.register"
), patch(
"app.startup.geo_re_resolve.register"
), patch(
"app.startup.history_sync.register"
), patch(
"app.startup.session_cleanup.register"
):
mock_db = AsyncMock()
mock_db.close = AsyncMock()
mock_open_db.return_value = mock_db
mock_is_setup.return_value = False
mock_geo_cache = MagicMock()
mock_geo_cache.load_cache_from_db = AsyncMock()
mock_geo_cache.count_unresolved = AsyncMock(return_value=0)
mock_geo_cache.init_geoip = MagicMock()
mock_geo_cache_class.return_value = mock_geo_cache
http_session, scheduler = await startup_shared_resources(app, settings)
# Verify scheduler is running
assert scheduler.running
# Cleanup
await http_session.close()
scheduler.shutdown(wait=False)