- Add missing TMDB async mock methods (_ensure_session, close) to all TMDB mocks in test_nfo_workflow.py - Refactor test_anime_add_nfo_isolation.py to mock get_nfo_factory() instead of asserting on series_app.nfo_service directly - Patch get_nfo_factory in test_background_loader_service.py to align with factory-based NFOService creation Fixes test failures caused by NFOService refactoring that introduced explicit TMDB session lifecycle and NFO factory pattern.
338 lines
11 KiB
Python
338 lines
11 KiB
Python
"""Integration tests to verify anime add only loads NFO/artwork for the specific anime.
|
|
|
|
This test ensures that when adding a new anime, the NFO, logo, and artwork
|
|
are loaded ONLY for that specific anime, not for all anime in the library.
|
|
"""
|
|
import asyncio
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, Mock, call, patch
|
|
|
|
import pytest
|
|
|
|
from src.server.services.background_loader_service import BackgroundLoaderService
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_anime_dir(tmp_path):
|
|
"""Create temporary anime directory with existing anime."""
|
|
anime_dir = tmp_path / "anime"
|
|
anime_dir.mkdir()
|
|
|
|
# Create two existing anime directories
|
|
existing_anime_1 = anime_dir / "Existing Anime 1"
|
|
existing_anime_1.mkdir()
|
|
(existing_anime_1 / "data").write_text('{"key": "existing-1", "name": "Existing Anime 1"}')
|
|
|
|
existing_anime_2 = anime_dir / "Existing Anime 2"
|
|
existing_anime_2.mkdir()
|
|
(existing_anime_2 / "data").write_text('{"key": "existing-2", "name": "Existing Anime 2"}')
|
|
|
|
return str(anime_dir)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_series_app(temp_anime_dir):
|
|
"""Create mock SeriesApp."""
|
|
app = MagicMock()
|
|
app.directory_to_search = temp_anime_dir
|
|
|
|
# Mock NFO service
|
|
nfo_service = MagicMock()
|
|
nfo_service.has_nfo = MagicMock(return_value=False)
|
|
nfo_service.create_tvshow_nfo = AsyncMock()
|
|
app.nfo_service = nfo_service
|
|
|
|
# Mock series list
|
|
app.list = MagicMock()
|
|
app.list.keyDict = {}
|
|
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_websocket_service():
|
|
"""Create mock WebSocket service."""
|
|
service = MagicMock()
|
|
service.broadcast = AsyncMock()
|
|
service.broadcast_to_room = AsyncMock()
|
|
return service
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_anime_service():
|
|
"""Create mock AnimeService."""
|
|
service = MagicMock()
|
|
service.rescan_series = AsyncMock()
|
|
return service
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_database():
|
|
"""Mock database access for all NFO isolation tests."""
|
|
mock_db = AsyncMock()
|
|
mock_db.commit = AsyncMock()
|
|
|
|
with patch("src.server.database.connection.get_db_session") as mock_get_db, patch("src.server.database.service.AnimeSeriesService") as mock_service:
|
|
mock_get_db.return_value.__aenter__ = AsyncMock(return_value=mock_db)
|
|
mock_get_db.return_value.__aexit__ = AsyncMock(return_value=None)
|
|
mock_service.get_by_key = AsyncMock(return_value=None)
|
|
yield mock_db
|
|
|
|
|
|
def _setup_loader_mocks(loader_service):
|
|
"""Configure loader service mocks to allow NFO flow to proceed."""
|
|
loader_service.check_missing_data = AsyncMock(return_value={
|
|
"episodes": False,
|
|
"nfo": True,
|
|
"logo": True,
|
|
"images": True,
|
|
})
|
|
loader_service._scan_missing_episodes = AsyncMock()
|
|
loader_service._broadcast_status = AsyncMock()
|
|
|
|
|
|
def _mock_nfo_factory(mock_nfo_service):
|
|
"""Create a mock NFO factory that returns the given mock service."""
|
|
mock_factory = MagicMock()
|
|
mock_factory.create = MagicMock(return_value=mock_nfo_service)
|
|
return mock_factory
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_anime_loads_nfo_only_for_new_anime(
|
|
temp_anime_dir,
|
|
mock_series_app,
|
|
mock_websocket_service,
|
|
mock_anime_service,
|
|
):
|
|
"""Test that adding a new anime only loads NFO/artwork for that specific anime.
|
|
|
|
This test verifies:
|
|
1. NFO service is called only once for the new anime
|
|
2. The call is made with the correct anime name/folder
|
|
3. Existing anime are not affected
|
|
"""
|
|
loader_service = BackgroundLoaderService(
|
|
websocket_service=mock_websocket_service,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app,
|
|
)
|
|
_setup_loader_mocks(loader_service)
|
|
|
|
# Set up mock NFO service via factory
|
|
mock_nfo_service = AsyncMock()
|
|
mock_nfo_service.create_tvshow_nfo = AsyncMock(return_value="/anime/New Anime (2024)/tvshow.nfo")
|
|
mock_factory = _mock_nfo_factory(mock_nfo_service)
|
|
|
|
with patch("src.server.services.background_loader_service.get_nfo_factory", return_value=mock_factory):
|
|
await loader_service.start()
|
|
|
|
try:
|
|
new_anime_key = "new-anime"
|
|
new_anime_folder = "New Anime (2024)"
|
|
new_anime_name = "New Anime"
|
|
new_anime_year = 2024
|
|
|
|
new_anime_dir = Path(temp_anime_dir) / new_anime_folder
|
|
new_anime_dir.mkdir()
|
|
|
|
await loader_service.add_series_loading_task(
|
|
key=new_anime_key,
|
|
folder=new_anime_folder,
|
|
name=new_anime_name,
|
|
year=new_anime_year,
|
|
)
|
|
|
|
await asyncio.sleep(1.0)
|
|
|
|
assert mock_nfo_service.create_tvshow_nfo.call_count == 1
|
|
|
|
call_args = mock_nfo_service.create_tvshow_nfo.call_args
|
|
assert call_args is not None
|
|
|
|
kwargs = call_args.kwargs
|
|
assert kwargs["serie_name"] == new_anime_name
|
|
assert kwargs["serie_folder"] == new_anime_folder
|
|
assert kwargs["year"] == new_anime_year
|
|
assert kwargs["download_poster"] is True
|
|
assert kwargs["download_logo"] is True
|
|
assert kwargs["download_fanart"] is True
|
|
|
|
all_calls = mock_nfo_service.create_tvshow_nfo.call_args_list
|
|
for call_obj in all_calls:
|
|
call_kwargs = call_obj.kwargs
|
|
assert call_kwargs["serie_name"] != "Existing Anime 1"
|
|
assert call_kwargs["serie_name"] != "Existing Anime 2"
|
|
assert call_kwargs["serie_folder"] != "Existing Anime 1"
|
|
assert call_kwargs["serie_folder"] != "Existing Anime 2"
|
|
|
|
finally:
|
|
await loader_service.stop()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_anime_has_nfo_check_is_isolated(
|
|
temp_anime_dir,
|
|
mock_series_app,
|
|
mock_websocket_service,
|
|
mock_anime_service,
|
|
):
|
|
"""Test that has_nfo check is called only for the specific anime being added."""
|
|
loader_service = BackgroundLoaderService(
|
|
websocket_service=mock_websocket_service,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app,
|
|
)
|
|
_setup_loader_mocks(loader_service)
|
|
|
|
await loader_service.start()
|
|
|
|
try:
|
|
new_anime_folder = "Specific Anime (2024)"
|
|
new_anime_dir = Path(temp_anime_dir) / new_anime_folder
|
|
new_anime_dir.mkdir()
|
|
|
|
await loader_service.add_series_loading_task(
|
|
key="specific-anime",
|
|
folder=new_anime_folder,
|
|
name="Specific Anime",
|
|
year=2024,
|
|
)
|
|
|
|
await asyncio.sleep(1.0)
|
|
|
|
assert mock_series_app.nfo_service.has_nfo.call_count >= 1
|
|
|
|
call_args_list = mock_series_app.nfo_service.has_nfo.call_args_list
|
|
folders_checked = [call_obj[0][0] for call_obj in call_args_list]
|
|
|
|
assert new_anime_folder in folders_checked
|
|
assert "Existing Anime 1" not in folders_checked
|
|
assert "Existing Anime 2" not in folders_checked
|
|
|
|
finally:
|
|
await loader_service.stop()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_anime_added_each_loads_independently(
|
|
temp_anime_dir,
|
|
mock_series_app,
|
|
mock_websocket_service,
|
|
mock_anime_service,
|
|
):
|
|
"""Test that adding multiple anime loads NFO/artwork for each one independently."""
|
|
loader_service = BackgroundLoaderService(
|
|
websocket_service=mock_websocket_service,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app,
|
|
)
|
|
_setup_loader_mocks(loader_service)
|
|
|
|
# Set up mock NFO service via factory
|
|
mock_nfo_service = AsyncMock()
|
|
mock_nfo_service.create_tvshow_nfo = AsyncMock(return_value="/anime/tvshow.nfo")
|
|
mock_factory = _mock_nfo_factory(mock_nfo_service)
|
|
|
|
with patch("src.server.services.background_loader_service.get_nfo_factory", return_value=mock_factory):
|
|
await loader_service.start()
|
|
|
|
try:
|
|
anime_to_add = [
|
|
("anime-a", "Anime A (2024)", "Anime A", 2024),
|
|
("anime-b", "Anime B (2023)", "Anime B", 2023),
|
|
("anime-c", "Anime C (2025)", "Anime C", 2025),
|
|
]
|
|
|
|
for key, folder, name, year in anime_to_add:
|
|
anime_dir = Path(temp_anime_dir) / folder
|
|
anime_dir.mkdir()
|
|
|
|
await loader_service.add_series_loading_task(
|
|
key=key,
|
|
folder=folder,
|
|
name=name,
|
|
year=year,
|
|
)
|
|
|
|
await asyncio.sleep(2.0)
|
|
|
|
assert mock_nfo_service.create_tvshow_nfo.call_count == 3
|
|
|
|
all_calls = mock_nfo_service.create_tvshow_nfo.call_args_list
|
|
|
|
called_names = [call_obj.kwargs["serie_name"] for call_obj in all_calls]
|
|
called_folders = [call_obj.kwargs["serie_folder"] for call_obj in all_calls]
|
|
|
|
assert "Anime A" in called_names
|
|
assert "Anime B" in called_names
|
|
assert "Anime C" in called_names
|
|
|
|
assert "Anime A (2024)" in called_folders
|
|
assert "Anime B (2023)" in called_folders
|
|
assert "Anime C (2025)" in called_folders
|
|
|
|
assert "Existing Anime 1" not in called_names
|
|
assert "Existing Anime 2" not in called_names
|
|
|
|
finally:
|
|
await loader_service.stop()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_nfo_service_receives_correct_parameters(
|
|
temp_anime_dir,
|
|
mock_series_app,
|
|
mock_websocket_service,
|
|
mock_anime_service,
|
|
):
|
|
"""Test that NFO service receives all required parameters for the specific anime."""
|
|
loader_service = BackgroundLoaderService(
|
|
websocket_service=mock_websocket_service,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app,
|
|
)
|
|
_setup_loader_mocks(loader_service)
|
|
|
|
# Set up mock NFO service via factory
|
|
mock_nfo_service = AsyncMock()
|
|
mock_nfo_service.create_tvshow_nfo = AsyncMock(return_value="/anime/Test Anime Series (2024)/tvshow.nfo")
|
|
mock_factory = _mock_nfo_factory(mock_nfo_service)
|
|
|
|
with patch("src.server.services.background_loader_service.get_nfo_factory", return_value=mock_factory):
|
|
await loader_service.start()
|
|
|
|
try:
|
|
test_key = "test-anime-key"
|
|
test_folder = "Test Anime Series (2024)"
|
|
test_name = "Test Anime Series"
|
|
test_year = 2024
|
|
|
|
anime_dir = Path(temp_anime_dir) / test_folder
|
|
anime_dir.mkdir()
|
|
|
|
await loader_service.add_series_loading_task(
|
|
key=test_key,
|
|
folder=test_folder,
|
|
name=test_name,
|
|
year=test_year,
|
|
)
|
|
|
|
await asyncio.sleep(1.0)
|
|
|
|
assert mock_nfo_service.create_tvshow_nfo.call_count == 1
|
|
|
|
call_kwargs = mock_nfo_service.create_tvshow_nfo.call_args.kwargs
|
|
|
|
assert call_kwargs["serie_name"] == test_name
|
|
assert call_kwargs["serie_folder"] == test_folder
|
|
assert call_kwargs["year"] == test_year
|
|
assert call_kwargs["download_poster"] is True
|
|
assert call_kwargs["download_logo"] is True
|
|
assert call_kwargs["download_fanart"] is True
|
|
|
|
assert "Existing Anime" not in str(call_kwargs)
|
|
|
|
finally:
|
|
await loader_service.stop()
|