""" Unit tests for Anime-Sama fallback mechanism Tests player priority, caching, and URL validation """ import pytest from unittest.mock import Mock, AsyncMock, patch, MagicMock from httpx import TimeoutException, ConnectError from app.downloaders.anime_sites.animesama import AnimeSamaDownloader class TestAnimeSamaFallback: """Tests for Anime-Sama fallback mechanism""" @pytest.fixture def downloader(self): """Create AnimeSamaDownloader instance""" return AnimeSamaDownloader() @pytest.mark.asyncio async def test_fallback_tries_players_in_priority_order(self, downloader): """ Test that fallback tries players in priority order: VidMoly -> SendVid -> Sibnet -> Lpayer """ # Mock each player extraction method with patch.object(downloader, '_extract_from_vidmoly') as mock_vidmoly, \ patch.object(downloader, '_extract_from_sendvid') as mock_sendvid, \ patch.object(downloader, '_extract_from_sibnet') as mock_sibnet, \ patch.object(downloader, '_extract_from_lpayer') as mock_lpayer, \ patch.object(downloader, '_test_video_url', new_callable=AsyncMock) as mock_test_url: # Make vidmoly and sendvid fail, sibnet succeed mock_vidmoly.side_effect = Exception("VidMoly failed") mock_sendvid.side_effect = Exception("SendVid failed") mock_sibnet.return_value = ("http://sibnet.com/video.mp4", "video.mp4") mock_lpayer.return_value = ("http://lpayer.com/video.mp4", "video.mp4") # Make validation pass for sibnet mock_test_url.return_value = True result = await downloader.get_download_link_with_fallback( "http://vidmoly.to/test", anime_page_url="https://anime-sama.si/catalogue/test/vostfr/" ) # Verify player order was correct assert mock_vidmoly.called, "VidMoly should be tried first" assert mock_sendvid.called, "SendVid should be tried second" assert mock_sibnet.called, "Sibnet should be tried third" assert not mock_lpayer.called, "Lpayer should not be called since Sibnet succeeded" assert result == ("http://sibnet.com/video.mp4", "video.mp4") @pytest.mark.asyncio async def test_caching_mechanism_stores_working_player(self, downloader): """ Test that caching mechanism stores working player for same anime URL. After first successful player, subsequent requests should use cached player first. """ # Setup: First request - vidmoly fails, sendvid succeeds with patch.object(downloader, '_extract_from_vidmoly') as mock_vidmoly, \ patch.object(downloader, '_extract_from_sendvid') as mock_sendvid, \ patch.object(downloader, '_extract_from_sibnet') as mock_sibnet, \ patch.object(downloader, '_extract_from_lpayer') as mock_lpayer, \ patch.object(downloader, '_test_video_url', new_callable=AsyncMock) as mock_test_url: # First request: vidmoly fails, sendvid succeeds mock_vidmoly.side_effect = Exception("VidMoly failed") mock_sendvid.return_value = ("http://sendvid.com/video.mp4", "video.mp4") mock_test_url.return_value = True anime_url = "https://anime-sama.si/catalogue/test/vostfr/" result1 = await downloader.get_download_link_with_fallback( "http://vidmoly.to/test", anime_page_url=anime_url ) # Verify caching worked assert anime_url in downloader._working_players assert downloader._working_players[anime_url] == "sendvid" # Reset mocks for second request mock_vidmoly.reset_mock() mock_sendvid.reset_mock() mock_sibnet.reset_mock() mock_lpayer.reset_mock() # Second request: Should try sendvid first (cached) mock_sendvid.return_value = ("http://sendvid.com/video2.mp4", "video2.mp4") mock_test_url.return_value = True result2 = await downloader.get_download_link_with_fallback( "http://vidmoly.to/test", anime_page_url=anime_url ) # Verify sendvid was tried first (due to cache) assert mock_sendvid.call_count == 1, "Cached player (sendvid) should be tried first" @pytest.mark.asyncio async def test_all_players_failing_raises_exception(self, downloader): """ Test that when all players fail, an exception is raised with proper error message. """ with patch.object(downloader, '_extract_from_vidmoly') as mock_vidmoly, \ patch.object(downloader, '_extract_from_sendvid') as mock_sendvid, \ patch.object(downloader, '_extract_from_sibnet') as mock_sibnet, \ patch.object(downloader, '_extract_from_lpayer_api') as mock_lpayer_api, \ patch.object(downloader, '_extract_from_smoothpre') as mock_smoothpre: # All players fail mock_vidmoly.side_effect = Exception("VidMoly error") mock_sendvid.side_effect = Exception("SendVid error") mock_sibnet.side_effect = Exception("Sibnet error") mock_lpayer_api.side_effect = Exception("Lpayer error") mock_smoothpre.side_effect = Exception("Smoothpre error") anime_url = "https://anime-sama.si/catalogue/test/vostfr/" with pytest.raises(Exception) as exc_info: await downloader.get_download_link_with_fallback( "http://vidmoly.to/test", anime_page_url=anime_url ) # Verify error message mentions all players failed assert "All players failed" in str(exc_info.value) # Verify all players were tried assert mock_vidmoly.called assert mock_sendvid.called assert mock_sibnet.called assert mock_lpayer_api.called @pytest.mark.asyncio async def test_test_video_url_returns_true_for_valid_url(self, downloader): """ Test that _test_video_url returns True for valid video URL (HTTP 200 with content). """ # Mock the client to return valid response mock_response = Mock() mock_response.status_code = 200 mock_response.content = b"video content data" with patch.object(downloader.client, 'get', new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_response result = await downloader._test_video_url("http://example.com/video.mp4") assert result is True mock_get.assert_called_once() # Verify Range header was included call_args = mock_get.call_args assert "Range" in call_args.kwargs.get("headers", {}) @pytest.mark.asyncio async def test_test_video_url_returns_false_for_invalid_url(self, downloader): """ Test that _test_video_url returns False for invalid/non-working URL. """ # Test case 1: HTTP error status mock_response = Mock() mock_response.status_code = 404 with patch.object(downloader.client, 'get', new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_response result = await downloader._test_video_url("http://example.com/notfound.mp4") assert result is False @pytest.mark.asyncio async def test_test_video_url_returns_false_for_empty_response(self, downloader): """ Test that _test_video_url returns False for empty response content. """ mock_response = Mock() mock_response.status_code = 200 mock_response.content = b"" # Empty content with patch.object(downloader.client, 'get', new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_response result = await downloader._test_video_url("http://example.com/empty.mp4") assert result is False @pytest.mark.asyncio async def test_test_video_url_returns_false_for_timeout(self, downloader): """ Test that _test_video_url returns False for timeout. """ with patch.object(downloader.client, 'get', new_callable=AsyncMock) as mock_get: mock_get.side_effect = TimeoutException("Request timeout") result = await downloader._test_video_url("http://example.com/slow.mp4") assert result is False @pytest.mark.asyncio async def test_test_video_url_returns_false_for_connection_error(self, downloader): """ Test that _test_video_url returns False for connection error. """ with patch.object(downloader.client, 'get', new_callable=AsyncMock) as mock_get: mock_get.side_effect = ConnectError("Connection failed") result = await downloader._test_video_url("http://example.com/badhost.mp4") assert result is False @pytest.mark.asyncio async def test_fallback_skips_invalid_player_url(self, downloader): """ Test that fallback skips players that return invalid URLs (validation fails). """ with patch.object(downloader, '_extract_from_vidmoly') as mock_vidmoly, \ patch.object(downloader, '_extract_from_sendvid') as mock_sendvid, \ patch.object(downloader, '_extract_from_sibnet') as mock_sibnet, \ patch.object(downloader, '_test_video_url', new_callable=AsyncMock) as mock_test_url: # Vidmoly returns URL but validation fails mock_vidmoly.return_value = ("http://vidmoly.com/video.mp4", "video.mp4") # SendVid returns URL and validation passes mock_sendvid.return_value = ("http://sendvid.com/video.mp4", "video.mp4") mock_sibnet.return_value = ("http://sibnet.com/video.mp4", "video.mp4") # First call (vidmoly): validation fails # Second call (sendvid): validation passes # Third call (sibnet): not called because sendvid succeeded mock_test_url.side_effect = [False, True] result = await downloader.get_download_link_with_fallback( "http://vidmoly.to/test", anime_page_url="https://anime-sama.si/catalogue/test/vostfr/" ) # Verify validation was called for vidmoly assert mock_test_url.call_count >= 1 # Verify sendvid was also tried after vidmoly failed validation assert mock_sendvid.called @pytest.mark.asyncio async def test_cache_not_used_without_anime_page_url(self, downloader): """ Test that caching is not used when anime_page_url is not provided. """ with patch.object(downloader, '_extract_from_vidmoly') as mock_vidmoly, \ patch.object(downloader, '_extract_from_sendvid') as mock_sendvid, \ patch.object(downloader, '_test_video_url', new_callable=AsyncMock) as mock_test_url: # First request: no anime_page_url, vidmoly succeeds mock_vidmoly.return_value = ("http://vidmoly.com/video.mp4", "video.mp4") mock_test_url.return_value = True result1 = await downloader.get_download_link_with_fallback( "http://vidmoly.to/test" ) # Cache should be empty (no anime_page_url provided) assert len(downloader._working_players) == 0 # Second request: still no anime_page_url, should not use cache mock_vidmoly.reset_mock() result2 = await downloader.get_download_link_with_fallback( "http://vidmoly.to/test" ) # Vidmoly should still be called (no cache used) assert mock_vidmoly.call_count == 1