EpisodeMatcher/tests/test_tvdb_cache.py
Jarian f1aa2c4cda test: add test suite with 90% coverage
- 176 unit tests covering ConfigManager, FileClassifier, EpisodeRenamer,
  TVDBClient, MockTVDBClient, TVDBCache, and episode_matcher CLI
- Tests organized in tests/ with conftest.py shared fixtures
- pytest-cov configured with 90% coverage threshold in pyproject.toml
- CI workflow updated with COVERAGE_CORE=sysmon for Python 3.13 compat
- .gitignore updated for coverage artifacts and venv
2026-07-06 14:31:22 +00:00

237 lines
7.9 KiB
Python

"""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