"""Tests for src/TVDBProvider/tvdb_cache.py - TVDBCache.""" import sys import json import time from pathlib import Path from datetime import datetime, timedelta from unittest.mock import patch sys.path.insert(0, str(Path(__file__).parent.parent)) import pytest from src.TVDBProvider.tvdb_cache import TVDBCache class TestTVDBCacheInit: def test_init_creates_dir(self, tmp_path): cache = TVDBCache(str(tmp_path / "cache")) assert (tmp_path / "cache").exists() def test_init_default_dir(self): cache = TVDBCache() assert cache.cache_dir.exists() assert cache.cache_duration == timedelta(days=7) def test_init_absolute_path(self): cache = TVDBCache("/tmp/absolute_cache_test") try: assert cache.cache_dir.is_absolute() finally: import shutil if cache.cache_dir.exists(): shutil.rmtree(cache.cache_dir, ignore_errors=True) class TestSanitizeName: def test_basic_sanitization(self): cache = TVDBCache.__new__(TVDBCache) result = cache._sanitize_name("Game of Thrones") assert result == "game_of_thrones" def test_special_chars_removed(self): cache = TVDBCache.__new__(TVDBCache) result = cache._sanitize_name("The Simpsons (1989)") assert "(" not in result assert ")" not in result def test_dots_stripped(self): cache = TVDBCache.__new__(TVDBCache) result = cache._sanitize_name("../../etc/passwd") assert ".." not in result assert "/" not in result def test_slashes_stripped(self): cache = TVDBCache.__new__(TVDBCache) result = cache._sanitize_name("a/b") assert result == "ab" def test_backslash_stripped(self): cache = TVDBCache.__new__(TVDBCache) result = cache._sanitize_name("a\\b") assert result == "ab" def test_numbers_preserved(self): cache = TVDBCache.__new__(TVDBCache) result = cache._sanitize_name("Show 123") assert "123" in result def test_underscores_preserved(self): cache = TVDBCache.__new__(TVDBCache) result = cache._sanitize_name("my_show") assert result == "my_show" class TestCacheKey: def test_cache_key_format(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) key = cache._get_cache_key("Test Show", 3) assert key == "test_show_s03" def test_cache_key_special_chars(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) key = cache._get_cache_key("Test (Show)!", 1) assert "(" not in key assert ")" not in key class TestCacheFile: def test_cache_file_path(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) f = cache._get_cache_file("test_s01") assert f.name == "test_s01.json" def test_cache_file_escape_detected(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) with pytest.raises(ValueError, match="escape detected"): cache._get_cache_file("../evil") class TestCacheValidity: def test_valid_cache(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) f = tmp_path / "c" / "valid.json" f.write_text("data") assert cache._is_cache_valid(f) is True def test_expired_cache(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) cache.cache_duration = timedelta(seconds=0) f = tmp_path / "c" / "old.json" f.write_text("data") time.sleep(0.1) assert cache._is_cache_valid(f) is False def test_nonexistent_cache(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) assert cache._is_cache_valid(tmp_path / "c" / "nope.json") is False class TestCacheEpisodes: def test_cache_and_retrieve(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) episodes = [{"episode_number": 1, "runtime": 45}] result = cache.cache_episodes("Test Show", 1, episodes) assert result is True retrieved = cache.get_cached_episodes("Test Show", 1) assert len(retrieved) == 1 assert retrieved[0]["episode_number"] == 1 def test_cache_write_failure(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) cache.cache_dir = Path("/nonexistent_dir") result = cache.cache_episodes("Test", 1, []) assert result is False def test_cache_data_structure(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) episodes = [{"episode_number": 1, "runtime": 45}] cache.cache_episodes("Test", 1, episodes) cache_file = tmp_path / "c" / "test_s01.json" data = json.loads(cache_file.read_text()) assert data["series_name"] == "Test" assert data["season_number"] == 1 assert "cached_at" in data assert len(data["episodes"]) == 1 def test_cache_empty_episodes(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) result = cache.cache_episodes("Empty", 1, []) assert result is True retrieved = cache.get_cached_episodes("Empty", 1) assert retrieved == [] class TestGetCachedEpisodes: def test_cache_miss(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) result = cache.get_cached_episodes("NoShow", 1) assert result is None def test_corrupted_json(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) f = tmp_path / "c" / "broken.json" f.write_text("{invalid") result = cache.get_cached_episodes("Broken", 1) assert result is None def test_missing_fields(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) f = tmp_path / "c" / "partial_s01.json" f.write_text(json.dumps({"only": "this"})) result = cache.get_cached_episodes("Partial", 1) assert result is None class TestClearCache: def test_clear_removes_files(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) cache.cache_episodes("Show1", 1, []) cache.cache_episodes("Show2", 1, []) result = cache.clear_cache() assert result is True files = list((tmp_path / "c").glob("*.json")) assert len(files) == 0 def test_clear_empty(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) assert cache.clear_cache() is True class TestListCache: def test_list_entries(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) cache.cache_episodes("Show1", 1, [{"ep": 1}]) cache.cache_episodes("Show2", 2, [{"ep": 1}, {"ep": 2}]) entries = cache.list_cache() assert len(entries) == 2 for e in entries: assert "series_name" in e assert "season_number" in e assert "episode_count" in e assert "is_valid" in e def test_list_empty(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) entries = cache.list_cache() assert entries == [] def test_list_entry_fields(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) cache.cache_episodes("Test", 1, [{"e": 1}]) entries = cache.list_cache() e = entries[0] assert e["series_name"] == "Test" assert e["season_number"] == 1 assert e["episode_count"] == 1 assert "cached_at" in e assert "age_days" in e assert "cache_key" in e def test_list_sorted_by_date(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) cache.cache_episodes("First", 1, []) time.sleep(0.01) cache.cache_episodes("Second", 1, []) entries = cache.list_cache() assert entries[0]["series_name"] == "Second" def test_list_corrupted_file(self, tmp_path): cache = TVDBCache(str(tmp_path / "c")) cache.cache_episodes("Good", 1, [{"e": 1}]) (tmp_path / "c" / "bad_s01.json").write_text("{bad}") entries = cache.list_cache() assert len(entries) == 1