341 lines
12 KiB
Python
341 lines
12 KiB
Python
"""
|
|
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"
|