"""Unit tests for AnimeService. Tests cover service initialization, async operations, caching, error handling, and progress reporting integration. """ from __future__ import annotations import asyncio from unittest.mock import AsyncMock, MagicMock, patch import pytest from src.server.services.anime_service import AnimeService, AnimeServiceError from src.server.services.progress_service import ProgressService @pytest.fixture def mock_series_app(): """Create a mock SeriesApp instance.""" with patch("src.server.services.anime_service.SeriesApp") as mock_class: mock_instance = MagicMock() mock_instance.series_list = [] mock_instance.search = MagicMock(return_value=[]) mock_instance.ReScan = MagicMock() mock_instance.download = MagicMock(return_value=True) mock_class.return_value = mock_instance yield mock_instance @pytest.fixture def mock_progress_service(): """Create a mock ProgressService instance.""" service = MagicMock(spec=ProgressService) service.start_progress = AsyncMock() service.update_progress = AsyncMock() service.complete_progress = AsyncMock() service.fail_progress = AsyncMock() return service @pytest.fixture def anime_service(tmp_path, mock_series_app, mock_progress_service): """Create an AnimeService instance for testing.""" return AnimeService( directory=str(tmp_path), max_workers=2, progress_service=mock_progress_service, ) class TestAnimeServiceInitialization: """Test AnimeService initialization.""" def test_initialization_success(self, tmp_path, mock_progress_service): """Test successful service initialization.""" with patch("src.server.services.anime_service.SeriesApp"): service = AnimeService( directory=str(tmp_path), max_workers=2, progress_service=mock_progress_service, ) assert service._directory == str(tmp_path) assert service._executor is not None assert service._progress_service is mock_progress_service def test_initialization_failure_raises_error( self, tmp_path, mock_progress_service ): """Test SeriesApp initialization failure raises error.""" with patch( "src.server.services.anime_service.SeriesApp" ) as mock_class: mock_class.side_effect = Exception("Initialization failed") with pytest.raises( AnimeServiceError, match="Initialization failed" ): AnimeService( directory=str(tmp_path), progress_service=mock_progress_service, ) class TestListMissing: """Test list_missing operation.""" @pytest.mark.asyncio async def test_list_missing_empty(self, anime_service, mock_series_app): """Test listing missing episodes when list is empty.""" mock_series_app.series_list = [] result = await anime_service.list_missing() assert isinstance(result, list) assert len(result) == 0 @pytest.mark.asyncio async def test_list_missing_with_series( self, anime_service, mock_series_app ): """Test listing missing episodes with series data.""" mock_series_app.series_list = [ {"name": "Test Series 1", "missing": [1, 2]}, {"name": "Test Series 2", "missing": [3]}, ] result = await anime_service.list_missing() assert len(result) == 2 assert result[0]["name"] == "Test Series 1" assert result[1]["name"] == "Test Series 2" @pytest.mark.asyncio async def test_list_missing_caching(self, anime_service, mock_series_app): """Test that list_missing uses caching.""" mock_series_app.series_list = [{"name": "Test Series"}] # First call result1 = await anime_service.list_missing() # Second call (should use cache) result2 = await anime_service.list_missing() assert result1 == result2 @pytest.mark.asyncio async def test_list_missing_error_handling( self, anime_service, mock_series_app ): """Test error handling in list_missing.""" mock_series_app.series_list = None # Cause an error # Error message will be about NoneType not being iterable with pytest.raises(AnimeServiceError): await anime_service.list_missing() class TestSearch: """Test search operation.""" @pytest.mark.asyncio async def test_search_empty_query(self, anime_service): """Test search with empty query returns empty list.""" result = await anime_service.search("") assert result == [] @pytest.mark.asyncio async def test_search_success(self, anime_service, mock_series_app): """Test successful search operation.""" mock_series_app.search.return_value = [ {"name": "Test Anime", "url": "http://example.com"} ] result = await anime_service.search("test") assert len(result) == 1 assert result[0]["name"] == "Test Anime" mock_series_app.search.assert_called_once_with("test") @pytest.mark.asyncio async def test_search_error_handling( self, anime_service, mock_series_app ): """Test error handling during search.""" mock_series_app.search.side_effect = Exception("Search failed") with pytest.raises(AnimeServiceError, match="Search failed"): await anime_service.search("test query") class TestRescan: """Test rescan operation.""" @pytest.mark.asyncio async def test_rescan_success( self, anime_service, mock_series_app, mock_progress_service ): """Test successful rescan operation.""" await anime_service.rescan() # Verify SeriesApp.ReScan was called mock_series_app.ReScan.assert_called_once() # Verify progress tracking mock_progress_service.start_progress.assert_called_once() mock_progress_service.complete_progress.assert_called_once() @pytest.mark.asyncio async def test_rescan_with_callback(self, anime_service, mock_series_app): """Test rescan with progress callback.""" callback_called = False callback_data = None def callback(data): nonlocal callback_called, callback_data callback_called = True callback_data = data # Mock ReScan to call the callback def mock_rescan(cb): if cb: cb({"current": 5, "total": 10, "message": "Scanning..."}) mock_series_app.ReScan.side_effect = mock_rescan await anime_service.rescan(callback=callback) assert callback_called assert callback_data is not None @pytest.mark.asyncio async def test_rescan_clears_cache(self, anime_service, mock_series_app): """Test that rescan clears the list cache.""" # Populate cache mock_series_app.series_list = [{"name": "Test"}] await anime_service.list_missing() # Update series list mock_series_app.series_list = [{"name": "Test"}, {"name": "New"}] # Rescan should clear cache await anime_service.rescan() # Next list_missing should return updated data result = await anime_service.list_missing() assert len(result) == 2 @pytest.mark.asyncio async def test_rescan_error_handling( self, anime_service, mock_series_app, mock_progress_service ): """Test error handling during rescan.""" mock_series_app.ReScan.side_effect = Exception("Rescan failed") with pytest.raises(AnimeServiceError, match="Rescan failed"): await anime_service.rescan() # Verify progress failure was recorded mock_progress_service.fail_progress.assert_called_once() class TestDownload: """Test download operation.""" @pytest.mark.asyncio async def test_download_success(self, anime_service, mock_series_app): """Test successful download operation.""" mock_series_app.download.return_value = True result = await anime_service.download( serie_folder="test_series", season=1, episode=1, key="test_key", ) assert result is True mock_series_app.download.assert_called_once_with( "test_series", 1, 1, "test_key", None ) @pytest.mark.asyncio async def test_download_with_callback(self, anime_service, mock_series_app): """Test download with progress callback.""" callback = MagicMock() mock_series_app.download.return_value = True result = await anime_service.download( serie_folder="test_series", season=1, episode=1, key="test_key", callback=callback, ) assert result is True # Verify callback was passed to SeriesApp mock_series_app.download.assert_called_once_with( "test_series", 1, 1, "test_key", callback ) @pytest.mark.asyncio async def test_download_error_handling(self, anime_service, mock_series_app): """Test error handling during download.""" mock_series_app.download.side_effect = Exception("Download failed") with pytest.raises(AnimeServiceError, match="Download failed"): await anime_service.download( serie_folder="test_series", season=1, episode=1, key="test_key", ) class TestConcurrency: """Test concurrent operations.""" @pytest.mark.asyncio async def test_multiple_concurrent_operations( self, anime_service, mock_series_app ): """Test that multiple operations can run concurrently.""" mock_series_app.search.return_value = [{"name": "Test"}] # Run multiple searches concurrently tasks = [ anime_service.search("query1"), anime_service.search("query2"), anime_service.search("query3"), ] results = await asyncio.gather(*tasks) assert len(results) == 3 assert all(len(r) == 1 for r in results) class TestFactoryFunction: """Test factory function.""" def test_get_anime_service(self, tmp_path): """Test get_anime_service factory function.""" from src.server.services.anime_service import get_anime_service with patch("src.server.services.anime_service.SeriesApp"): service = get_anime_service(directory=str(tmp_path)) assert isinstance(service, AnimeService) assert service._directory == str(tmp_path)