"""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=tmpdir, # Use tmpdir (exists) instead of /etc/fail2ban geoip_db_path=None, # None = skip geoip validation 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, startup_db = await startup_shared_resources(app, settings) # Verify all stages completed successfully assert http_session is not None assert scheduler is not None assert startup_db 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, startup_db = await startup_shared_resources(app, settings) # Verify scheduler is running assert scheduler.running # Cleanup await http_session.close() scheduler.shutdown(wait=False)