|
|
@@ -0,0 +1,568 @@
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+"""
|
|
|
+Tests fuer StreamingCache — JSON-Katalog-Cache mit In-Memory-Indizes.
|
|
|
+
|
|
|
+Deckt JSON I/O, Index-Aufbau, Suche, Filter (Sprache, Genre, Bewertung),
|
|
|
+Slug-Generierung und atomares Speichern ab.
|
|
|
+"""
|
|
|
+
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import json
|
|
|
+from pathlib import Path
|
|
|
+from unittest.mock import AsyncMock, patch
|
|
|
+
|
|
|
+import pytest
|
|
|
+
|
|
|
+from plugins.streaming.catalog.cache import StreamingCache
|
|
|
+
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# Hilfsfunktionen und Testdaten
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+def _make_title(
|
|
|
+ name: str,
|
|
|
+ ty: str = "serie",
|
|
|
+ genres: list[str] | None = None,
|
|
|
+ rating: float = 7.0,
|
|
|
+ year: int = 2020,
|
|
|
+ services: dict | None = None,
|
|
|
+ orig_lang: str = "ja",
|
|
|
+ tmdb_id: int = 1,
|
|
|
+) -> dict:
|
|
|
+ """Erzeugt ein realistisches Title-Dict."""
|
|
|
+ if genres is None:
|
|
|
+ genres = ["Action"]
|
|
|
+ if services is None:
|
|
|
+ services = {
|
|
|
+ "netflix": {"dub": ["de", "en"], "sub": ["de"]},
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ "t": name,
|
|
|
+ "ty": ty,
|
|
|
+ "g": genres,
|
|
|
+ "d": f"Beschreibung fuer {name}",
|
|
|
+ "yr": year,
|
|
|
+ "rt": rating,
|
|
|
+ "orig_lang": orig_lang,
|
|
|
+ "on": services,
|
|
|
+ "tmdb_id": tmdb_id,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+NARUTO = _make_title(
|
|
|
+ "Naruto Shippuden",
|
|
|
+ genres=["Action", "Anime"],
|
|
|
+ rating=8.1,
|
|
|
+ year=2007,
|
|
|
+ services={
|
|
|
+ "crunchyroll": {"dub": ["ja"], "sub": ["de"]},
|
|
|
+ "netflix": {"dub": ["de", "ja"], "sub": ["de"]},
|
|
|
+ },
|
|
|
+ tmdb_id=46559,
|
|
|
+)
|
|
|
+
|
|
|
+ONE_PIECE = _make_title(
|
|
|
+ "One Piece",
|
|
|
+ genres=["Action", "Anime"],
|
|
|
+ rating=8.7,
|
|
|
+ year=1999,
|
|
|
+ services={
|
|
|
+ "crunchyroll": {"dub": ["ja"], "sub": ["de"]},
|
|
|
+ },
|
|
|
+ tmdb_id=37854,
|
|
|
+)
|
|
|
+
|
|
|
+BREAKING_BAD = _make_title(
|
|
|
+ "Breaking Bad",
|
|
|
+ genres=["Drama", "Thriller"],
|
|
|
+ rating=9.5,
|
|
|
+ year=2008,
|
|
|
+ services={
|
|
|
+ "netflix": {"dub": ["de", "en"], "sub": ["de", "en"]},
|
|
|
+ },
|
|
|
+ orig_lang="en",
|
|
|
+ tmdb_id=1396,
|
|
|
+)
|
|
|
+
|
|
|
+DARK = _make_title(
|
|
|
+ "Dark",
|
|
|
+ genres=["Drama", "Mystery"],
|
|
|
+ rating=8.8,
|
|
|
+ year=2017,
|
|
|
+ services={
|
|
|
+ "netflix": {"dub": ["de"], "sub": ["de", "en"]},
|
|
|
+ },
|
|
|
+ orig_lang="de",
|
|
|
+ tmdb_id=70523,
|
|
|
+)
|
|
|
+
|
|
|
+INCEPTION = _make_title(
|
|
|
+ "Inception",
|
|
|
+ ty="film",
|
|
|
+ genres=["Action", "Science Fiction"],
|
|
|
+ rating=8.4,
|
|
|
+ year=2010,
|
|
|
+ services={
|
|
|
+ "netflix": {"dub": ["de", "en"], "sub": ["de"]},
|
|
|
+ "amazon_prime": {"dub": ["de", "en"], "sub": ["de"]},
|
|
|
+ },
|
|
|
+ orig_lang="en",
|
|
|
+ tmdb_id=27205,
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+def _copy_title(title: dict) -> dict:
|
|
|
+ """Tiefe Kopie eines Title-Dicts (on-Dict separat kopieren)."""
|
|
|
+ import copy
|
|
|
+ return copy.deepcopy(title)
|
|
|
+
|
|
|
+
|
|
|
+def _seed_cache(cache: StreamingCache) -> None:
|
|
|
+ """Befuellt den Cache mit Testdaten und baut Indizes auf."""
|
|
|
+ cache.update_title("naruto-shippuden", _copy_title(NARUTO))
|
|
|
+ cache.update_title("one-piece", _copy_title(ONE_PIECE))
|
|
|
+ cache.update_title("breaking-bad", _copy_title(BREAKING_BAD))
|
|
|
+ cache.update_title("dark", _copy_title(DARK))
|
|
|
+ cache.update_title("inception", _copy_title(INCEPTION))
|
|
|
+ cache._rebuild_indices()
|
|
|
+
|
|
|
+
|
|
|
+@pytest.fixture()
|
|
|
+def cache(tmp_path: Path) -> StreamingCache:
|
|
|
+ """Liefert einen leeren StreamingCache mit temporaerem Verzeichnis."""
|
|
|
+ return StreamingCache(tmp_path)
|
|
|
+
|
|
|
+
|
|
|
+@pytest.fixture()
|
|
|
+def seeded_cache(cache: StreamingCache) -> StreamingCache:
|
|
|
+ """Liefert einen StreamingCache mit vorbereiteten Testdaten."""
|
|
|
+ _seed_cache(cache)
|
|
|
+ return cache
|
|
|
+
|
|
|
+
|
|
|
+# ===================================================================
|
|
|
+# JSON I/O
|
|
|
+# ===================================================================
|
|
|
+
|
|
|
+
|
|
|
+class TestLoadSave:
|
|
|
+ """Tests fuer load(), save() und save_async()."""
|
|
|
+
|
|
|
+ def test_load_fehlende_datei_gibt_false(self, cache: StreamingCache) -> None:
|
|
|
+ """Laden ohne vorhandene Datei gibt False und leere Daten."""
|
|
|
+ ergebnis = cache.load()
|
|
|
+
|
|
|
+ assert ergebnis is False
|
|
|
+ assert cache._titles == {}
|
|
|
+ assert cache._services == {}
|
|
|
+ assert cache._recent == {}
|
|
|
+
|
|
|
+ def test_save_und_load_roundtrip(self, cache: StreamingCache) -> None:
|
|
|
+ """Gespeicherte Daten koennen exakt wieder geladen werden."""
|
|
|
+ _seed_cache(cache)
|
|
|
+ cache.save()
|
|
|
+
|
|
|
+ # Neuen Cache aus demselben Verzeichnis laden
|
|
|
+ cache2 = StreamingCache(cache._cache_dir)
|
|
|
+ ergebnis = cache2.load()
|
|
|
+
|
|
|
+ assert ergebnis is True
|
|
|
+ assert len(cache2._titles) == 5
|
|
|
+ assert cache2._titles["naruto-shippuden"]["t"] == "Naruto Shippuden"
|
|
|
+ assert cache2._titles["breaking-bad"]["rt"] == 9.5
|
|
|
+
|
|
|
+ async def test_save_async_ruft_to_thread_auf(self, cache: StreamingCache) -> None:
|
|
|
+ """save_async() delegiert ueber asyncio.to_thread."""
|
|
|
+ _seed_cache(cache)
|
|
|
+
|
|
|
+ with patch("plugins.streaming.catalog.cache.asyncio.to_thread", new_callable=AsyncMock) as mock_to_thread:
|
|
|
+ await cache.save_async()
|
|
|
+ mock_to_thread.assert_awaited_once_with(cache._save_sync)
|
|
|
+
|
|
|
+ def test_load_korrupte_datei_gibt_false(self, cache: StreamingCache) -> None:
|
|
|
+ """Beschaedigte JSON-Datei fuehrt zu False und leeren Daten."""
|
|
|
+ cache._file_path.write_text("{kaputt:", encoding="utf-8")
|
|
|
+
|
|
|
+ ergebnis = cache.load()
|
|
|
+
|
|
|
+ assert ergebnis is False
|
|
|
+ assert cache._titles == {}
|
|
|
+
|
|
|
+ def test_atomares_schreiben_tmp_rename(self, cache: StreamingCache) -> None:
|
|
|
+ """Speicherung nutzt tmp-Datei + Umbenennung (atomar)."""
|
|
|
+ _seed_cache(cache)
|
|
|
+ cache.save()
|
|
|
+
|
|
|
+ # Nach dem Speichern darf keine .tmp-Datei uebrig sein
|
|
|
+ tmp_datei = cache._file_path.with_suffix(".tmp")
|
|
|
+ assert not tmp_datei.exists()
|
|
|
+
|
|
|
+ # Die eigentliche Datei muss existieren und gueltiges JSON enthalten
|
|
|
+ assert cache._file_path.exists()
|
|
|
+ inhalt = json.loads(cache._file_path.read_text(encoding="utf-8"))
|
|
|
+ assert "titles" in inhalt
|
|
|
+ assert "services" in inhalt
|
|
|
+ assert "recent" in inhalt
|
|
|
+
|
|
|
+
|
|
|
+# ===================================================================
|
|
|
+# Daten-Mutation
|
|
|
+# ===================================================================
|
|
|
+
|
|
|
+
|
|
|
+class TestUpdateTitle:
|
|
|
+ """Tests fuer update_title()."""
|
|
|
+
|
|
|
+ def test_neuer_titel_wird_hinzugefuegt(self, cache: StreamingCache) -> None:
|
|
|
+ """Ein neuer Titel wird korrekt in _titles gespeichert."""
|
|
|
+ cache.update_title("naruto-shippuden", _copy_title(NARUTO))
|
|
|
+
|
|
|
+ assert "naruto-shippuden" in cache._titles
|
|
|
+ assert cache._titles["naruto-shippuden"]["t"] == "Naruto Shippuden"
|
|
|
+
|
|
|
+ def test_bestehender_titel_wird_aktualisiert(self, cache: StreamingCache) -> None:
|
|
|
+ """Beim Update eines Titels werden on-Dicts zusammengefuehrt."""
|
|
|
+ cache.update_title("naruto-shippuden", _copy_title(NARUTO))
|
|
|
+
|
|
|
+ # Zusaetzlichen Dienst hinzufuegen
|
|
|
+ update_daten = _make_title(
|
|
|
+ "Naruto Shippuden",
|
|
|
+ services={"amazon_prime": {"dub": ["de"], "sub": ["de"]}},
|
|
|
+ )
|
|
|
+ cache.update_title("naruto-shippuden", update_daten)
|
|
|
+
|
|
|
+ on = cache._titles["naruto-shippuden"]["on"]
|
|
|
+ # Alte Dienste muessen erhalten bleiben
|
|
|
+ assert "crunchyroll" in on
|
|
|
+ assert "netflix" in on
|
|
|
+ # Neuer Dienst muss vorhanden sein
|
|
|
+ assert "amazon_prime" in on
|
|
|
+
|
|
|
+
|
|
|
+class TestUpdateServices:
|
|
|
+ """Tests fuer update_services()."""
|
|
|
+
|
|
|
+ def test_dienste_werden_gespeichert(self, cache: StreamingCache) -> None:
|
|
|
+ """update_services() speichert Dienst-Informationen."""
|
|
|
+ dienste = {
|
|
|
+ "netflix": {"name": "Netflix", "logo": "netflix.png"},
|
|
|
+ "crunchyroll": {"name": "Crunchyroll", "logo": "cr.png"},
|
|
|
+ }
|
|
|
+ cache.update_services(dienste)
|
|
|
+
|
|
|
+ assert cache._services["netflix"]["name"] == "Netflix"
|
|
|
+ assert cache._services["crunchyroll"]["name"] == "Crunchyroll"
|
|
|
+
|
|
|
+
|
|
|
+class TestSetRecent:
|
|
|
+ """Tests fuer set_recent()."""
|
|
|
+
|
|
|
+ def test_recent_liste_wird_gesetzt(self, cache: StreamingCache) -> None:
|
|
|
+ """set_recent() speichert die Slug-Liste fuer einen Dienst."""
|
|
|
+ cache.set_recent("netflix", ["naruto-shippuden", "dark"])
|
|
|
+
|
|
|
+ assert cache._recent["netflix"] == ["naruto-shippuden", "dark"]
|
|
|
+
|
|
|
+
|
|
|
+class TestPruneMissing:
|
|
|
+ """Tests fuer prune_missing()."""
|
|
|
+
|
|
|
+ def test_entfernt_dienst_zuordnung_und_leere_titel(
|
|
|
+ self, seeded_cache: StreamingCache,
|
|
|
+ ) -> None:
|
|
|
+ """prune_missing() entfernt Dienst-Zuordnungen und leere Titel."""
|
|
|
+ # dark ist nur auf netflix — wenn netflix entfernt wird, wird dark geloescht
|
|
|
+ # naruto ist auf crunchyroll + netflix — netflix wird entfernt, titel bleibt
|
|
|
+ aktuelle_slugs = {"breaking-bad", "inception"}
|
|
|
+ entfernt = seeded_cache.prune_missing("netflix", aktuelle_slugs)
|
|
|
+
|
|
|
+ # naruto-shippuden verliert netflix, dark verliert netflix
|
|
|
+ assert entfernt == 2
|
|
|
+ # dark hat keinen Dienst mehr → komplett geloescht
|
|
|
+ assert "dark" not in seeded_cache._titles
|
|
|
+ # naruto hat noch crunchyroll
|
|
|
+ assert "naruto-shippuden" in seeded_cache._titles
|
|
|
+ assert "netflix" not in seeded_cache._titles["naruto-shippuden"]["on"]
|
|
|
+
|
|
|
+
|
|
|
+# ===================================================================
|
|
|
+# Titel-Suche
|
|
|
+# ===================================================================
|
|
|
+
|
|
|
+
|
|
|
+class TestSearchTitle:
|
|
|
+ """Tests fuer search_title()."""
|
|
|
+
|
|
|
+ def test_exakte_suche(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Exakter Titel wird gefunden."""
|
|
|
+ ergebnis = seeded_cache.search_title("Naruto Shippuden")
|
|
|
+
|
|
|
+ assert len(ergebnis) >= 1
|
|
|
+ assert ergebnis[0]["t"] == "Naruto Shippuden"
|
|
|
+
|
|
|
+ def test_teiluebereinstimmung(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Teilweise Uebereinstimmung funktioniert."""
|
|
|
+ ergebnis = seeded_cache.search_title("Naruto")
|
|
|
+
|
|
|
+ assert len(ergebnis) >= 1
|
|
|
+ titel_namen = [t["t"] for t in ergebnis]
|
|
|
+ assert "Naruto Shippuden" in titel_namen
|
|
|
+
|
|
|
+ def test_kein_treffer_gibt_leere_liste(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Unbekannter Suchbegriff gibt leere Liste."""
|
|
|
+ ergebnis = seeded_cache.search_title("Nichtexistent XYZ")
|
|
|
+
|
|
|
+ assert ergebnis == []
|
|
|
+
|
|
|
+ def test_limit_wird_eingehalten(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Das Limit beschraenkt die Ergebnisse."""
|
|
|
+ # "action" sollte mehrere Treffer liefern, die "action" im Titel
|
|
|
+ # enthalten koennten — wir nutzen stattdessen breiteren Begriff
|
|
|
+ ergebnis = seeded_cache.search_title("a", limit=2)
|
|
|
+
|
|
|
+ assert len(ergebnis) <= 2
|
|
|
+
|
|
|
+
|
|
|
+class TestFindTitle:
|
|
|
+ """Tests fuer find_title()."""
|
|
|
+
|
|
|
+ def test_bester_treffer_wird_gefunden(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """find_title() liefert den besten einzelnen Treffer."""
|
|
|
+ ergebnis = seeded_cache.find_title("Breaking Bad")
|
|
|
+
|
|
|
+ assert ergebnis is not None
|
|
|
+ assert ergebnis["t"] == "Breaking Bad"
|
|
|
+
|
|
|
+ def test_kein_treffer_gibt_none(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Unbekannter Suchbegriff gibt None."""
|
|
|
+ ergebnis = seeded_cache.find_title("Nichtexistent XYZ")
|
|
|
+
|
|
|
+ assert ergebnis is None
|
|
|
+
|
|
|
+
|
|
|
+# ===================================================================
|
|
|
+# Dienst-Suche
|
|
|
+# ===================================================================
|
|
|
+
|
|
|
+
|
|
|
+class TestSearchOnService:
|
|
|
+ """Tests fuer search_on_service()."""
|
|
|
+
|
|
|
+ def test_filter_nach_dienst(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Nur Titel auf dem angegebenen Dienst werden zurueckgegeben."""
|
|
|
+ ergebnis = seeded_cache.search_on_service("crunchyroll")
|
|
|
+
|
|
|
+ assert len(ergebnis) >= 2 # Naruto + One Piece
|
|
|
+ for titel in ergebnis:
|
|
|
+ assert "crunchyroll" in titel["on"]
|
|
|
+
|
|
|
+ def test_filter_nach_genre(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Genre-Filter schraenkt die Ergebnisse ein."""
|
|
|
+ ergebnis = seeded_cache.search_on_service("netflix", genre="Drama")
|
|
|
+
|
|
|
+ for titel in ergebnis:
|
|
|
+ assert any("Drama" in g for g in titel.get("g", []))
|
|
|
+
|
|
|
+ def test_filter_dienst_und_genre(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Kombination von Dienst- und Genre-Filter."""
|
|
|
+ ergebnis = seeded_cache.search_on_service(
|
|
|
+ "netflix", genre="Anime",
|
|
|
+ )
|
|
|
+
|
|
|
+ for titel in ergebnis:
|
|
|
+ assert "netflix" in titel["on"]
|
|
|
+ assert any("Anime" in g for g in titel.get("g", []))
|
|
|
+
|
|
|
+
|
|
|
+class TestGetServicesForTitle:
|
|
|
+ """Tests fuer get_services_for_title()."""
|
|
|
+
|
|
|
+ def test_alle_dienste_zurueckgegeben(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Gibt alle Dienste zurueck, auf denen der Titel verfuegbar ist."""
|
|
|
+ dienste = seeded_cache.get_services_for_title("naruto-shippuden")
|
|
|
+
|
|
|
+ assert set(dienste) == {"crunchyroll", "netflix"}
|
|
|
+
|
|
|
+ def test_unbekannter_slug_gibt_leere_liste(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Unbekannter Slug gibt leere Liste."""
|
|
|
+ dienste = seeded_cache.get_services_for_title("nichtexistent")
|
|
|
+
|
|
|
+ assert dienste == []
|
|
|
+
|
|
|
+
|
|
|
+# ===================================================================
|
|
|
+# Recent
|
|
|
+# ===================================================================
|
|
|
+
|
|
|
+
|
|
|
+class TestGetRecent:
|
|
|
+ """Tests fuer get_recent()."""
|
|
|
+
|
|
|
+ def test_recent_titel_werden_aufgeloest(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """get_recent() loest Slugs zu Title-Dicts auf."""
|
|
|
+ seeded_cache.set_recent("netflix", ["naruto-shippuden", "dark"])
|
|
|
+
|
|
|
+ ergebnis = seeded_cache.get_recent("netflix")
|
|
|
+
|
|
|
+ assert len(ergebnis) == 2
|
|
|
+ namen = [t["t"] for t in ergebnis]
|
|
|
+ assert "Naruto Shippuden" in namen
|
|
|
+ assert "Dark" in namen
|
|
|
+
|
|
|
+ def test_leere_recent_gibt_leere_liste(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Kein recent-Eintrag gibt leere Liste."""
|
|
|
+ ergebnis = seeded_cache.get_recent("disney_plus")
|
|
|
+
|
|
|
+ assert ergebnis == []
|
|
|
+
|
|
|
+
|
|
|
+# ===================================================================
|
|
|
+# Genre-Abfragen
|
|
|
+# ===================================================================
|
|
|
+
|
|
|
+
|
|
|
+class TestGetByGenre:
|
|
|
+ """Tests fuer get_by_genre()."""
|
|
|
+
|
|
|
+ def test_findet_nach_genre(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Titel des gesuchten Genres werden gefunden."""
|
|
|
+ ergebnis = seeded_cache.get_by_genre("Action")
|
|
|
+
|
|
|
+ assert len(ergebnis) >= 2 # Naruto, One Piece, Inception
|
|
|
+ for titel in ergebnis:
|
|
|
+ assert any("Action" in g for g in titel.get("g", []))
|
|
|
+
|
|
|
+ def test_mit_dienst_filter(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Genre + Dienst-Filter schraenkt Ergebnisse ein."""
|
|
|
+ ergebnis = seeded_cache.get_by_genre("Action", service_id="crunchyroll")
|
|
|
+
|
|
|
+ for titel in ergebnis:
|
|
|
+ assert "crunchyroll" in titel["on"]
|
|
|
+
|
|
|
+ def test_case_insensitive(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Genre-Suche ist case-insensitive."""
|
|
|
+ ergebnis_lower = seeded_cache.get_by_genre("action")
|
|
|
+ ergebnis_upper = seeded_cache.get_by_genre("Action")
|
|
|
+
|
|
|
+ assert len(ergebnis_lower) == len(ergebnis_upper)
|
|
|
+
|
|
|
+
|
|
|
+# ===================================================================
|
|
|
+# Sprach-Filter (Dub/Sub)
|
|
|
+# ===================================================================
|
|
|
+
|
|
|
+
|
|
|
+class TestGetWithDub:
|
|
|
+ """Tests fuer get_with_dub()."""
|
|
|
+
|
|
|
+ def test_findet_deutsche_synchronisation(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Titel mit deutscher Synchronisation werden gefunden."""
|
|
|
+ ergebnis = seeded_cache.get_with_dub("de")
|
|
|
+
|
|
|
+ assert len(ergebnis) >= 1
|
|
|
+ # Breaking Bad, Dark, Inception haben alle deutsches Dub
|
|
|
+ namen = [t["t"] for t in ergebnis]
|
|
|
+ assert "Breaking Bad" in namen
|
|
|
+
|
|
|
+ def test_filter_nach_dienst(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Dub-Suche mit Dienst-Filter."""
|
|
|
+ ergebnis = seeded_cache.get_with_dub("ja", service_id="crunchyroll")
|
|
|
+
|
|
|
+ for titel in ergebnis:
|
|
|
+ cr_info = titel["on"].get("crunchyroll", {})
|
|
|
+ assert "ja" in cr_info.get("dub", [])
|
|
|
+
|
|
|
+ def test_filter_nach_genre(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Dub-Suche mit Genre-Filter."""
|
|
|
+ ergebnis = seeded_cache.get_with_dub("de", genre="Drama")
|
|
|
+
|
|
|
+ for titel in ergebnis:
|
|
|
+ assert any("Drama" in g for g in titel.get("g", []))
|
|
|
+
|
|
|
+
|
|
|
+class TestGetWithSub:
|
|
|
+ """Tests fuer get_with_sub()."""
|
|
|
+
|
|
|
+ def test_findet_deutsche_untertitel(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Titel mit deutschen Untertiteln werden gefunden."""
|
|
|
+ ergebnis = seeded_cache.get_with_sub("de")
|
|
|
+
|
|
|
+ assert len(ergebnis) >= 3
|
|
|
+
|
|
|
+ def test_kein_treffer_gibt_leere_liste(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Unbekannte Sprache gibt leere Liste."""
|
|
|
+ ergebnis = seeded_cache.get_with_sub("xx")
|
|
|
+
|
|
|
+ assert ergebnis == []
|
|
|
+
|
|
|
+
|
|
|
+# ===================================================================
|
|
|
+# Empfehlungen
|
|
|
+# ===================================================================
|
|
|
+
|
|
|
+
|
|
|
+class TestGetRecommendations:
|
|
|
+ """Tests fuer get_recommendations()."""
|
|
|
+
|
|
|
+ def test_filter_nach_bewertung(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """Nur Titel ueber der Mindestbewertung werden zurueckgegeben."""
|
|
|
+ ergebnis = seeded_cache.get_recommendations("Action", min_rating=8.0)
|
|
|
+
|
|
|
+ for titel in ergebnis:
|
|
|
+ assert (titel.get("rt") or 0) >= 8.0
|
|
|
+
|
|
|
+ def test_sortierung_nach_bewertung_absteigend(
|
|
|
+ self, seeded_cache: StreamingCache,
|
|
|
+ ) -> None:
|
|
|
+ """Ergebnisse sind nach Bewertung absteigend sortiert."""
|
|
|
+ ergebnis = seeded_cache.get_recommendations("Action", min_rating=0.0)
|
|
|
+
|
|
|
+ bewertungen = [t.get("rt", 0) for t in ergebnis]
|
|
|
+ assert bewertungen == sorted(bewertungen, reverse=True)
|
|
|
+
|
|
|
+
|
|
|
+# ===================================================================
|
|
|
+# Slug-Generierung
|
|
|
+# ===================================================================
|
|
|
+
|
|
|
+
|
|
|
+class TestGenerateSlug:
|
|
|
+ """Tests fuer _generate_slug()."""
|
|
|
+
|
|
|
+ def test_einfacher_titel(self) -> None:
|
|
|
+ """'Naruto Shippuden' ergibt 'naruto-shippuden'."""
|
|
|
+ assert StreamingCache._generate_slug("Naruto Shippuden") == "naruto-shippuden"
|
|
|
+
|
|
|
+ def test_sonderzeichen_entfernt(self) -> None:
|
|
|
+ """Sonderzeichen werden durch Bindestriche ersetzt."""
|
|
|
+ assert StreamingCache._generate_slug("Jujutsu Kaisen: 0") == "jujutsu-kaisen-0"
|
|
|
+ assert StreamingCache._generate_slug("Re:Zero") == "re-zero"
|
|
|
+
|
|
|
+ def test_leerer_string(self) -> None:
|
|
|
+ """Leerer String ergibt leeren Slug."""
|
|
|
+ assert StreamingCache._generate_slug("") == ""
|
|
|
+
|
|
|
+ def test_nur_sonderzeichen(self) -> None:
|
|
|
+ """Nur Sonderzeichen ergibt leeren Slug."""
|
|
|
+ assert StreamingCache._generate_slug("!!!???") == ""
|
|
|
+
|
|
|
+
|
|
|
+# ===================================================================
|
|
|
+# Statistiken
|
|
|
+# ===================================================================
|
|
|
+
|
|
|
+
|
|
|
+class TestGetStats:
|
|
|
+ """Tests fuer get_stats()."""
|
|
|
+
|
|
|
+ def test_korrekte_zaehler(self, seeded_cache: StreamingCache) -> None:
|
|
|
+ """get_stats() liefert korrekte Zaehler."""
|
|
|
+ seeded_cache.update_services({"netflix": {"name": "Netflix"}})
|
|
|
+ stats = seeded_cache.get_stats()
|
|
|
+
|
|
|
+ assert stats["title_count"] == 5
|
|
|
+ assert stats["service_count"] == 1
|
|
|
+ assert stats["genre_count"] >= 4 # Action, Anime, Drama, Thriller, ...
|
|
|
+ assert stats["index_service_count"] >= 2 # netflix, crunchyroll, ...
|