"""Unit tests for enhanced_provider.py - Caching, recovery, download logic.""" import json import os from typing import Any, Dict, List from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest from src.core.error_handler import ( DownloadError, NetworkError, NonRetryableError, RetryableError, ) from src.core.providers.base_provider import Loader # Import the class but we need a concrete subclass to test it from src.core.providers.enhanced_provider import EnhancedAniWorldLoader class ConcreteEnhancedLoader(EnhancedAniWorldLoader): """Concrete subclass that bridges PascalCase methods to abstract interface.""" def subscribe_download_progress(self, handler): pass def unsubscribe_download_progress(self, handler): pass def search(self, word: str) -> List[Dict[str, Any]]: return self.Search(word) def is_language(self, season, episode, key, language="German Dub"): return self.IsLanguage(season, episode, key, language) def download(self, base_directory, serie_folder, season, episode, key, language="German Dub", **kwargs): return self.Download(base_directory, serie_folder, season, episode, key, language) def get_site_key(self) -> str: return self.GetSiteKey() def get_title(self, key: str) -> str: return self.GetTitle(key) @pytest.fixture def enhanced_loader(): """Create ConcreteEnhancedLoader with mocked externals.""" with patch( "src.core.providers.enhanced_provider.UserAgent" ) as mock_ua, patch( "src.core.providers.enhanced_provider.get_integrity_manager" ): mock_ua.return_value.random = "MockAgent/1.0" loader = ConcreteEnhancedLoader() loader.session = MagicMock() return loader class TestEnhancedLoaderInit: """Test EnhancedAniWorldLoader initialization.""" def test_initializes_with_download_stats(self, enhanced_loader): """Should initialize download statistics at zero.""" stats = enhanced_loader.download_stats assert stats["total_downloads"] == 0 assert stats["successful_downloads"] == 0 assert stats["failed_downloads"] == 0 assert stats["retried_downloads"] == 0 def test_initializes_with_caches(self, enhanced_loader): """Should initialize empty caches.""" assert enhanced_loader._KeyHTMLDict == {} assert enhanced_loader._EpisodeHTMLDict == {} def test_site_key(self, enhanced_loader): """GetSiteKey should return 'aniworld.to'.""" assert enhanced_loader.GetSiteKey() == "aniworld.to" def test_has_supported_providers(self, enhanced_loader): """Should have a list of supported providers.""" assert isinstance(enhanced_loader.SUPPORTED_PROVIDERS, list) assert len(enhanced_loader.SUPPORTED_PROVIDERS) > 0 assert "VOE" in enhanced_loader.SUPPORTED_PROVIDERS class TestEnhancedSearch: """Test enhanced search with error recovery.""" def test_search_empty_term_raises(self, enhanced_loader): """Search with empty term should raise ValueError.""" with pytest.raises(ValueError, match="empty"): enhanced_loader.Search("") def test_search_whitespace_only_raises(self, enhanced_loader): """Search with whitespace-only term should raise ValueError.""" with pytest.raises(ValueError, match="empty"): enhanced_loader.Search(" ") def test_search_successful(self, enhanced_loader): """Successful search should return parsed list.""" mock_response = MagicMock() mock_response.ok = True mock_response.text = json.dumps([ {"title": "Naruto", "link": "/anime/stream/naruto"} ]) enhanced_loader.session.get.return_value = mock_response result = enhanced_loader.Search("Naruto") assert len(result) == 1 assert result[0]["title"] == "Naruto" class TestParseAnimeResponse: """Test JSON parsing strategies.""" def test_parse_valid_json_list(self, enhanced_loader): """Should parse valid JSON list.""" text = '[{"title": "Naruto"}]' result = enhanced_loader._parse_anime_response(text) assert len(result) == 1 def test_parse_html_escaped_json(self, enhanced_loader): """Should handle HTML-escaped JSON.""" text = '[{"title": "Naruto & Boruto"}]' result = enhanced_loader._parse_anime_response(text) assert result[0]["title"] == "Naruto & Boruto" def test_parse_empty_response_raises(self, enhanced_loader): """Empty response should raise ValueError.""" with pytest.raises(ValueError, match="Empty response"): enhanced_loader._parse_anime_response("") def test_parse_whitespace_only_raises(self, enhanced_loader): """Whitespace-only response should raise ValueError.""" with pytest.raises(ValueError, match="Empty response"): enhanced_loader._parse_anime_response(" ") def test_parse_html_response_raises(self, enhanced_loader): """HTML response instead of JSON should raise ValueError.""" with pytest.raises(ValueError): enhanced_loader._parse_anime_response( "" ) def test_parse_bom_json(self, enhanced_loader): """Should handle BOM-prefixed JSON.""" text = '\ufeff[{"title": "Test"}]' result = enhanced_loader._parse_anime_response(text) assert len(result) == 1 def test_parse_control_characters(self, enhanced_loader): """Should strip control characters and parse.""" text = '[{"title": "Na\x00ruto"}]' result = enhanced_loader._parse_anime_response(text) assert len(result) == 1 def test_parse_non_list_result_raises(self, enhanced_loader): """Non-list JSON should raise ValueError.""" with pytest.raises(ValueError): enhanced_loader._parse_anime_response('{"key": "value"}') class TestLanguageKey: """Test language code mapping.""" def test_german_dub(self, enhanced_loader): """German Dub should map to 1.""" assert enhanced_loader._GetLanguageKey("German Dub") == 1 def test_english_sub(self, enhanced_loader): """English Sub should map to 2.""" assert enhanced_loader._GetLanguageKey("English Sub") == 2 def test_german_sub(self, enhanced_loader): """German Sub should map to 3.""" assert enhanced_loader._GetLanguageKey("German Sub") == 3 def test_unknown_language(self, enhanced_loader): """Unknown language should map to 0.""" assert enhanced_loader._GetLanguageKey("French Dub") == 0 class TestEnhancedIsLanguage: """Test language availability checking with recovery.""" def test_is_language_available(self, enhanced_loader): """Should return True when language is available.""" html = """
""" mock_response = MagicMock() mock_response.content = html.encode("utf-8") enhanced_loader._EpisodeHTMLDict[( "naruto", 1, 1 )] = mock_response result = enhanced_loader.IsLanguage(1, 1, "naruto", "German Dub") assert result is True def test_is_language_not_available(self, enhanced_loader): """Should return False when language is not available.""" html = """
""" mock_response = MagicMock() mock_response.content = html.encode("utf-8") enhanced_loader._EpisodeHTMLDict[( "naruto", 1, 1 )] = mock_response result = enhanced_loader.IsLanguage(1, 1, "naruto", "German Sub") assert result is False def test_is_language_no_language_box(self, enhanced_loader): """Should return False when no language box in HTML.""" html = "" mock_response = MagicMock() mock_response.content = html.encode("utf-8") enhanced_loader._EpisodeHTMLDict[( "naruto", 1, 1 )] = mock_response result = enhanced_loader.IsLanguage(1, 1, "naruto", "German Dub") assert result is False class TestEnhancedGetTitle: """Test title extraction with error recovery.""" def test_get_title_successful(self, enhanced_loader): """Should extract title from HTML.""" html = """

