"""Unit tests for download queue Pydantic models. This module tests all download-related models including validation, serialization, and field constraints. """ from datetime import datetime, timedelta import pytest from pydantic import ValidationError from src.server.models.download import ( DownloadItem, DownloadPriority, DownloadProgress, DownloadRequest, DownloadResponse, DownloadStatus, EpisodeIdentifier, QueueOperationRequest, QueueReorderRequest, QueueStats, QueueStatus, QueueStatusResponse, ) class TestDownloadStatus: """Test DownloadStatus enum.""" def test_all_statuses_exist(self): """Test that all expected statuses are defined.""" assert DownloadStatus.PENDING == "pending" assert DownloadStatus.DOWNLOADING == "downloading" assert DownloadStatus.PAUSED == "paused" assert DownloadStatus.COMPLETED == "completed" assert DownloadStatus.FAILED == "failed" assert DownloadStatus.CANCELLED == "cancelled" def test_status_values(self): """Test that status values are lowercase strings.""" for status in DownloadStatus: assert isinstance(status.value, str) assert status.value.islower() class TestDownloadPriority: """Test DownloadPriority enum.""" def test_all_priorities_exist(self): """Test that all expected priorities are defined.""" assert DownloadPriority.LOW == "low" assert DownloadPriority.NORMAL == "normal" assert DownloadPriority.HIGH == "high" def test_priority_values(self): """Test that priority values are lowercase strings.""" for priority in DownloadPriority: assert isinstance(priority.value, str) assert priority.value.islower() class TestEpisodeIdentifier: """Test EpisodeIdentifier model.""" def test_valid_episode_identifier(self): """Test creating a valid episode identifier.""" episode = EpisodeIdentifier( season=1, episode=5, title="Test Episode" ) assert episode.season == 1 assert episode.episode == 5 assert episode.title == "Test Episode" def test_episode_identifier_without_title(self): """Test creating episode identifier without title.""" episode = EpisodeIdentifier(season=2, episode=10) assert episode.season == 2 assert episode.episode == 10 assert episode.title is None def test_invalid_season_number(self): """Test that season must be positive.""" with pytest.raises(ValidationError) as exc_info: EpisodeIdentifier(season=0, episode=1) errors = exc_info.value.errors() assert any("season" in str(e["loc"]) for e in errors) def test_invalid_episode_number(self): """Test that episode must be positive.""" with pytest.raises(ValidationError) as exc_info: EpisodeIdentifier(season=1, episode=0) errors = exc_info.value.errors() assert any("episode" in str(e["loc"]) for e in errors) def test_negative_season_rejected(self): """Test that negative season is rejected.""" with pytest.raises(ValidationError): EpisodeIdentifier(season=-1, episode=1) def test_negative_episode_rejected(self): """Test that negative episode is rejected.""" with pytest.raises(ValidationError): EpisodeIdentifier(season=1, episode=-1) class TestDownloadProgress: """Test DownloadProgress model.""" def test_valid_progress(self): """Test creating valid progress information.""" progress = DownloadProgress( percent=45.5, downloaded_mb=100.0, total_mb=220.0, speed_mbps=5.5, eta_seconds=120 ) assert progress.percent == 45.5 assert progress.downloaded_mb == 100.0 assert progress.total_mb == 220.0 assert progress.speed_mbps == 5.5 assert progress.eta_seconds == 120 def test_progress_defaults(self): """Test default values for progress.""" progress = DownloadProgress() assert progress.percent == 0.0 assert progress.downloaded_mb == 0.0 assert progress.total_mb is None assert progress.speed_mbps is None assert progress.eta_seconds is None def test_percent_range_validation(self): """Test that percent must be between 0 and 100.""" # Valid boundary values DownloadProgress(percent=0.0) DownloadProgress(percent=100.0) # Invalid values with pytest.raises(ValidationError): DownloadProgress(percent=-0.1) with pytest.raises(ValidationError): DownloadProgress(percent=100.1) def test_negative_downloaded_mb_rejected(self): """Test that negative downloaded_mb is rejected.""" with pytest.raises(ValidationError): DownloadProgress(downloaded_mb=-1.0) def test_negative_total_mb_rejected(self): """Test that negative total_mb is rejected.""" with pytest.raises(ValidationError): DownloadProgress(total_mb=-1.0) def test_negative_speed_rejected(self): """Test that negative speed is rejected.""" with pytest.raises(ValidationError): DownloadProgress(speed_mbps=-1.0) def test_negative_eta_rejected(self): """Test that negative ETA is rejected.""" with pytest.raises(ValidationError): DownloadProgress(eta_seconds=-1) class TestDownloadItem: """Test DownloadItem model.""" def test_valid_download_item(self): """Test creating a valid download item.""" episode = EpisodeIdentifier(season=1, episode=5) item = DownloadItem( id="download_123", serie_id="serie_456", serie_name="Test Series", episode=episode, status=DownloadStatus.PENDING, priority=DownloadPriority.HIGH ) assert item.id == "download_123" assert item.serie_id == "serie_456" assert item.serie_name == "Test Series" assert item.episode == episode assert item.status == DownloadStatus.PENDING assert item.priority == DownloadPriority.HIGH def test_download_item_defaults(self): """Test default values for download item.""" episode = EpisodeIdentifier(season=1, episode=1) item = DownloadItem( id="test_id", serie_id="serie_id", serie_name="Test", episode=episode ) assert item.status == DownloadStatus.PENDING assert item.priority == DownloadPriority.NORMAL assert item.started_at is None assert item.completed_at is None assert item.progress is None assert item.error is None assert item.retry_count == 0 assert item.source_url is None def test_download_item_with_progress(self): """Test download item with progress information.""" episode = EpisodeIdentifier(season=1, episode=1) progress = DownloadProgress(percent=50.0, downloaded_mb=100.0) item = DownloadItem( id="test_id", serie_id="serie_id", serie_name="Test", episode=episode, progress=progress ) assert item.progress is not None assert item.progress.percent == 50.0 def test_download_item_with_timestamps(self): """Test download item with timestamp fields.""" episode = EpisodeIdentifier(season=1, episode=1) now = datetime.utcnow() item = DownloadItem( id="test_id", serie_id="serie_id", serie_name="Test", episode=episode, started_at=now, completed_at=now + timedelta(minutes=5) ) assert item.started_at == now assert item.completed_at == now + timedelta(minutes=5) def test_empty_serie_name_rejected(self): """Test that empty serie name is rejected.""" episode = EpisodeIdentifier(season=1, episode=1) with pytest.raises(ValidationError): DownloadItem( id="test_id", serie_id="serie_id", serie_name="", episode=episode ) def test_negative_retry_count_rejected(self): """Test that negative retry count is rejected.""" episode = EpisodeIdentifier(season=1, episode=1) with pytest.raises(ValidationError): DownloadItem( id="test_id", serie_id="serie_id", serie_name="Test", episode=episode, retry_count=-1 ) def test_added_at_auto_generated(self): """Test that added_at is automatically set.""" episode = EpisodeIdentifier(season=1, episode=1) before = datetime.utcnow() item = DownloadItem( id="test_id", serie_id="serie_id", serie_name="Test", episode=episode ) after = datetime.utcnow() assert before <= item.added_at <= after class TestQueueStatus: """Test QueueStatus model.""" def test_valid_queue_status(self): """Test creating valid queue status.""" episode = EpisodeIdentifier(season=1, episode=1) item = DownloadItem( id="test_id", serie_id="serie_id", serie_name="Test", episode=episode ) status = QueueStatus( is_running=True, is_paused=False, active_downloads=[item], pending_queue=[item], completed_downloads=[], failed_downloads=[] ) assert status.is_running is True assert status.is_paused is False assert len(status.active_downloads) == 1 assert len(status.pending_queue) == 1 def test_queue_status_defaults(self): """Test default values for queue status.""" status = QueueStatus() assert status.is_running is False assert status.is_paused is False assert status.active_downloads == [] assert status.pending_queue == [] assert status.completed_downloads == [] assert status.failed_downloads == [] class TestQueueStats: """Test QueueStats model.""" def test_valid_queue_stats(self): """Test creating valid queue statistics.""" stats = QueueStats( total_items=10, pending_count=3, active_count=2, completed_count=4, failed_count=1, total_downloaded_mb=500.5, average_speed_mbps=5.0, estimated_time_remaining=120 ) assert stats.total_items == 10 assert stats.pending_count == 3 assert stats.active_count == 2 assert stats.completed_count == 4 assert stats.failed_count == 1 assert stats.total_downloaded_mb == 500.5 assert stats.average_speed_mbps == 5.0 assert stats.estimated_time_remaining == 120 def test_queue_stats_defaults(self): """Test default values for queue stats.""" stats = QueueStats() assert stats.total_items == 0 assert stats.pending_count == 0 assert stats.active_count == 0 assert stats.completed_count == 0 assert stats.failed_count == 0 assert stats.total_downloaded_mb == 0.0 assert stats.average_speed_mbps is None assert stats.estimated_time_remaining is None def test_negative_counts_rejected(self): """Test that negative counts are rejected.""" with pytest.raises(ValidationError): QueueStats(total_items=-1) with pytest.raises(ValidationError): QueueStats(pending_count=-1) with pytest.raises(ValidationError): QueueStats(active_count=-1) with pytest.raises(ValidationError): QueueStats(completed_count=-1) with pytest.raises(ValidationError): QueueStats(failed_count=-1) def test_negative_speed_rejected(self): """Test that negative speed is rejected.""" with pytest.raises(ValidationError): QueueStats(average_speed_mbps=-1.0) def test_negative_eta_rejected(self): """Test that negative ETA is rejected.""" with pytest.raises(ValidationError): QueueStats(estimated_time_remaining=-1) class TestDownloadRequest: """Test DownloadRequest model.""" def test_valid_download_request(self): """Test creating a valid download request.""" episode1 = EpisodeIdentifier(season=1, episode=1) episode2 = EpisodeIdentifier(season=1, episode=2) request = DownloadRequest( serie_id="serie_123", serie_name="Test Series", episodes=[episode1, episode2], priority=DownloadPriority.HIGH ) assert request.serie_id == "serie_123" assert request.serie_name == "Test Series" assert len(request.episodes) == 2 assert request.priority == DownloadPriority.HIGH def test_download_request_default_priority(self): """Test default priority for download request.""" episode = EpisodeIdentifier(season=1, episode=1) request = DownloadRequest( serie_id="serie_123", serie_name="Test Series", episodes=[episode] ) assert request.priority == DownloadPriority.NORMAL def test_empty_episodes_list_allowed(self): """Test that empty episodes list is allowed at model level (endpoint validates).""" # Empty list is now allowed at model level; endpoint validates request = DownloadRequest( serie_id="serie_123", serie_name="Test Series", episodes=[] ) assert request.episodes == [] def test_empty_serie_name_rejected(self): """Test that empty serie name is rejected.""" episode = EpisodeIdentifier(season=1, episode=1) with pytest.raises(ValidationError): DownloadRequest( serie_id="serie_123", serie_name="", episodes=[episode] ) class TestDownloadResponse: """Test DownloadResponse model.""" def test_valid_download_response(self): """Test creating a valid download response.""" response = DownloadResponse( status="success", message="Added 2 episodes to queue", added_items=["item1", "item2"], failed_items=[] ) assert response.status == "success" assert response.message == "Added 2 episodes to queue" assert len(response.added_items) == 2 assert response.failed_items == [] def test_download_response_defaults(self): """Test default values for download response.""" response = DownloadResponse( status="success", message="Test message" ) assert response.added_items == [] assert response.failed_items == [] class TestQueueOperationRequest: """Test QueueOperationRequest model.""" def test_valid_operation_request(self): """Test creating a valid operation request.""" request = QueueOperationRequest( item_ids=["item1", "item2", "item3"] ) assert len(request.item_ids) == 3 assert "item1" in request.item_ids def test_empty_item_ids_allowed(self): """Test that empty item_ids list is allowed at model level (endpoint validates).""" # Empty list is now allowed at model level; endpoint validates request = QueueOperationRequest(item_ids=[]) assert request.item_ids == [] class TestQueueReorderRequest: """Test QueueReorderRequest model.""" def test_valid_reorder_request(self): """Test creating a valid reorder request.""" request = QueueReorderRequest( item_id="item_123", new_position=5 ) assert request.item_id == "item_123" assert request.new_position == 5 def test_zero_position_allowed(self): """Test that position zero is allowed.""" request = QueueReorderRequest( item_id="item_123", new_position=0 ) assert request.new_position == 0 def test_negative_position_rejected(self): """Test that negative position is rejected.""" with pytest.raises(ValidationError): QueueReorderRequest( item_id="item_123", new_position=-1 ) class TestQueueStatusResponse: """Test QueueStatusResponse model.""" def test_valid_status_response(self): """Test creating a valid status response.""" status = QueueStatus() stats = QueueStats() response = QueueStatusResponse( status=status, statistics=stats ) assert response.status is not None assert response.statistics is not None class TestModelSerialization: """Test model serialization and deserialization.""" def test_download_item_to_dict(self): """Test serializing download item to dict.""" episode = EpisodeIdentifier(season=1, episode=5, title="Test") item = DownloadItem( id="test_id", serie_id="serie_id", serie_name="Test Series", episode=episode ) data = item.model_dump() assert data["id"] == "test_id" assert data["serie_name"] == "Test Series" assert data["episode"]["season"] == 1 assert data["episode"]["episode"] == 5 def test_download_item_from_dict(self): """Test deserializing download item from dict.""" data = { "id": "test_id", "serie_id": "serie_id", "serie_name": "Test Series", "episode": { "season": 1, "episode": 5, "title": "Test Episode" } } item = DownloadItem(**data) assert item.id == "test_id" assert item.serie_name == "Test Series" assert item.episode.season == 1 def test_queue_status_to_json(self): """Test serializing queue status to JSON.""" status = QueueStatus(is_running=True) json_str = status.model_dump_json() assert '"is_running":true' in json_str.lower() def test_queue_stats_from_json(self): """Test deserializing queue stats from JSON.""" json_str = '{"total_items": 5, "pending_count": 3}' stats = QueueStats.model_validate_json(json_str) assert stats.total_items == 5 assert stats.pending_count == 3