"""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