219 lines
7.6 KiB
Python
219 lines
7.6 KiB
Python
"""Unit tests for base_provider.py - Abstract base class and interface contracts."""
|
|
|
|
from abc import ABC
|
|
from typing import Any, Dict, List
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from src.core.providers.base_provider import Loader
|
|
|
|
|
|
class TestLoaderAbstractInterface:
|
|
"""Test that Loader defines the correct abstract interface."""
|
|
|
|
def test_loader_is_abstract_class(self):
|
|
"""Loader should be an abstract class and not directly instantiable."""
|
|
assert issubclass(Loader, ABC)
|
|
with pytest.raises(TypeError):
|
|
Loader()
|
|
|
|
def test_loader_defines_search_method(self):
|
|
"""Loader must define abstract search method."""
|
|
assert hasattr(Loader, "search")
|
|
assert getattr(Loader.search, "__isabstractmethod__", False)
|
|
|
|
def test_loader_defines_is_language_method(self):
|
|
"""Loader must define abstract is_language method."""
|
|
assert hasattr(Loader, "is_language")
|
|
assert getattr(Loader.is_language, "__isabstractmethod__", False)
|
|
|
|
def test_loader_defines_download_method(self):
|
|
"""Loader must define abstract download method."""
|
|
assert hasattr(Loader, "download")
|
|
assert getattr(Loader.download, "__isabstractmethod__", False)
|
|
|
|
def test_loader_defines_get_site_key_method(self):
|
|
"""Loader must define abstract get_site_key method."""
|
|
assert hasattr(Loader, "get_site_key")
|
|
assert getattr(Loader.get_site_key, "__isabstractmethod__", False)
|
|
|
|
def test_loader_defines_get_title_method(self):
|
|
"""Loader must define abstract get_title method."""
|
|
assert hasattr(Loader, "get_title")
|
|
assert getattr(Loader.get_title, "__isabstractmethod__", False)
|
|
|
|
def test_loader_defines_get_season_episode_count_method(self):
|
|
"""Loader must define abstract get_season_episode_count method."""
|
|
assert hasattr(Loader, "get_season_episode_count")
|
|
assert getattr(
|
|
Loader.get_season_episode_count, "__isabstractmethod__", False
|
|
)
|
|
|
|
def test_loader_defines_subscribe_download_progress(self):
|
|
"""Loader must define abstract subscribe_download_progress method."""
|
|
assert hasattr(Loader, "subscribe_download_progress")
|
|
assert getattr(
|
|
Loader.subscribe_download_progress, "__isabstractmethod__", False
|
|
)
|
|
|
|
def test_loader_defines_unsubscribe_download_progress(self):
|
|
"""Loader must define abstract unsubscribe_download_progress method."""
|
|
assert hasattr(Loader, "unsubscribe_download_progress")
|
|
assert getattr(
|
|
Loader.unsubscribe_download_progress, "__isabstractmethod__", False
|
|
)
|
|
|
|
|
|
class ConcreteLoader(Loader):
|
|
"""Minimal concrete implementation for testing inheritance."""
|
|
|
|
def subscribe_download_progress(self, handler):
|
|
pass
|
|
|
|
def unsubscribe_download_progress(self, handler):
|
|
pass
|
|
|
|
def search(self, word: str) -> List[Dict[str, Any]]:
|
|
return [{"title": word}]
|
|
|
|
def is_language(
|
|
self,
|
|
season: int,
|
|
episode: int,
|
|
key: str,
|
|
language: str = "German Dub",
|
|
) -> bool:
|
|
return True
|
|
|
|
def download(
|
|
self,
|
|
base_directory: str,
|
|
serie_folder: str,
|
|
season: int,
|
|
episode: int,
|
|
key: str,
|
|
language: str = "German Dub",
|
|
) -> bool:
|
|
return True
|
|
|
|
def get_site_key(self) -> str:
|
|
return "test.provider"
|
|
|
|
def get_title(self, key: str) -> str:
|
|
return f"Title for {key}"
|
|
|
|
def get_season_episode_count(self, slug: str) -> Dict[int, int]:
|
|
return {1: 12, 2: 24}
|
|
|
|
|
|
class TestLoaderInheritance:
|
|
"""Test concrete implementations of the Loader interface."""
|
|
|
|
def test_concrete_loader_can_be_instantiated(self):
|
|
"""A fully implemented subclass should instantiate without error."""
|
|
loader = ConcreteLoader()
|
|
assert isinstance(loader, Loader)
|
|
|
|
def test_concrete_loader_search(self):
|
|
"""Concrete search should return a list of dicts."""
|
|
loader = ConcreteLoader()
|
|
result = loader.search("Naruto")
|
|
assert isinstance(result, list)
|
|
assert result[0]["title"] == "Naruto"
|
|
|
|
def test_concrete_loader_is_language(self):
|
|
"""Concrete is_language should return bool."""
|
|
loader = ConcreteLoader()
|
|
assert loader.is_language(1, 1, "naruto") is True
|
|
|
|
def test_concrete_loader_is_language_default_param(self):
|
|
"""is_language should default to 'German Dub' language."""
|
|
loader = ConcreteLoader()
|
|
result = loader.is_language(1, 1, "naruto")
|
|
assert isinstance(result, bool)
|
|
|
|
def test_concrete_loader_download(self):
|
|
"""Concrete download should return bool."""
|
|
loader = ConcreteLoader()
|
|
result = loader.download("/base", "folder", 1, 1, "naruto")
|
|
assert result is True
|
|
|
|
def test_concrete_loader_get_site_key(self):
|
|
"""Concrete get_site_key should return a string."""
|
|
loader = ConcreteLoader()
|
|
assert loader.get_site_key() == "test.provider"
|
|
|
|
def test_concrete_loader_get_title(self):
|
|
"""Concrete get_title should return title string."""
|
|
loader = ConcreteLoader()
|
|
assert loader.get_title("naruto") == "Title for naruto"
|
|
|
|
def test_concrete_loader_get_season_episode_count(self):
|
|
"""Concrete get_season_episode_count should return dict."""
|
|
loader = ConcreteLoader()
|
|
result = loader.get_season_episode_count("naruto")
|
|
assert isinstance(result, dict)
|
|
assert result[1] == 12
|
|
assert result[2] == 24
|
|
|
|
def test_concrete_loader_subscribe_download_progress(self):
|
|
"""subscribe_download_progress should accept handler without error."""
|
|
loader = ConcreteLoader()
|
|
handler = MagicMock()
|
|
loader.subscribe_download_progress(handler)
|
|
|
|
def test_concrete_loader_unsubscribe_download_progress(self):
|
|
"""unsubscribe_download_progress should accept handler without error."""
|
|
loader = ConcreteLoader()
|
|
handler = MagicMock()
|
|
loader.unsubscribe_download_progress(handler)
|
|
|
|
|
|
class IncompleteLoader(Loader):
|
|
"""Incomplete implementation missing some abstract methods."""
|
|
|
|
def search(self, word: str) -> List[Dict[str, Any]]:
|
|
return []
|
|
|
|
def is_language(self, season, episode, key, language="German Dub"):
|
|
return False
|
|
|
|
# Deliberately omit download, get_site_key, etc.
|
|
|
|
|
|
class TestIncompleteImplementation:
|
|
"""Test that incomplete implementations cannot be instantiated."""
|
|
|
|
def test_incomplete_loader_raises_type_error(self):
|
|
"""Loader subclass missing abstract methods cannot be instantiated."""
|
|
with pytest.raises(TypeError):
|
|
IncompleteLoader()
|
|
|
|
|
|
class TestLoaderMethodSignatures:
|
|
"""Test method signatures match the expected contract."""
|
|
|
|
def test_search_returns_list(self):
|
|
"""search() should return List[Dict[str, Any]]."""
|
|
loader = ConcreteLoader()
|
|
result = loader.search("test")
|
|
assert isinstance(result, list)
|
|
for item in result:
|
|
assert isinstance(item, dict)
|
|
|
|
def test_download_returns_bool(self):
|
|
"""download() should return bool."""
|
|
loader = ConcreteLoader()
|
|
result = loader.download("/dir", "folder", 1, 1, "key")
|
|
assert isinstance(result, bool)
|
|
|
|
def test_get_season_episode_count_returns_dict_int_int(self):
|
|
"""get_season_episode_count() should return Dict[int, int]."""
|
|
loader = ConcreteLoader()
|
|
result = loader.get_season_episode_count("slug")
|
|
assert isinstance(result, dict)
|
|
for k, v in result.items():
|
|
assert isinstance(k, int)
|
|
assert isinstance(v, int)
|