Files
Aniworld/tests/integration/test_cli_workflows.py
Lukas c6da967893 Task 6: Add CLI Interface tests (25 tests)
- test_nfo_cli.py: 19 tests for main dispatcher (scan/status/update/unknown),
  scan_and_create_nfo, check_nfo_status, update_nfo_files
- test_cli_workflows.py: 6 integration tests for end-to-end scan workflow,
  update workflow, error handling, and per-series error continuation
2026-02-15 17:49:11 +01:00

185 lines
6.3 KiB
Python

"""Integration tests for CLI workflows.
Tests end-to-end CLI command execution using subprocess-style invocation of
the nfo_cli main() function with mocked services.
"""
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.cli.nfo_cli import main, scan_and_create_nfo, update_nfo_files
def _mock_serie(name: str, has_nfo: bool = False):
"""Create a mock serie object."""
s = MagicMock()
s.name = name
s.folder = name
s.has_nfo.return_value = has_nfo
s.has_poster.return_value = False
s.has_logo.return_value = False
s.has_fanart.return_value = False
return s
class TestScanWorkflow:
"""End-to-end scan workflow."""
@pytest.mark.asyncio
@patch("src.cli.nfo_cli.SeriesManagerService")
@patch("src.cli.nfo_cli.settings")
async def test_scan_creates_nfo_and_closes(self, mock_settings, mock_sms):
"""Full scan workflow: init → scan → close."""
mock_settings.tmdb_api_key = "key"
mock_settings.anime_directory = "/anime"
mock_settings.nfo_auto_create = True
mock_settings.nfo_update_on_scan = False
mock_settings.nfo_download_poster = False
mock_settings.nfo_download_logo = False
mock_settings.nfo_download_fanart = False
series = [_mock_serie("Naruto"), _mock_serie("Bleach")]
mock_serie_list = MagicMock()
mock_serie_list.get_all.return_value = series
mock_serie_list.load_series = MagicMock()
manager = MagicMock()
manager.get_serie_list.return_value = mock_serie_list
manager.scan_and_process_nfo = AsyncMock()
manager.close = AsyncMock()
mock_sms.from_settings.return_value = manager
result = await scan_and_create_nfo()
assert result == 0
manager.scan_and_process_nfo.assert_awaited_once()
manager.close.assert_awaited_once()
@pytest.mark.asyncio
@patch("src.cli.nfo_cli.SeriesManagerService")
@patch("src.cli.nfo_cli.settings")
async def test_scan_all_have_nfo_and_no_update(self, mock_settings, mock_sms):
"""When all series have NFO and update_on_scan is False, returns 0 early."""
mock_settings.tmdb_api_key = "key"
mock_settings.anime_directory = "/anime"
mock_settings.nfo_auto_create = True
mock_settings.nfo_update_on_scan = False
mock_settings.nfo_download_poster = False
mock_settings.nfo_download_logo = False
mock_settings.nfo_download_fanart = False
series = [_mock_serie("Naruto", has_nfo=True)]
mock_serie_list = MagicMock()
mock_serie_list.get_all.return_value = series
manager = MagicMock()
manager.get_serie_list.return_value = mock_serie_list
mock_sms.from_settings.return_value = manager
result = await scan_and_create_nfo()
assert result == 0
class TestUpdateWorkflow:
"""End-to-end update workflow."""
@pytest.mark.asyncio
@patch("src.cli.nfo_cli.asyncio")
@patch("src.cli.nfo_cli.settings")
async def test_update_processes_each_serie(self, mock_settings, mock_sleeper):
"""Update calls update_tvshow_nfo for every serie with NFO."""
mock_settings.tmdb_api_key = "key"
mock_settings.anime_directory = "/anime"
mock_settings.nfo_download_poster = True
mock_settings.nfo_download_logo = False
mock_settings.nfo_download_fanart = False
mock_sleeper.sleep = AsyncMock()
series = [
_mock_serie("A", has_nfo=True),
_mock_serie("B", has_nfo=True),
_mock_serie("C", has_nfo=False),
]
nfo_svc = MagicMock()
nfo_svc.update_tvshow_nfo = AsyncMock()
nfo_svc.close = AsyncMock()
with patch("src.core.entities.SerieList.SerieList") as mock_sl:
mock_list = MagicMock()
mock_list.get_all.return_value = series
mock_sl.return_value = mock_list
with patch(
"src.core.services.nfo_factory.create_nfo_service",
return_value=nfo_svc,
):
result = await update_nfo_files()
assert result == 0
# Only A and B have NFO
assert nfo_svc.update_tvshow_nfo.await_count == 2
nfo_svc.close.assert_awaited_once()
@pytest.mark.asyncio
@patch("src.cli.nfo_cli.asyncio")
@patch("src.cli.nfo_cli.settings")
async def test_update_continues_on_per_series_error(
self, mock_settings, mock_sleeper
):
"""An error updating one serie doesn't stop others."""
mock_settings.tmdb_api_key = "key"
mock_settings.anime_directory = "/anime"
mock_settings.nfo_download_poster = False
mock_settings.nfo_download_logo = False
mock_settings.nfo_download_fanart = False
mock_sleeper.sleep = AsyncMock()
series = [
_mock_serie("OK", has_nfo=True),
_mock_serie("Fail", has_nfo=True),
]
nfo_svc = MagicMock()
# First call succeeds, second raises
nfo_svc.update_tvshow_nfo = AsyncMock(
side_effect=[None, RuntimeError("api down")]
)
nfo_svc.close = AsyncMock()
with patch("src.core.entities.SerieList.SerieList") as mock_sl:
mock_list = MagicMock()
mock_list.get_all.return_value = series
mock_sl.return_value = mock_list
with patch(
"src.core.services.nfo_factory.create_nfo_service",
return_value=nfo_svc,
):
result = await update_nfo_files()
assert result == 0
assert nfo_svc.update_tvshow_nfo.await_count == 2
class TestErrorHandlingWorkflows:
"""Test error paths in CLI workflows."""
@patch("src.cli.nfo_cli.sys")
def test_main_usage_printed_on_no_args(self, mock_sys, capsys):
"""Shows usage and returns 1 with no args."""
mock_sys.argv = ["nfo_cli"]
result = main()
assert result == 1
@pytest.mark.asyncio
@patch("src.cli.nfo_cli.settings")
async def test_missing_config_returns_1(self, mock_settings):
"""Missing required settings yields exit code 1."""
mock_settings.tmdb_api_key = None
assert await scan_and_create_nfo() == 1
mock_settings.tmdb_api_key = "key"
mock_settings.anime_directory = None
assert await scan_and_create_nfo() == 1