mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-25 12:24:52 -08:00
280 lines
9.0 KiB
Python
280 lines
9.0 KiB
Python
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from fastanime.cli.config.loader import ConfigLoader
|
|
from fastanime.cli.config.model import AppConfig, GeneralConfig
|
|
from fastanime.core.exceptions import ConfigError
|
|
|
|
# ==============================================================================
|
|
# Pytest Fixtures
|
|
# ==============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_config_dir(tmp_path: Path) -> Path:
|
|
"""Creates a temporary directory for config files for each test."""
|
|
config_dir = tmp_path / "config"
|
|
config_dir.mkdir()
|
|
return config_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def valid_config_content() -> str:
|
|
"""Provides the content for a valid, complete config.ini file."""
|
|
return """
|
|
[general]
|
|
provider = hianime
|
|
selector = fzf
|
|
auto_select_anime_result = false
|
|
icons = true
|
|
preview = text
|
|
image_renderer = icat
|
|
preferred_language = romaji
|
|
sub_lang = jpn
|
|
manga_viewer = feh
|
|
downloads_dir = ~/MyAnimeDownloads
|
|
check_for_updates = false
|
|
cache_requests = false
|
|
max_cache_lifetime = 01:00:00
|
|
normalize_titles = false
|
|
discord = true
|
|
|
|
[stream]
|
|
player = vlc
|
|
quality = 720
|
|
translation_type = dub
|
|
server = gogoanime
|
|
auto_next = true
|
|
continue_from_watch_history = false
|
|
preferred_watch_history = remote
|
|
auto_skip = true
|
|
episode_complete_at = 95
|
|
ytdlp_format = best
|
|
|
|
[anilist]
|
|
per_page = 25
|
|
sort_by = TRENDING_DESC
|
|
default_media_list_tracking = track
|
|
force_forward_tracking = false
|
|
recent = 10
|
|
|
|
[fzf]
|
|
opts = --reverse --height=80%
|
|
header_color = 255,0,0
|
|
preview_header_color = 0,255,0
|
|
preview_separator_color = 0,0,255
|
|
|
|
[rofi]
|
|
theme_main = /path/to/main.rasi
|
|
theme_preview = /path/to/preview.rasi
|
|
theme_confirm = /path/to/confirm.rasi
|
|
theme_input = /path/to/input.rasi
|
|
|
|
[mpv]
|
|
args = --fullscreen
|
|
pre_args =
|
|
disable_popen = false
|
|
use_python_mpv = true
|
|
"""
|
|
|
|
|
|
@pytest.fixture
|
|
def partial_config_content() -> str:
|
|
"""Provides content for a partial config file to test default value handling."""
|
|
return """
|
|
[general]
|
|
provider = hianime
|
|
|
|
[stream]
|
|
quality = 720
|
|
"""
|
|
|
|
|
|
@pytest.fixture
|
|
def malformed_ini_content() -> str:
|
|
"""Provides content with invalid .ini syntax that configparser will fail on."""
|
|
return "[general\nkey = value"
|
|
|
|
|
|
# ==============================================================================
|
|
# Test Class for ConfigLoader
|
|
# ==============================================================================
|
|
|
|
|
|
class TestConfigLoader:
|
|
def test_load_creates_and_loads_default_config(self, temp_config_dir: Path):
|
|
"""
|
|
GIVEN no config file exists.
|
|
WHEN the ConfigLoader loads configuration.
|
|
THEN it should create a default config file and load default values.
|
|
"""
|
|
# ARRANGE
|
|
config_path = temp_config_dir / "config.ini"
|
|
assert not config_path.exists()
|
|
loader = ConfigLoader(config_path=config_path)
|
|
|
|
# ACT: Mock click.echo to prevent printing during tests
|
|
with patch("click.echo"):
|
|
config = loader.load()
|
|
|
|
# ASSERT: File creation and content
|
|
assert config_path.exists()
|
|
created_content = config_path.read_text(encoding="utf-8")
|
|
assert "[general]" in created_content
|
|
assert "# Configuration for general application behavior" in created_content
|
|
|
|
# ASSERT: Loaded object has default values.
|
|
# Direct object comparison can be brittle, so we test key attributes.
|
|
default_config = AppConfig.model_validate({})
|
|
assert config.general.provider == default_config.general.provider
|
|
assert config.stream.quality == default_config.stream.quality
|
|
assert config.anilist.per_page == default_config.anilist.per_page
|
|
# A full comparison might fail due to how Path objects or multi-line strings
|
|
# are instantiated vs. read from a file. Testing key values is more robust.
|
|
|
|
def test_load_from_valid_full_config(
|
|
self, temp_config_dir: Path, valid_config_content: str
|
|
):
|
|
"""
|
|
GIVEN a valid and complete config file exists.
|
|
WHEN the ConfigLoader loads it.
|
|
THEN it should return a correctly parsed AppConfig object with overridden values.
|
|
"""
|
|
# ARRANGE
|
|
config_path = temp_config_dir / "config.ini"
|
|
config_path.write_text(valid_config_content)
|
|
loader = ConfigLoader(config_path=config_path)
|
|
|
|
# ACT
|
|
config = loader.load()
|
|
|
|
# ASSERT
|
|
assert isinstance(config, AppConfig)
|
|
assert config.general.provider == "hianime"
|
|
assert config.general.auto_select_anime_result is False
|
|
assert config.general.downloads_dir == Path("~/MyAnimeDownloads")
|
|
assert config.stream.quality == "720"
|
|
assert config.stream.player == "vlc"
|
|
assert config.anilist.per_page == 25
|
|
assert config.fzf.opts == "--reverse --height=80%"
|
|
assert config.mpv.use_python_mpv is True
|
|
|
|
def test_load_from_partial_config(
|
|
self, temp_config_dir: Path, partial_config_content: str
|
|
):
|
|
"""
|
|
GIVEN a partial config file exists.
|
|
WHEN the ConfigLoader loads it.
|
|
THEN it should load specified values and use defaults for missing ones.
|
|
"""
|
|
# ARRANGE
|
|
config_path = temp_config_dir / "config.ini"
|
|
config_path.write_text(partial_config_content)
|
|
loader = ConfigLoader(config_path=config_path)
|
|
|
|
# ACT
|
|
config = loader.load()
|
|
|
|
# ASSERT: Specified values are loaded correctly
|
|
assert config.general.provider == "hianime"
|
|
assert config.stream.quality == "720"
|
|
|
|
# ASSERT: Other values fall back to defaults
|
|
default_general = GeneralConfig()
|
|
assert config.general.selector == default_general.selector
|
|
assert config.general.icons is False
|
|
assert config.stream.player == "mpv"
|
|
assert config.anilist.per_page == 15
|
|
|
|
@pytest.mark.parametrize(
|
|
"value, expected",
|
|
[
|
|
("true", True),
|
|
("false", False),
|
|
("yes", True),
|
|
("no", False),
|
|
("on", True),
|
|
("off", False),
|
|
("1", True),
|
|
("0", False),
|
|
],
|
|
)
|
|
def test_boolean_value_handling(
|
|
self, temp_config_dir: Path, value: str, expected: bool
|
|
):
|
|
"""
|
|
GIVEN a config file with various boolean string representations.
|
|
WHEN the ConfigLoader loads it.
|
|
THEN pydantic should correctly parse them into boolean values.
|
|
"""
|
|
# ARRANGE
|
|
content = f"[general]\nauto_select_anime_result = {value}\n"
|
|
config_path = temp_config_dir / "config.ini"
|
|
config_path.write_text(content)
|
|
loader = ConfigLoader(config_path=config_path)
|
|
|
|
# ACT
|
|
config = loader.load()
|
|
|
|
# ASSERT
|
|
assert config.general.auto_select_anime_result is expected
|
|
|
|
def test_load_raises_error_for_malformed_ini(
|
|
self, temp_config_dir: Path, malformed_ini_content: str
|
|
):
|
|
"""
|
|
GIVEN a config file has invalid .ini syntax that configparser will reject.
|
|
WHEN the ConfigLoader loads it.
|
|
THEN it should raise a ConfigError.
|
|
"""
|
|
# ARRANGE
|
|
config_path = temp_config_dir / "config.ini"
|
|
config_path.write_text(malformed_ini_content)
|
|
loader = ConfigLoader(config_path=config_path)
|
|
|
|
# ACT & ASSERT
|
|
with pytest.raises(ConfigError, match="Error parsing configuration file"):
|
|
loader.load()
|
|
|
|
def test_load_raises_error_for_invalid_value(self, temp_config_dir: Path):
|
|
"""
|
|
GIVEN a config file contains a value that fails model validation.
|
|
WHEN the ConfigLoader loads it.
|
|
THEN it should raise a ConfigError with a helpful message.
|
|
"""
|
|
# ARRANGE
|
|
invalid_content = "[stream]\nquality = 9001\n"
|
|
config_path = temp_config_dir / "config.ini"
|
|
config_path.write_text(invalid_content)
|
|
loader = ConfigLoader(config_path=config_path)
|
|
|
|
# ACT & ASSERT
|
|
with pytest.raises(ConfigError) as exc_info:
|
|
loader.load()
|
|
|
|
# Check for a user-friendly error message
|
|
assert "Configuration error" in str(exc_info.value)
|
|
assert "stream.quality" in str(exc_info.value)
|
|
|
|
def test_load_raises_error_if_default_config_cannot_be_written(
|
|
self, temp_config_dir: Path
|
|
):
|
|
"""
|
|
GIVEN the default config file cannot be written due to permissions.
|
|
WHEN the ConfigLoader attempts to create it.
|
|
THEN it should raise a ConfigError.
|
|
"""
|
|
# ARRANGE
|
|
config_path = temp_config_dir / "unwritable_dir" / "config.ini"
|
|
loader = ConfigLoader(config_path=config_path)
|
|
|
|
# ACT & ASSERT: Mock Path.write_text to simulate a permissions error
|
|
with patch("pathlib.Path.write_text", side_effect=PermissionError):
|
|
with patch("click.echo"): # Mock echo to keep test output clean
|
|
with pytest.raises(ConfigError) as exc_info:
|
|
loader.load()
|
|
|
|
assert "Could not create default configuration file" in str(exc_info.value)
|
|
assert "Please check permissions" in str(exc_info.value)
|