""" Integration tests for asynchronous series data loading. Tests the complete flow from API to database with WebSocket notifications. Note: These tests focus on the integration between components and proper async behavior. Mocking is used to avoid dependencies on external services (TMDB, Aniworld, etc.). """ import asyncio from unittest.mock import AsyncMock, Mock import pytest from httpx import ASGITransport, AsyncClient from src.server.fastapi_app import app from src.server.services.background_loader_service import ( BackgroundLoaderService, LoadingStatus, ) @pytest.fixture async def client(): """Create an async test client.""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac class TestBackgroundLoaderIntegration: """Integration tests for BackgroundLoaderService.""" @pytest.mark.asyncio async def test_loader_initialization(self): """Test that background loader initializes correctly.""" mock_websocket = AsyncMock() mock_anime_service = AsyncMock() mock_series_app = Mock() loader = BackgroundLoaderService( websocket_service=mock_websocket, anime_service=mock_anime_service, series_app=mock_series_app ) assert loader.websocket_service == mock_websocket assert loader.anime_service == mock_anime_service assert loader.series_app == mock_series_app assert loader.task_queue is not None assert loader.active_tasks == {} @pytest.mark.asyncio async def test_loader_start_stop(self): """Test starting and stopping the background loader.""" mock_websocket = AsyncMock() mock_anime_service = AsyncMock() mock_series_app = Mock() loader = BackgroundLoaderService( websocket_service=mock_websocket, anime_service=mock_anime_service, series_app=mock_series_app ) # Start loader await loader.start() assert loader.worker_task is not None assert not loader.worker_task.done() # Stop loader await loader.stop() assert loader.worker_task.done() @pytest.mark.asyncio async def test_add_series_loading_task(self): """Test adding a series loading task to the queue.""" mock_websocket = AsyncMock() mock_anime_service = AsyncMock() mock_series_app = Mock() loader = BackgroundLoaderService( websocket_service=mock_websocket, anime_service=mock_anime_service, series_app=mock_series_app ) await loader.start() try: # Add a task await loader.add_series_loading_task( key="test-series", folder="test_folder", name="Test Series" ) # Wait a moment for task to be processed await asyncio.sleep(0.2) # Verify task was added assert "test-series" in loader.active_tasks task = loader.active_tasks["test-series"] assert task.key == "test-series" assert task.name == "Test Series" assert task.folder == "test_folder" finally: await loader.stop() @pytest.mark.asyncio async def test_multiple_tasks_concurrent(self): """Test loader handles multiple concurrent tasks.""" mock_websocket = AsyncMock() mock_anime_service = AsyncMock() mock_series_app = Mock() # Setup mocks to simulate async work mock_anime_service.scan_series_episodes = AsyncMock() mock_anime_service.update_series = AsyncMock() mock_series_app.nfo_service = Mock() mock_series_app.nfo_service.create_nfo_async = AsyncMock() loader = BackgroundLoaderService( websocket_service=mock_websocket, anime_service=mock_anime_service, series_app=mock_series_app ) await loader.start() try: # Add multiple tasks quickly series_count = 3 for i in range(series_count): await loader.add_series_loading_task( key=f"series-{i}", folder=f"folder_{i}", name=f"Series {i}" ) # Wait for processing await asyncio.sleep(0.5) # Verify all tasks were registered assert len(loader.active_tasks) >= series_count finally: await loader.stop() @pytest.mark.asyncio async def test_graceful_shutdown(self): """Test graceful shutdown with pending tasks.""" mock_websocket = AsyncMock() mock_anime_service = AsyncMock() mock_series_app = Mock() loader = BackgroundLoaderService( websocket_service=mock_websocket, anime_service=mock_anime_service, series_app=mock_series_app ) await loader.start() # Add tasks for i in range(5): await loader.add_series_loading_task( key=f"series-{i}", folder=f"folder_{i}", name=f"Series {i}" ) # Stop should complete without exceptions try: await loader.stop() shutdown_success = True except Exception: shutdown_success = False assert shutdown_success # Check all worker tasks are done assert all(task.done() for task in loader.worker_tasks) @pytest.mark.asyncio async def test_no_duplicate_tasks(self): """Test that duplicate tasks for same series are handled.""" mock_websocket = AsyncMock() mock_anime_service = AsyncMock() mock_series_app = Mock() loader = BackgroundLoaderService( websocket_service=mock_websocket, anime_service=mock_anime_service, series_app=mock_series_app ) await loader.start() try: # Try to add same series multiple times for _ in range(3): await loader.add_series_loading_task( key="test-series", folder="test_folder", name="Test Series" ) await asyncio.sleep(0.2) # Should only have one task for this series series_tasks = [k for k in loader.active_tasks if k == "test-series"] assert len(series_tasks) == 1 finally: await loader.stop() class TestLoadingStatusEnum: """Tests for LoadingStatus enum.""" def test_loading_status_values(self): """Test LoadingStatus enum has expected values.""" assert LoadingStatus.PENDING.value == "pending" assert LoadingStatus.LOADING_EPISODES.value == "loading_episodes" assert LoadingStatus.LOADING_NFO.value == "loading_nfo" assert LoadingStatus.LOADING_LOGO.value == "loading_logo" assert LoadingStatus.LOADING_IMAGES.value == "loading_images" assert LoadingStatus.COMPLETED.value == "completed" assert LoadingStatus.FAILED.value == "failed" def test_loading_status_string_repr(self): """Test LoadingStatus can be used as strings.""" status = LoadingStatus.LOADING_EPISODES # The enum string representation includes the class name assert status.value == "loading_episodes" assert status == LoadingStatus.LOADING_EPISODES class TestAsyncBehavior: """Tests to verify async and non-blocking behavior.""" @pytest.mark.asyncio async def test_adding_tasks_is_fast(self): """Test that adding tasks doesn't block.""" mock_websocket = AsyncMock() mock_anime_service = AsyncMock() mock_series_app = Mock() # Make operations slow async def slow_operation(*args, **kwargs): await asyncio.sleep(0.5) mock_anime_service.scan_series_episodes = AsyncMock(side_effect=slow_operation) mock_anime_service.update_series = AsyncMock() mock_series_app.nfo_service = Mock() mock_series_app.nfo_service.create_nfo_async = AsyncMock(side_effect=slow_operation) loader = BackgroundLoaderService( websocket_service=mock_websocket, anime_service=mock_anime_service, series_app=mock_series_app ) await loader.start() try: # Add multiple series quickly start_time = asyncio.get_event_loop().time() for i in range(5): await loader.add_series_loading_task( key=f"series-{i}", folder=f"folder_{i}", name=f"Series {i}" ) add_time = asyncio.get_event_loop().time() - start_time # Adding tasks should be fast (< 0.1s for 5 series) # even though processing is slow assert add_time < 0.1 # Verify all were queued await asyncio.sleep(0.1) assert len(loader.active_tasks) == 5 finally: await loader.stop()