Files
tvh2jellyfin/tests/test_tvdb.py
Dan Head a8b247a83f Initial commit: TVH2Jellyfin application
A Docker-based Python application that transfers completed recordings
from TVHeadend to Jellyfin media storage via SCP.

Features:
- Monitors TVHeadend for completed recordings via JSON API
- Transfers files via SCP with SSH key authentication
- Organizes files by show name
- Deletes recordings from TVHeadend after successful transfer
- Tracks transferred files to avoid duplicates
- Dry run mode for testing without making changes
2026-04-03 09:31:04 +01:00

191 lines
6.2 KiB
Python

"""Tests for TheTVDB client."""
from unittest.mock import Mock, patch
import pytest
from tvh2jellyfin.tvdb import EpisodeInfo, TVDBClient, TVDBConfig
class TestEpisodeInfo:
"""Tests for EpisodeInfo dataclass."""
def test_formatted_episode_single_digit(self):
"""Test SxxExx formatting with single digit numbers."""
info = EpisodeInfo(
series_id=123,
series_name="Test Show",
season=1,
episode=5,
episode_name="Pilot",
air_date="2024-01-15",
)
assert info.formatted_episode == "S01E05"
def test_formatted_episode_double_digit(self):
"""Test SxxExx formatting with double digit numbers."""
info = EpisodeInfo(
series_id=123,
series_name="Test Show",
season=12,
episode=25,
episode_name="Episode",
air_date="2024-01-15",
)
assert info.formatted_episode == "S12E25"
def test_formatted_episode_large_numbers(self):
"""Test formatting with season/episode > 99."""
info = EpisodeInfo(
series_id=123,
series_name="Long Running Show",
season=67,
episode=125,
episode_name="Episode",
air_date="2024-01-15",
)
assert info.formatted_episode == "S67E125"
class TestTVDBClient:
"""Tests for TVDB API client."""
@pytest.fixture
def config(self):
return TVDBConfig(
api_key="test_key",
pin="",
cache_ttl=3600,
show_mapping_file=None,
enabled=True,
)
@pytest.fixture
def client(self, config):
with patch.object(TVDBClient, "_authenticate"):
return TVDBClient(config)
def test_cache_valid(self, client):
"""Test cache validity check."""
import time
# Recent timestamp should be valid
assert client._is_cache_valid(time.time() - 100)
# Old timestamp should be invalid
assert not client._is_cache_valid(time.time() - 7200)
@patch("requests.Session.post")
def test_authenticate(self, mock_post, config):
"""Test authentication flow."""
mock_post.return_value.json.return_value = {"data": {"token": "test_token"}}
mock_post.return_value.raise_for_status = Mock()
client = TVDBClient(config)
client._authenticate()
assert client._token == "test_token"
mock_post.assert_called_once()
@patch("requests.Session.get")
@patch("requests.Session.post")
def test_search_series_success(self, mock_post, mock_get, config):
"""Test successful series search."""
mock_post.return_value.json.return_value = {"data": {"token": "test_token"}}
mock_post.return_value.raise_for_status = Mock()
mock_get.return_value.json.return_value = {
"data": [{"tvdb_id": 456, "name": "The Test Show"}]
}
mock_get.return_value.raise_for_status = Mock()
client = TVDBClient(config)
result = client.search_series("Test Show")
assert result is not None
assert result[0] == 456
assert result[1] == "The Test Show"
@patch("requests.Session.get")
@patch("requests.Session.post")
def test_search_series_not_found(self, mock_post, mock_get, config):
"""Test series search with no results."""
mock_post.return_value.json.return_value = {"data": {"token": "test_token"}}
mock_post.return_value.raise_for_status = Mock()
mock_get.return_value.json.return_value = {"data": []}
mock_get.return_value.raise_for_status = Mock()
client = TVDBClient(config)
result = client.search_series("Nonexistent Show")
assert result is None
@patch("tvh2jellyfin.tvdb.requests.Session")
def test_lookup_episode_success(self, mock_session_class, config):
"""Test successful episode lookup."""
from datetime import datetime
mock_session = Mock()
mock_session_class.return_value = mock_session
# Use a timestamp and derive the expected air date from it
timestamp = 1767744000
expected_air_date = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d")
# Mock authentication
mock_session.post.return_value.json.return_value = {"data": {"token": "test_token"}}
mock_session.post.return_value.raise_for_status = Mock()
# Mock search and episode responses
mock_session.get.return_value.raise_for_status = Mock()
mock_session.get.return_value.json.side_effect = [
{"data": [{"tvdb_id": 456, "name": "Coronation Street"}]},
{
"data": {
"name": "Coronation Street",
"episodes": [
{
"seasonNumber": 67,
"number": 4,
"name": "Episode 4",
"aired": expected_air_date,
}
],
},
"links": {},
},
]
client = TVDBClient(config)
result = client.lookup_episode("Coronation Street", timestamp)
assert result is not None
assert result.season == 67
assert result.episode == 4
assert result.formatted_episode == "S67E04"
def test_show_mapping_file(self, tmp_path, config):
"""Test show name mapping from file."""
mapping_file = tmp_path / "mapping.json"
mapping_file.write_text('{"Doctor Who": "Doctor Who (2005)"}')
config.show_mapping_file = str(mapping_file)
with patch.object(TVDBClient, "_authenticate"):
client = TVDBClient(config)
assert client._show_mapping.get("Doctor Who") == "Doctor Who (2005)"
def test_caching_prevents_duplicate_calls(self, client):
"""Test that cache prevents duplicate API calls."""
import time
# Manually populate cache
client._series_cache["test show"] = (123, "Test Show", time.time())
# Search should use cache, not make API call
result = client.search_series("test show")
assert result == (123, "Test Show")