Aniworld/tests/unit/test_download_models.py
2025-11-15 09:11:02 +01:00

553 lines
18 KiB
Python

"""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, timezone
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 uppercase strings."""
for priority in DownloadPriority:
assert isinstance(priority.value, str)
assert priority.value.isupper()
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.now(timezone.utc)
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.now(timezone.utc)
item = DownloadItem(
id="test_id",
serie_id="serie_id",
serie_name="Test",
episode=episode
)
after = datetime.now(timezone.utc)
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