Task 4.5: Update Pydantic models to use key as primary identifier
- Updated AnimeSeriesResponse and SearchResult models in anime.py: - Changed 'id' field to 'key' as the primary series identifier - Added 'folder' as optional metadata field - Added field validator to normalize key to lowercase and strip whitespace - Added comprehensive docstrings explaining identifier usage - Updated DownloadItem and DownloadRequest models in download.py: - Added field validator for serie_id normalization (lowercase, stripped) - Improved documentation for serie_id (primary identifier) vs serie_folder (metadata) - Updated test_anime_models.py with comprehensive tests: - Tests for key normalization and whitespace stripping - Tests for folder as optional metadata - Reorganized tests into proper class structure - Updated test_download_models.py with validator tests: - Tests for serie_id normalization in DownloadItem - Tests for serie_id normalization in DownloadRequest All 885 tests pass.
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
"""Unit tests for anime Pydantic models.
|
||||
|
||||
This module tests all anime-related models including validation,
|
||||
serialization, and field constraints.
|
||||
"""
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from src.server.models.anime import (
|
||||
@@ -9,101 +15,139 @@ from src.server.models.anime import (
|
||||
)
|
||||
|
||||
|
||||
def test_episode_info_basic():
|
||||
ep = EpisodeInfo(episode_number=1, title="Pilot", duration_seconds=1500)
|
||||
assert ep.episode_number == 1
|
||||
assert ep.title == "Pilot"
|
||||
assert ep.duration_seconds == 1500
|
||||
assert ep.available is True
|
||||
class TestEpisodeInfo:
|
||||
"""Tests for EpisodeInfo model."""
|
||||
|
||||
def test_episode_info_basic(self):
|
||||
"""Test creating a basic episode info."""
|
||||
ep = EpisodeInfo(episode_number=1, title="Pilot", duration_seconds=1500)
|
||||
assert ep.episode_number == 1
|
||||
assert ep.title == "Pilot"
|
||||
assert ep.duration_seconds == 1500
|
||||
assert ep.available is True
|
||||
|
||||
def test_episode_info_without_optional_fields(self):
|
||||
"""Test episode info with only required fields."""
|
||||
ep = EpisodeInfo(episode_number=5)
|
||||
assert ep.episode_number == 5
|
||||
assert ep.title is None
|
||||
assert ep.duration_seconds is None
|
||||
assert ep.available is True
|
||||
|
||||
def test_invalid_episode_number(self):
|
||||
"""Test that episode number must be positive."""
|
||||
with pytest.raises(ValidationError):
|
||||
EpisodeInfo(episode_number=0)
|
||||
|
||||
|
||||
def test_missing_episode_count():
|
||||
m = MissingEpisodeInfo(from_episode=5, to_episode=7)
|
||||
assert m.count == 3
|
||||
class TestMissingEpisodeInfo:
|
||||
"""Tests for MissingEpisodeInfo model."""
|
||||
|
||||
def test_missing_episode_count(self):
|
||||
"""Test count property calculation."""
|
||||
m = MissingEpisodeInfo(from_episode=5, to_episode=7)
|
||||
assert m.count == 3
|
||||
|
||||
def test_single_missing_episode(self):
|
||||
"""Test count for single missing episode."""
|
||||
m = MissingEpisodeInfo(from_episode=5, to_episode=5)
|
||||
assert m.count == 1
|
||||
|
||||
|
||||
def test_anime_series_response():
|
||||
ep = EpisodeInfo(episode_number=1, title="Ep1")
|
||||
series = AnimeSeriesResponse(
|
||||
id="series-123",
|
||||
title="My Anime",
|
||||
episodes=[ep],
|
||||
total_episodes=12,
|
||||
)
|
||||
class TestAnimeSeriesResponse:
|
||||
"""Tests for AnimeSeriesResponse model."""
|
||||
|
||||
assert series.id == "series-123"
|
||||
assert series.episodes[0].title == "Ep1"
|
||||
def test_anime_series_response_with_key(self):
|
||||
"""Test creating series response with key as identifier."""
|
||||
ep = EpisodeInfo(episode_number=1, title="Ep1")
|
||||
series = AnimeSeriesResponse(
|
||||
key="attack-on-titan",
|
||||
title="Attack on Titan",
|
||||
folder="Attack on Titan (2013)",
|
||||
episodes=[ep],
|
||||
total_episodes=12,
|
||||
)
|
||||
|
||||
assert series.key == "attack-on-titan"
|
||||
assert series.title == "Attack on Titan"
|
||||
assert series.folder == "Attack on Titan (2013)"
|
||||
assert series.episodes[0].title == "Ep1"
|
||||
|
||||
def test_key_normalization(self):
|
||||
"""Test that key is normalized to lowercase."""
|
||||
series = AnimeSeriesResponse(
|
||||
key="ATTACK-ON-TITAN",
|
||||
title="Attack on Titan"
|
||||
)
|
||||
assert series.key == "attack-on-titan"
|
||||
|
||||
def test_key_whitespace_stripped(self):
|
||||
"""Test that key whitespace is stripped."""
|
||||
series = AnimeSeriesResponse(
|
||||
key=" attack-on-titan ",
|
||||
title="Attack on Titan"
|
||||
)
|
||||
assert series.key == "attack-on-titan"
|
||||
|
||||
def test_folder_is_optional(self):
|
||||
"""Test that folder is optional metadata."""
|
||||
series = AnimeSeriesResponse(
|
||||
key="my-anime",
|
||||
title="My Anime"
|
||||
)
|
||||
assert series.folder is None
|
||||
|
||||
|
||||
def test_search_request_validation():
|
||||
# valid
|
||||
req = SearchRequest(query="naruto", limit=5)
|
||||
assert req.query == "naruto"
|
||||
class TestSearchRequest:
|
||||
"""Tests for SearchRequest model."""
|
||||
|
||||
# invalid: empty query
|
||||
try:
|
||||
SearchRequest(query="", limit=5)
|
||||
raised = False
|
||||
except ValidationError:
|
||||
raised = True
|
||||
assert raised
|
||||
def test_search_request_validation(self):
|
||||
"""Test valid search request."""
|
||||
req = SearchRequest(query="naruto", limit=5)
|
||||
assert req.query == "naruto"
|
||||
assert req.limit == 5
|
||||
|
||||
def test_search_request_empty_query_rejected(self):
|
||||
"""Test that empty query is rejected."""
|
||||
with pytest.raises(ValidationError):
|
||||
SearchRequest(query="", limit=5)
|
||||
|
||||
def test_search_request_defaults(self):
|
||||
"""Test default values."""
|
||||
req = SearchRequest(query="test")
|
||||
assert req.limit == 10
|
||||
assert req.include_adult is False
|
||||
|
||||
|
||||
def test_search_result_optional_fields():
|
||||
res = SearchResult(id="s1", title="T1", snippet="snip", score=0.9)
|
||||
assert res.score == 0.9
|
||||
class TestSearchResult:
|
||||
"""Tests for SearchResult model."""
|
||||
|
||||
from pydantic import ValidationError
|
||||
def test_search_result_with_key(self):
|
||||
"""Test search result with key as identifier."""
|
||||
res = SearchResult(
|
||||
key="naruto",
|
||||
title="Naruto",
|
||||
folder="Naruto (2002)",
|
||||
snippet="A ninja story",
|
||||
score=0.9
|
||||
)
|
||||
assert res.key == "naruto"
|
||||
assert res.title == "Naruto"
|
||||
assert res.folder == "Naruto (2002)"
|
||||
assert res.score == 0.9
|
||||
|
||||
from src.server.models.anime import (
|
||||
AnimeSeriesResponse,
|
||||
EpisodeInfo,
|
||||
MissingEpisodeInfo,
|
||||
SearchRequest,
|
||||
SearchResult,
|
||||
)
|
||||
def test_key_normalization(self):
|
||||
"""Test that key is normalized to lowercase."""
|
||||
res = SearchResult(key="NARUTO", title="Naruto")
|
||||
assert res.key == "naruto"
|
||||
|
||||
def test_folder_is_optional(self):
|
||||
"""Test that folder is optional metadata."""
|
||||
res = SearchResult(key="test", title="Test")
|
||||
assert res.folder is None
|
||||
|
||||
def test_episode_info_basic():
|
||||
ep = EpisodeInfo(episode_number=1, title="Pilot", duration_seconds=1500)
|
||||
assert ep.episode_number == 1
|
||||
assert ep.title == "Pilot"
|
||||
assert ep.duration_seconds == 1500
|
||||
assert ep.available is True
|
||||
|
||||
|
||||
def test_missing_episode_count():
|
||||
m = MissingEpisodeInfo(from_episode=5, to_episode=7)
|
||||
assert m.count == 3
|
||||
|
||||
|
||||
def test_anime_series_response():
|
||||
ep = EpisodeInfo(episode_number=1, title="Ep1")
|
||||
series = AnimeSeriesResponse(
|
||||
id="series-123",
|
||||
title="My Anime",
|
||||
episodes=[ep],
|
||||
total_episodes=12,
|
||||
)
|
||||
|
||||
assert series.id == "series-123"
|
||||
assert series.episodes[0].title == "Ep1"
|
||||
|
||||
|
||||
def test_search_request_validation():
|
||||
# valid
|
||||
req = SearchRequest(query="naruto", limit=5)
|
||||
assert req.query == "naruto"
|
||||
|
||||
# invalid: empty query
|
||||
try:
|
||||
SearchRequest(query="", limit=5)
|
||||
raised = False
|
||||
except ValidationError:
|
||||
raised = True
|
||||
assert raised
|
||||
|
||||
|
||||
def test_search_result_optional_fields():
|
||||
res = SearchResult(id="s1", title="T1", snippet="snip", score=0.9)
|
||||
assert res.score == 0.9
|
||||
def test_optional_fields(self):
|
||||
"""Test optional fields."""
|
||||
res = SearchResult(key="s1", title="T1", snippet="snip", score=0.9)
|
||||
assert res.score == 0.9
|
||||
assert res.snippet == "snip"
|
||||
|
||||
Reference in New Issue
Block a user