""" Unit tests for YouTubeService """ from unittest.mock import AsyncMock, MagicMock, patch import pytest class TestYouTubeService: """Tests for YouTubeService class""" @pytest.fixture def service(self): """Create YouTubeService instance""" from unittest.mock import MagicMock from youtube_tui.services.youtube import YouTubeService with patch("youtube_cli.main.YouTubeCLI"): service = YouTubeService.__new__(YouTubeService) service.cli = MagicMock() # Set up format_duration to use the real implementation def real_format_duration(seconds): if not seconds: return "0:00" hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) secs = int(seconds % 60) if hours > 0: return f"{hours}:{minutes:02d}:{secs:02d}" else: return f"{minutes}:{secs:02d}" service.cli.format_duration = real_format_duration service.console = MagicMock() return service @pytest.fixture def mock_yt_dlp_result(self): """Mock yt-dlp search result""" return { "id": "abc123", "title": "Sample Video", "author": "Sample Channel", "channel": "Sample Channel", "channel_id": "channel123", "length": "10:30", "view_count": 150000, "upload_date": "20240115", "description": "A sample video description", "thumbnail": "https://i.ytimg.com/vi/abc123/hqdefault.jpg", "url": "https://www.youtube.com/watch?v=abc123", } @pytest.mark.asyncio async def test_search_videos_success(self, service, mock_yt_dlp_result): """Test successful video search""" from youtube_tui.models.video import Video # Setup mock service.cli.search_videos.return_value = [mock_yt_dlp_result] service._create_video_from_result = MagicMock( return_value=MagicMock(spec=Video) ) # Call the method - patch asyncio.to_thread since that's how it's imported with patch("asyncio.to_thread") as mock_to_thread: # The _search function returns a list of Video objects mock_to_thread.return_value = [mock_yt_dlp_result] results = await service.search_videos("test query") # Verify assert isinstance(results, list) @pytest.mark.asyncio async def test_search_videos_empty_result(self, service): """Test search with no results""" service.cli.search_videos.return_value = [] results = await service.search_videos("test query") assert results == [] @pytest.mark.asyncio async def test_search_videos_error(self, service): """Test search error handling""" service.cli.search_videos.side_effect = Exception("Network error") service.console.print = MagicMock() with pytest.raises(Exception): # SearchError wrapped in asyncio await service.search_videos("test query") @pytest.mark.asyncio async def test_search_videos_custom_page(self, service, mock_yt_dlp_result): """Test search with custom page parameter""" from unittest.mock import MagicMock from youtube_tui.models.video import Video service.cli.search_videos.return_value = [mock_yt_dlp_result] # Mock _create_video_from_result to return proper Video objects service._create_video_from_result = MagicMock( return_value=MagicMock( spec=Video, video_id="abc123", title="Sample Video", channel="Sample Channel", channel_id="channel123", duration="10:30", view_count="150000", upload_date="20240115", description="A sample video description", thumbnail_url="https://i.ytimg.com/vi/abc123/hqdefault.jpg", url="https://www.youtube.com/watch?v=abc123", is_short=False, ) ) # Mock asyncio.to_thread to execute the actual _search function async def mock_to_thread(func, *args, **kwargs): # Execute the function synchronously result = func() return result with patch("asyncio.to_thread", mock_to_thread): await service.search_videos("test query", page=2, per_page=10) # Note: per_page is ignored per the implementation service.cli.search_videos.assert_called_once_with( "test query", service.cli.config, 2, return_results=True ) @pytest.mark.asyncio async def test_download_video_success(self, service, mock_video): """Test successful video download""" service.cli.download_video.return_value = True result = await service.download_video(mock_video, "Music") assert result is True service.cli.download_video.assert_called_once() @pytest.mark.asyncio async def test_download_video_failure(self, service, mock_video): """Test video download failure""" service.cli.download_video.return_value = False service.console.print = MagicMock() result = await service.download_video(mock_video, "Music") assert result is False @pytest.mark.asyncio async def test_download_video_error(self, service, mock_video): """Test download error handling""" service.cli.download_video.side_effect = Exception("Download failed") service.console.print = MagicMock() with pytest.raises(Exception): # DownloadError wrapped in asyncio await service.download_video(mock_video, "Music") @pytest.mark.asyncio async def test_download_playlist_success(self, service, mock_video): """Test successful playlist download""" service.cli.download_playlist.return_value = True result = await service.download_playlist(mock_video, "Music") assert result is True service.cli.download_playlist.assert_called_once() @pytest.mark.asyncio async def test_download_playlist_error(self, service, mock_video): """Test playlist download error handling""" service.cli.download_playlist.side_effect = Exception("Playlist error") service.console.print = MagicMock() with pytest.raises(Exception): # DownloadError wrapped in asyncio await service.download_playlist(mock_video, "Music") @pytest.mark.asyncio async def test_get_categories_success(self, service): """Test getting categories""" service.cli.get_categories.return_value = ["Music", "Videos", "Movies"] categories = await service.get_categories() assert categories == ["Music", "Videos", "Movies"] @pytest.mark.asyncio async def test_is_video_downloaded_true(self, service): """Test checking downloaded video (exists)""" service.cli.is_video_downloaded.return_value = True result = await service.is_video_downloaded("video123") assert result is True @pytest.mark.asyncio async def test_is_video_downloaded_false(self, service): """Test checking downloaded video (not exists)""" service.cli.is_video_downloaded.return_value = False result = await service.is_video_downloaded("video123") assert result is False @pytest.mark.asyncio async def test_add_to_archive_success(self, service, mock_video): """Test adding video to archive""" service.cli.add_to_archive = MagicMock() await service.add_to_archive(mock_video) service.cli.add_to_archive.assert_called_once_with( { "url": mock_video.url, "id": mock_video.video_id, "title": mock_video.title, } ) @pytest.mark.asyncio async def test_add_to_archive_error(self, service, mock_video): """Test archive error handling""" service.cli.add_to_archive.side_effect = Exception("Archive error") service.console.print = MagicMock() with pytest.raises(Exception): # ArchiveError wrapped in asyncio await service.add_to_archive(mock_video) @pytest.mark.asyncio async def test_get_archive_success(self, service): """Test loading archive""" mock_archive = { "video123": { "url": "https://youtube.com/watch?v=video123", "id": "video123", } } service.cli.load_archive.return_value = mock_archive result = await service.get_archive() assert result == mock_archive @pytest.mark.asyncio async def test_get_downloaded_video_ids(self, service): """Test getting downloaded video IDs""" mock_archive = { "video1": {"url": "https://youtube.com/watch?v=video1"}, "video2": {"url": "https://youtube.com/watch?v=video2"}, } service.get_archive = AsyncMock(return_value=mock_archive) result = await service.get_downloaded_video_ids() assert result == {"video1", "video2"} @pytest.mark.asyncio async def test_remove_from_archive_success(self, service): """Test removing video from archive""" service.cli.load_archive.return_value = {"video123": {"url": "test"}} service.cli.save_archive = MagicMock() result = await service.remove_from_archive("video123") assert result is True @pytest.mark.asyncio async def test_remove_from_archive_not_found(self, service): """Test removing non-existent video from archive""" service.cli.load_archive.return_value = {"video123": {"url": "test"}} service.cli.save_archive = MagicMock() result = await service.remove_from_archive("video999") assert result is False service.cli.save_archive.assert_not_called() @pytest.mark.asyncio async def test_remove_from_archive_error(self, service): """Test archive removal error handling""" service.cli.load_archive.side_effect = Exception("Archive error") service.cli.save_archive = MagicMock() result = await service.remove_from_archive("video123") assert result is False def test_create_video_from_result(self, service, mock_yt_dlp_result): """Test creating Video from yt-dlp result""" result = service._create_video_from_result(mock_yt_dlp_result) assert result.video_id == "abc123" assert result.title == "Sample Video" assert result.channel == "Sample Channel" assert result.duration == "10:30" assert result.view_count == "150000" def test_create_video_from_result_channel_field(self, service): """Test creating Video with channel field instead of author""" result_data = { "id": "abc123", "title": "Sample Video", "channel": "Sample Channel", "channel_id": "channel123", "length": "5:00", "view_count": 1000, "upload_date": "20240101", "description": "Test", "thumbnail": "https://example.com/thumb.jpg", "url": "https://youtube.com/watch?v=abc123", } result = service._create_video_from_result(result_data) assert result.channel == "Sample Channel" def test_format_duration(self, service): """Test duration formatting""" result = service.format_duration(3665) # 3665 seconds = 1 hour, 1 minute, 5 seconds # Format should be HH:MM:SS or MM:SS assert result in ["1:01:05", "01:01:05"] def test_format_duration_minutes(self, service): """Test duration formatting for minutes""" result = service.format_duration(125) assert result == "2:05" def test_format_duration_seconds(self, service): """Test duration formatting for seconds only""" result = service.format_duration(30) assert result == "0:30"