Attack on Titan

""" mock_response = MagicMock() mock_response.content = html.encode("utf-8") enhanced_loader._KeyHTMLDict["aot"] = mock_response result = enhanced_loader.GetTitle("aot") assert result == "Attack on Titan" def test_get_title_missing_returns_fallback(self, enhanced_loader): """Should return fallback title when not found in HTML.""" html = "" mock_response = MagicMock() mock_response.content = html.encode("utf-8") enhanced_loader._KeyHTMLDict["unknown"] = mock_response result = enhanced_loader.GetTitle("unknown") assert "Unknown_Title" in result class TestEnhancedCache: """Test cache operations.""" def test_clear_cache(self, enhanced_loader): """ClearCache should empty all caches.""" enhanced_loader._KeyHTMLDict["key"] = "data" enhanced_loader._EpisodeHTMLDict[("key", 1, 1)] = "data" enhanced_loader.ClearCache() assert len(enhanced_loader._KeyHTMLDict) == 0 assert len(enhanced_loader._EpisodeHTMLDict) == 0 def test_remove_from_cache(self, enhanced_loader): """RemoveFromCache should only clear episode cache.""" enhanced_loader._KeyHTMLDict["key"] = "data" enhanced_loader._EpisodeHTMLDict[("key", 1, 1)] = "data" enhanced_loader.RemoveFromCache() assert len(enhanced_loader._KeyHTMLDict) == 1 assert len(enhanced_loader._EpisodeHTMLDict) == 0 class TestEnhancedGetEpisodeHTML: """Test episode HTML fetching with validation.""" def test_empty_key_raises(self, enhanced_loader): """Empty key should raise ValueError.""" with pytest.raises(ValueError, match="empty"): enhanced_loader._GetEpisodeHTML(1, 1, "") def test_whitespace_key_raises(self, enhanced_loader): """Whitespace key should raise ValueError.""" with pytest.raises(ValueError, match="empty"): enhanced_loader._GetEpisodeHTML(1, 1, " ") def test_invalid_season_zero_raises(self, enhanced_loader): """Season 0 should raise ValueError.""" with pytest.raises(ValueError, match="Invalid season"): enhanced_loader._GetEpisodeHTML(0, 1, "naruto") def test_invalid_season_negative_raises(self, enhanced_loader): """Negative season should raise ValueError.""" with pytest.raises(ValueError, match="Invalid season"): enhanced_loader._GetEpisodeHTML(-1, 1, "naruto") def test_invalid_episode_zero_raises(self, enhanced_loader): """Episode 0 should raise ValueError.""" with pytest.raises(ValueError, match="Invalid episode"): enhanced_loader._GetEpisodeHTML(1, 0, "naruto") def test_cached_episode_returned(self, enhanced_loader): """Should return cached response without HTTP call.""" mock_response = MagicMock() enhanced_loader._EpisodeHTMLDict[("naruto", 1, 1)] = mock_response result = enhanced_loader._GetEpisodeHTML(1, 1, "naruto") assert result is mock_response enhanced_loader.session.get.assert_not_called() class TestDownloadStatistics: """Test download statistics tracking.""" def test_get_download_statistics(self, enhanced_loader): """Should return stats with calculated success rate.""" enhanced_loader.download_stats["total_downloads"] = 10 enhanced_loader.download_stats["successful_downloads"] = 8 enhanced_loader.download_stats["failed_downloads"] = 2 stats = enhanced_loader.get_download_statistics() assert stats["success_rate"] == 80.0 def test_statistics_zero_downloads(self, enhanced_loader): """Success rate should be 0 with no downloads.""" stats = enhanced_loader.get_download_statistics() assert stats["success_rate"] == 0 def test_reset_statistics(self, enhanced_loader): """reset_statistics should zero all counters.""" enhanced_loader.download_stats["total_downloads"] = 10 enhanced_loader.download_stats["successful_downloads"] = 8 enhanced_loader.reset_statistics() assert enhanced_loader.download_stats["total_downloads"] == 0 assert enhanced_loader.download_stats["successful_downloads"] == 0 class TestEnhancedDownloadValidation: """Test download input validation.""" @patch("src.core.providers.enhanced_provider.get_integrity_manager") def test_download_missing_base_directory_raises( self, mock_integrity, enhanced_loader ): """Download with empty base_directory should raise.""" with pytest.raises((ValueError, DownloadError)): enhanced_loader.Download("", "folder", 1, 1, "key") @patch("src.core.providers.enhanced_provider.get_integrity_manager") def test_download_missing_serie_folder_raises( self, mock_integrity, enhanced_loader ): """Download with empty serie_folder should raise.""" with pytest.raises((ValueError, DownloadError)): enhanced_loader.Download("/base", "", 1, 1, "key") @patch("src.core.providers.enhanced_provider.get_integrity_manager") def test_download_negative_season_raises( self, mock_integrity, enhanced_loader ): """Download with negative season should raise.""" with pytest.raises((ValueError, DownloadError)): enhanced_loader.Download("/base", "folder", -1, 1, "key") @patch("src.core.providers.enhanced_provider.get_integrity_manager") def test_download_negative_episode_raises( self, mock_integrity, enhanced_loader ): """Download with negative episode should raise.""" with pytest.raises((ValueError, DownloadError)): enhanced_loader.Download("/base", "folder", 1, -1, "key") @patch("src.core.providers.enhanced_provider.get_integrity_manager") def test_download_increments_total_count( self, mock_integrity, enhanced_loader ): """Download should increment total_downloads counter.""" # Make it fail fast so we don't need to mock everything enhanced_loader._KeyHTMLDict["key"] = MagicMock( content=b"

Test

" ) try: enhanced_loader.Download("/base", "folder", 1, 1, "key") except Exception: pass assert enhanced_loader.download_stats["total_downloads"] >= 1 class TestEnhancedProviderFromHTML: """Test provider extraction from episode HTML.""" def test_extract_providers(self, enhanced_loader): """Should extract providers with language keys from HTML.""" html = """
  • VOE

  • Vidmoly

  • """ mock_response = MagicMock() mock_response.content = html.encode("utf-8") enhanced_loader._EpisodeHTMLDict[("test", 1, 1)] = mock_response result = enhanced_loader._get_provider_from_html(1, 1, "test") assert "VOE" in result assert "Vidmoly" in result def test_extract_providers_empty_page(self, enhanced_loader): """Should return empty dict when no providers found.""" html = "" mock_response = MagicMock() mock_response.content = html.encode("utf-8") enhanced_loader._EpisodeHTMLDict[("test", 1, 1)] = mock_response result = enhanced_loader._get_provider_from_html(1, 1, "test") assert result == {}