- 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
237 lines
7.9 KiB
Python
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
|