- 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
150 lines
5.6 KiB
Python
150 lines
5.6 KiB
Python
"""Tests for src/config.py - ConfigManager."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
import pytest
|
|
from src.config import ConfigManager
|
|
|
|
|
|
class TestConfigManagerInit:
|
|
def test_init_no_config_file(self, tmp_path, monkeypatch):
|
|
cfg = ConfigManager(str(tmp_path / "nonexistent.json"))
|
|
assert cfg.config["tvdb_api_key"] == ""
|
|
assert cfg.config["default_episode_duration"] == 45
|
|
assert cfg.config["classification_thresholds"]["size_threshold_ratio"] == 0.3
|
|
|
|
def test_init_existing_config(self, config_file):
|
|
cfg = ConfigManager(str(config_file))
|
|
assert cfg.config["tvdb_api_key"] == "test_api_key_12345"
|
|
assert cfg.config["default_episode_duration"] == 45
|
|
|
|
def test_init_invalid_json(self, tmp_path):
|
|
bad = tmp_path / "config.json"
|
|
bad.write_text("{invalid json")
|
|
cfg = ConfigManager(str(bad))
|
|
assert cfg.config["tvdb_api_key"] == ""
|
|
|
|
def test_init_missing_keys_merged(self, tmp_path):
|
|
partial = tmp_path / "config.json"
|
|
partial.write_text(json.dumps({"tvdb_api_key": "abc"}))
|
|
cfg = ConfigManager(str(partial))
|
|
assert cfg.config["default_episode_duration"] == 45
|
|
assert "classification_thresholds" in cfg.config
|
|
|
|
def test_init_io_error(self, tmp_path, monkeypatch):
|
|
cfg_path = tmp_path / "config.json"
|
|
cfg_path.write_text("test")
|
|
monkeypatch.setattr(Path, "exists", lambda self: True)
|
|
|
|
original_open = open
|
|
|
|
def mock_open(*args, **kwargs):
|
|
raise PermissionError("denied")
|
|
|
|
with patch("builtins.open", mock_open):
|
|
cfg = ConfigManager(str(cfg_path))
|
|
assert cfg.config["tvdb_api_key"] == ""
|
|
|
|
|
|
class TestConfigManagerApiKeys:
|
|
def test_get_api_key_from_config(self, config_file):
|
|
cfg = ConfigManager(str(config_file))
|
|
with patch.dict(os.environ, {}, clear=True):
|
|
key = cfg.get_tvdb_api_key()
|
|
assert key == "test_api_key_12345"
|
|
|
|
def test_get_api_key_from_env(self, config_file, monkeypatch):
|
|
cfg = ConfigManager(str(config_file))
|
|
monkeypatch.setenv("TVDB_API_KEY", "env_key_999")
|
|
key = cfg.get_tvdb_api_key()
|
|
assert key == "env_key_999"
|
|
|
|
def test_get_api_key_empty_env(self, config_file, monkeypatch):
|
|
cfg = ConfigManager(str(config_file))
|
|
monkeypatch.setenv("TVDB_API_KEY", " ")
|
|
key = cfg.get_tvdb_api_key()
|
|
assert key == "test_api_key_12345"
|
|
|
|
def test_get_api_key_placeholder_rejected(self, empty_config_file):
|
|
cfg = ConfigManager(str(empty_config_file))
|
|
cfg.config["tvdb_api_key"] = "YOUR_TVDB_API_KEY_HERE"
|
|
assert cfg.get_tvdb_api_key() is None
|
|
|
|
def test_get_api_key_env_placeholder_rejected(self, empty_config_file, monkeypatch):
|
|
cfg = ConfigManager(str(empty_config_file))
|
|
monkeypatch.setenv("TVDB_API_KEY", "YOUR_TVDB_API_KEY_HERE")
|
|
assert cfg.get_tvdb_api_key() is None
|
|
|
|
def test_env_key_takes_priority(self, config_file, monkeypatch):
|
|
cfg = ConfigManager(str(config_file))
|
|
monkeypatch.setenv("TVDB_API_KEY", "env_priority")
|
|
assert cfg.get_tvdb_api_key() == "env_priority"
|
|
|
|
|
|
class TestConfigManagerSettings:
|
|
def test_get_default_episode_duration(self, config_file):
|
|
cfg = ConfigManager(str(config_file))
|
|
assert cfg.get_default_episode_duration() == 45
|
|
|
|
def test_get_default_episode_duration_custom(self, tmp_path):
|
|
f = tmp_path / "config.json"
|
|
f.write_text(json.dumps({"default_episode_duration": 60}))
|
|
cfg = ConfigManager(str(f))
|
|
assert cfg.get_default_episode_duration() == 60
|
|
|
|
def test_get_classification_thresholds(self, config_file):
|
|
cfg = ConfigManager(str(config_file))
|
|
t = cfg.get_classification_thresholds()
|
|
assert t["size_threshold_ratio"] == 0.3
|
|
assert t["duration_threshold_ratio"] == 0.4
|
|
|
|
def test_get_classification_thresholds_defaults(self, tmp_path):
|
|
cfg = ConfigManager(str(tmp_path / "no.json"))
|
|
t = cfg.get_classification_thresholds()
|
|
assert t["size_threshold_ratio"] == 0.3
|
|
|
|
|
|
class TestConfigManagerUpdate:
|
|
def test_update_api_key_success(self, tmp_path):
|
|
cfg = ConfigManager(str(tmp_path / "config.json"))
|
|
result = cfg.update_api_key("new_key_abc")
|
|
assert result is True
|
|
assert cfg.config["tvdb_api_key"] == "new_key_abc"
|
|
|
|
stored = json.loads((tmp_path / "config.json").read_text())
|
|
assert stored["tvdb_api_key"] == "new_key_abc"
|
|
|
|
def test_update_api_key_io_error(self, tmp_path):
|
|
cfg = ConfigManager(str(tmp_path / "config.json"))
|
|
cfg.update_api_key("new_key")
|
|
|
|
cfg.config_file = Path("/nonexistent_dir/config.json")
|
|
result = cfg.update_api_key("another_key")
|
|
assert result is False
|
|
|
|
|
|
class TestConfigManagerStatus:
|
|
def test_print_config_status_no_key(self, tmp_path, capsys):
|
|
cfg = ConfigManager(str(tmp_path / "no.json"))
|
|
cfg.print_config_status()
|
|
out = capsys.readouterr().out
|
|
assert "Configuration file:" in out
|
|
assert "Not configured" in out
|
|
|
|
def test_print_config_status_with_key(self, config_file, capsys):
|
|
cfg = ConfigManager(str(config_file))
|
|
cfg.print_config_status()
|
|
out = capsys.readouterr().out
|
|
assert "configured" in out
|
|
|
|
def test_print_config_status_masked_short_key(self, tmp_path):
|
|
f = tmp_path / "config.json"
|
|
f.write_text(json.dumps({"tvdb_api_key": "ab"}))
|
|
cfg = ConfigManager(str(f))
|
|
cfg.print_config_status()
|