diff --git a/instructions.md b/instructions.md index 7054948..cbb54f4 100644 --- a/instructions.md +++ b/instructions.md @@ -175,10 +175,10 @@ For each task completed: **Success Criteria:** -- [ ] `key` property has validation preventing empty values -- [ ] Docstrings clearly state `key` is the unique identifier -- [ ] `folder` is documented as metadata only -- [ ] All existing tests for `Serie` still pass +- [x] `key` property has validation preventing empty values +- [x] Docstrings clearly state `key` is the unique identifier +- [x] `folder` is documented as metadata only +- [x] All existing tests for `Serie` still pass **Test Command:** @@ -186,6 +186,25 @@ For each task completed: conda run -n AniWorld python -m pytest tests/unit/test_anime_models.py -v ``` +**Status:** ✅ COMPLETED + +**Implementation Details:** + +- Added comprehensive validation in `__init__` to ensure `key` is never empty or whitespace +- Added validation in `key` setter to prevent setting empty values +- Key values are automatically stripped of whitespace +- Added detailed docstrings explaining `key` as the unique identifier +- Documented `folder` as "filesystem folder name (metadata only, not used for lookups)" +- Created comprehensive test suite in `tests/unit/test_serie_class.py` with 16 tests covering: + - Key validation (empty, whitespace, valid values) + - Key setter validation + - Whitespace stripping behavior + - String representation + - Serialization/deserialization (to_dict/from_dict) + - File save/load functionality + - Documentation completeness +- All 56 Serie-related tests pass successfully + --- #### Task 1.2: Update SerieList to Use Key for Lookups diff --git a/src/core/entities/series.py b/src/core/entities/series.py index 19ecf73..2a45524 100644 --- a/src/core/entities/series.py +++ b/src/core/entities/series.py @@ -1,23 +1,65 @@ import json + class Serie: + """ + Represents an anime series with metadata and episode information. + + The `key` property is the unique identifier for the series (provider-assigned, URL-safe). + The `folder` property is the filesystem folder name (metadata only, not used for lookups). + + Args: + key: Unique series identifier from provider (e.g., "attack-on-titan"). Cannot be empty. + name: Display name of the series + site: Provider site URL + folder: Filesystem folder name (metadata only, e.g., "Attack on Titan (2013)") + episodeDict: Dictionary mapping season numbers to lists of episode numbers + + Raises: + ValueError: If key is None or empty string + """ + def __init__(self, key: str, name: str, site: str, folder: str, episodeDict: dict[int, list[int]]): - self._key = key + if not key or not key.strip(): + raise ValueError("Serie key cannot be None or empty") + + self._key = key.strip() self._name = name self._site = site self._folder = folder self._episodeDict = episodeDict + def __str__(self): """String representation of Serie object""" return f"Serie(key='{self.key}', name='{self.name}', site='{self.site}', folder='{self.folder}', episodeDict={self.episodeDict})" @property def key(self) -> str: + """ + Unique series identifier (primary identifier for all lookups). + + This is the provider-assigned, URL-safe identifier used throughout the application + for series identification, lookups, and operations. + + Returns: + str: The unique series key + """ return self._key @key.setter def key(self, value: str): - self._key = value + """ + Set the unique series identifier. + + Args: + value: New key value + + Raises: + ValueError: If value is None or empty string + """ + if not value or not value.strip(): + raise ValueError("Serie key cannot be None or empty") + self._key = value.strip() @property def name(self) -> str: @@ -37,10 +79,26 @@ class Serie: @property def folder(self) -> str: + """ + Filesystem folder name (metadata only, not used for lookups). + + This property contains the local directory name where the series + files are stored. It should NOT be used as an identifier for + series lookups - use `key` instead. + + Returns: + str: The filesystem folder name + """ return self._folder @folder.setter def folder(self, value: str): + """ + Set the filesystem folder name. + + Args: + value: Folder name for the series + """ self._folder = value @property diff --git a/tests/unit/test_serie_class.py b/tests/unit/test_serie_class.py new file mode 100644 index 0000000..5f86cc1 --- /dev/null +++ b/tests/unit/test_serie_class.py @@ -0,0 +1,244 @@ +""" +Unit tests for Serie class to verify key validation and identifier usage. +""" + +import json +import os +import tempfile + +import pytest + +from src.core.entities.series import Serie + + +class TestSerieValidation: + """Test Serie class validation logic.""" + + def test_serie_creation_with_valid_key(self): + """Test creating Serie with valid key.""" + serie = Serie( + key="attack-on-titan", + name="Attack on Titan", + site="https://aniworld.to/anime/stream/attack-on-titan", + folder="Attack on Titan (2013)", + episodeDict={1: [1, 2, 3], 2: [1, 2]} + ) + + assert serie.key == "attack-on-titan" + assert serie.name == "Attack on Titan" + assert serie.site == "https://aniworld.to/anime/stream/attack-on-titan" + assert serie.folder == "Attack on Titan (2013)" + assert serie.episodeDict == {1: [1, 2, 3], 2: [1, 2]} + + def test_serie_creation_with_empty_key_raises_error(self): + """Test that creating Serie with empty key raises ValueError.""" + with pytest.raises(ValueError, match="key cannot be None or empty"): + Serie( + key="", + name="Test Series", + site="https://example.com", + folder="Test Folder", + episodeDict={1: [1]} + ) + + def test_serie_creation_with_whitespace_key_raises_error(self): + """Test that creating Serie with whitespace-only key raises error.""" + with pytest.raises(ValueError, match="key cannot be None or empty"): + Serie( + key=" ", + name="Test Series", + site="https://example.com", + folder="Test Folder", + episodeDict={1: [1]} + ) + + def test_serie_key_is_stripped(self): + """Test that Serie key is stripped of whitespace.""" + serie = Serie( + key=" attack-on-titan ", + name="Attack on Titan", + site="https://example.com", + folder="Attack on Titan (2013)", + episodeDict={1: [1]} + ) + + assert serie.key == "attack-on-titan" + + def test_serie_key_setter_with_valid_value(self): + """Test setting key property with valid value.""" + serie = Serie( + key="initial-key", + name="Test", + site="https://example.com", + folder="Test Folder", + episodeDict={1: [1]} + ) + + serie.key = "new-key" + assert serie.key == "new-key" + + def test_serie_key_setter_with_empty_value_raises_error(self): + """Test that setting key to empty string raises ValueError.""" + serie = Serie( + key="initial-key", + name="Test", + site="https://example.com", + folder="Test Folder", + episodeDict={1: [1]} + ) + + with pytest.raises(ValueError, match="key cannot be None or empty"): + serie.key = "" + + def test_serie_key_setter_with_whitespace_raises_error(self): + """Test that setting key to whitespace raises ValueError.""" + serie = Serie( + key="initial-key", + name="Test", + site="https://example.com", + folder="Test Folder", + episodeDict={1: [1]} + ) + + with pytest.raises(ValueError, match="key cannot be None or empty"): + serie.key = " " + + def test_serie_key_setter_strips_whitespace(self): + """Test that key setter strips whitespace.""" + serie = Serie( + key="initial-key", + name="Test", + site="https://example.com", + folder="Test Folder", + episodeDict={1: [1]} + ) + + serie.key = " new-key " + assert serie.key == "new-key" + + +class TestSerieProperties: + """Test Serie class properties and methods.""" + + def test_serie_str_representation(self): + """Test string representation of Serie.""" + serie = Serie( + key="test-key", + name="Test Series", + site="https://example.com", + folder="Test Folder", + episodeDict={1: [1, 2]} + ) + + str_repr = str(serie) + assert "key='test-key'" in str_repr + assert "name='Test Series'" in str_repr + assert "folder='Test Folder'" in str_repr + + def test_serie_to_dict(self): + """Test conversion of Serie to dictionary.""" + serie = Serie( + key="test-key", + name="Test Series", + site="https://example.com", + folder="Test Folder", + episodeDict={1: [1, 2], 2: [1, 2, 3]} + ) + + data = serie.to_dict() + + assert data["key"] == "test-key" + assert data["name"] == "Test Series" + assert data["site"] == "https://example.com" + assert data["folder"] == "Test Folder" + assert "1" in data["episodeDict"] + assert data["episodeDict"]["1"] == [1, 2] + + def test_serie_from_dict(self): + """Test creating Serie from dictionary.""" + data = { + "key": "test-key", + "name": "Test Series", + "site": "https://example.com", + "folder": "Test Folder", + "episodeDict": {"1": [1, 2], "2": [1, 2, 3]} + } + + serie = Serie.from_dict(data) + + assert serie.key == "test-key" + assert serie.name == "Test Series" + assert serie.folder == "Test Folder" + assert serie.episodeDict == {1: [1, 2], 2: [1, 2, 3]} + + def test_serie_save_and_load_from_file(self): + """Test saving and loading Serie from file.""" + serie = Serie( + key="test-key", + name="Test Series", + site="https://example.com", + folder="Test Folder", + episodeDict={1: [1, 2, 3]} + ) + + # Create temporary file + with tempfile.NamedTemporaryFile( + mode='w', + delete=False, + suffix='.json' + ) as f: + temp_filename = f.name + + try: + # Save to file + serie.save_to_file(temp_filename) + + # Load from file + loaded_serie = Serie.load_from_file(temp_filename) + + # Verify all properties match + assert loaded_serie.key == serie.key + assert loaded_serie.name == serie.name + assert loaded_serie.site == serie.site + assert loaded_serie.folder == serie.folder + assert loaded_serie.episodeDict == serie.episodeDict + finally: + # Cleanup + if os.path.exists(temp_filename): + os.remove(temp_filename) + + def test_serie_folder_is_mutable(self): + """Test that folder property can be changed (it's metadata only).""" + serie = Serie( + key="test-key", + name="Test", + site="https://example.com", + folder="Old Folder", + episodeDict={1: [1]} + ) + + serie.folder = "New Folder" + assert serie.folder == "New Folder" + # Key should remain unchanged + assert serie.key == "test-key" + + +class TestSerieDocumentation: + """Test that Serie class has proper documentation.""" + + def test_serie_class_has_docstring(self): + """Test that Serie class has a docstring.""" + assert Serie.__doc__ is not None + assert "unique identifier" in Serie.__doc__.lower() + + def test_key_property_has_docstring(self): + """Test that key property has descriptive docstring.""" + assert Serie.key.fget.__doc__ is not None + assert "unique" in Serie.key.fget.__doc__.lower() + assert "identifier" in Serie.key.fget.__doc__.lower() + + def test_folder_property_has_docstring(self): + """Test that folder property documents it's metadata only.""" + assert Serie.folder.fget.__doc__ is not None + assert "metadata" in Serie.folder.fget.__doc__.lower() + assert "not used for lookups" in Serie.folder.fget.__doc__.lower()