feat(setup): add SetupService for anime folder initialization
Extract SetupService class from initialization_service to handle: - Scan data/ folder subdirectories - Extract title and year from folder names (YYYY pattern) - Create AnimeSeries records in database - Resolve provider keys via search (single exact match) Updates _scan_folders_to_database() to delegate to SetupService.run(). Adds comprehensive unit tests for SetupService.
This commit is contained in:
@@ -746,277 +746,179 @@ class TestScanFoldersToDatabase:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_folders_extracts_year(self, tmp_path):
|
||||
"""Folder 'Attack on Titan (2013)' → title='Attack on Titan', year=2013."""
|
||||
# Create a real directory structure
|
||||
"""Folder 'Attack on Titan (2013)' → title='Attack on Titan', year=2013.
|
||||
|
||||
This test verifies the integration between _scan_folders_to_database
|
||||
and SetupService.run(). Since _scan_folders_to_database now delegates
|
||||
to SetupService.run(), we mock SetupService.run() and verify it's called.
|
||||
"""
|
||||
anime_dir = tmp_path / "anime"
|
||||
anime_dir.mkdir()
|
||||
folder_path = anime_dir / "Attack on Titan (2013)"
|
||||
folder_path.mkdir()
|
||||
|
||||
mock_series_app = AsyncMock()
|
||||
mock_series_app.search.return_value = []
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db = MagicMock()
|
||||
mock_get_db.__aenter__.return_value = mock_db
|
||||
mock_get_db.__aexit__.return_value = None
|
||||
|
||||
with patch(
|
||||
'src.server.services.initialization_service.settings'
|
||||
) as mock_settings, \
|
||||
patch(
|
||||
'src.server.utils.dependencies.get_series_app',
|
||||
return_value=mock_series_app
|
||||
), \
|
||||
patch(
|
||||
'src.server.database.connection.get_db_session',
|
||||
return_value=mock_get_db
|
||||
), \
|
||||
patch(
|
||||
'src.server.services.initialization_service.AnimeSeriesService.get_by_folder',
|
||||
new_callable=AsyncMock, return_value=None
|
||||
), \
|
||||
patch(
|
||||
'src.server.services.initialization_service.AnimeSeriesService.create',
|
||||
new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
'src.server.services.initialization_service.SetupService.run',
|
||||
new_callable=AsyncMock, return_value=1
|
||||
) as mock_setup_run:
|
||||
mock_settings.anime_directory = anime_dir
|
||||
|
||||
result = await _scan_folders_to_database()
|
||||
|
||||
assert result == 1
|
||||
mock_create.assert_called_once()
|
||||
call_kwargs = mock_create.call_args.kwargs
|
||||
assert call_kwargs['name'] == "Attack on Titan"
|
||||
assert call_kwargs['year'] == 2013
|
||||
mock_setup_run.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_folders_no_year(self, tmp_path):
|
||||
"""Folder 'OnePiece' → title='OnePiece', year=None."""
|
||||
"""Folder 'OnePiece' → title='OnePiece', year=None.
|
||||
|
||||
This test verifies the integration between _scan_folders_to_database
|
||||
and SetupService.run(). Since _scan_folders_to_database now delegates
|
||||
to SetupService.run(), we mock SetupService.run() and verify it's called.
|
||||
"""
|
||||
anime_dir = tmp_path / "anime"
|
||||
anime_dir.mkdir()
|
||||
folder_path = anime_dir / "OnePiece"
|
||||
folder_path.mkdir()
|
||||
|
||||
mock_series_app = AsyncMock()
|
||||
mock_series_app.search.return_value = []
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db = MagicMock()
|
||||
mock_get_db.__aenter__.return_value = mock_db
|
||||
mock_get_db.__aexit__.return_value = None
|
||||
|
||||
with patch(
|
||||
'src.server.services.initialization_service.settings'
|
||||
) as mock_settings, \
|
||||
patch(
|
||||
'src.server.utils.dependencies.get_series_app',
|
||||
return_value=mock_series_app
|
||||
), \
|
||||
patch(
|
||||
'src.server.database.connection.get_db_session',
|
||||
return_value=mock_get_db
|
||||
), \
|
||||
patch(
|
||||
'src.server.services.initialization_service.AnimeSeriesService.get_by_folder',
|
||||
new_callable=AsyncMock, return_value=None
|
||||
), \
|
||||
patch(
|
||||
'src.server.services.initialization_service.AnimeSeriesService.create',
|
||||
new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
'src.server.services.initialization_service.SetupService.run',
|
||||
new_callable=AsyncMock, return_value=1
|
||||
) as mock_setup_run:
|
||||
mock_settings.anime_directory = anime_dir
|
||||
|
||||
result = await _scan_folders_to_database()
|
||||
|
||||
assert result == 1
|
||||
call_kwargs = mock_create.call_args.kwargs
|
||||
assert call_kwargs['name'] == "OnePiece"
|
||||
assert call_kwargs['year'] is None
|
||||
mock_setup_run.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_folders_single_match_uses_key(self, tmp_path):
|
||||
"""Search returns 1 match with same name → use its key."""
|
||||
"""Search returns 1 match with same name → use its key.
|
||||
|
||||
This test verifies the integration between _scan_folders_to_database
|
||||
and SetupService.run(). Since _scan_folders_to_database now delegates
|
||||
to SetupService.run(), we mock SetupService.run() and verify it's called.
|
||||
"""
|
||||
anime_dir = tmp_path / "anime"
|
||||
anime_dir.mkdir()
|
||||
folder_path = anime_dir / "Attack on Titan (2013)"
|
||||
folder_path.mkdir()
|
||||
|
||||
mock_series_app = AsyncMock()
|
||||
mock_series_app.search.return_value = [
|
||||
{'key': 'attack-on-titan', 'name': 'Attack on Titan'}
|
||||
]
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db = MagicMock()
|
||||
mock_get_db.__aenter__.return_value = mock_db
|
||||
mock_get_db.__aexit__.return_value = None
|
||||
|
||||
with patch(
|
||||
'src.server.services.initialization_service.settings'
|
||||
) as mock_settings, \
|
||||
patch(
|
||||
'src.server.utils.dependencies.get_series_app',
|
||||
return_value=mock_series_app
|
||||
), \
|
||||
patch(
|
||||
'src.server.database.connection.get_db_session',
|
||||
return_value=mock_get_db
|
||||
), \
|
||||
patch(
|
||||
'src.server.services.initialization_service.AnimeSeriesService.get_by_folder',
|
||||
new_callable=AsyncMock, return_value=None
|
||||
), \
|
||||
patch(
|
||||
'src.server.services.initialization_service.AnimeSeriesService.create',
|
||||
new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
'src.server.services.initialization_service.SetupService.run',
|
||||
new_callable=AsyncMock, return_value=1
|
||||
) as mock_setup_run:
|
||||
mock_settings.anime_directory = anime_dir
|
||||
|
||||
result = await _scan_folders_to_database()
|
||||
|
||||
assert result == 1
|
||||
call_kwargs = mock_create.call_args.kwargs
|
||||
assert call_kwargs['key'] == 'attack-on-titan'
|
||||
mock_setup_run.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_folders_no_match_leaves_key_empty(self, tmp_path):
|
||||
"""Search returns 0 results → key=''."""
|
||||
"""Search returns 0 results → key=''.
|
||||
|
||||
This test verifies the integration between _scan_folders_to_database
|
||||
and SetupService.run(). Since _scan_folders_to_database now delegates
|
||||
to SetupService.run(), we mock SetupService.run() and verify it's called.
|
||||
"""
|
||||
anime_dir = tmp_path / "anime"
|
||||
anime_dir.mkdir()
|
||||
folder_path = anime_dir / "Unknown Series (2020)"
|
||||
folder_path.mkdir()
|
||||
|
||||
mock_series_app = AsyncMock()
|
||||
mock_series_app.search.return_value = []
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db = MagicMock()
|
||||
mock_get_db.__aenter__.return_value = mock_db
|
||||
mock_get_db.__aexit__.return_value = None
|
||||
|
||||
with patch(
|
||||
'src.server.services.initialization_service.settings'
|
||||
) as mock_settings, \
|
||||
patch(
|
||||
'src.server.utils.dependencies.get_series_app',
|
||||
return_value=mock_series_app
|
||||
), \
|
||||
patch(
|
||||
'src.server.database.connection.get_db_session',
|
||||
return_value=mock_get_db
|
||||
), \
|
||||
patch(
|
||||
'src.server.services.initialization_service.AnimeSeriesService.get_by_folder',
|
||||
new_callable=AsyncMock, return_value=None
|
||||
), \
|
||||
patch(
|
||||
'src.server.services.initialization_service.AnimeSeriesService.create',
|
||||
new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
'src.server.services.initialization_service.SetupService.run',
|
||||
new_callable=AsyncMock, return_value=1
|
||||
) as mock_setup_run:
|
||||
mock_settings.anime_directory = anime_dir
|
||||
|
||||
result = await _scan_folders_to_database()
|
||||
|
||||
assert result == 1
|
||||
call_kwargs = mock_create.call_args.kwargs
|
||||
assert call_kwargs['key'] == ''
|
||||
mock_setup_run.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_folders_multiple_matches_leaves_key_empty(self, tmp_path):
|
||||
"""Search returns >1 results → key=''."""
|
||||
"""Search returns >1 results → key=''.
|
||||
|
||||
This test verifies the integration between _scan_folders_to_database
|
||||
and SetupService.run(). Since _scan_folders_to_database now delegates
|
||||
to SetupService.run(), we mock SetupService.run() and verify it's called.
|
||||
"""
|
||||
anime_dir = tmp_path / "anime"
|
||||
anime_dir.mkdir()
|
||||
folder_path = anime_dir / "Attack on Titan (2013)"
|
||||
folder_path.mkdir()
|
||||
|
||||
mock_series_app = AsyncMock()
|
||||
mock_series_app.search.return_value = [
|
||||
{'key': 'attack-on-titan', 'name': 'Attack on Titan'},
|
||||
{'key': 'attack-on-titan-clone', 'name': 'Attack on Titan Clone'}
|
||||
]
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db = MagicMock()
|
||||
mock_get_db.__aenter__.return_value = mock_db
|
||||
mock_get_db.__aexit__.return_value = None
|
||||
|
||||
with patch(
|
||||
'src.server.services.initialization_service.settings'
|
||||
) as mock_settings, \
|
||||
patch(
|
||||
'src.server.utils.dependencies.get_series_app',
|
||||
return_value=mock_series_app
|
||||
), \
|
||||
patch(
|
||||
'src.server.database.connection.get_db_session',
|
||||
return_value=mock_get_db
|
||||
), \
|
||||
patch(
|
||||
'src.server.services.initialization_service.AnimeSeriesService.get_by_folder',
|
||||
new_callable=AsyncMock, return_value=None
|
||||
), \
|
||||
patch(
|
||||
'src.server.services.initialization_service.AnimeSeriesService.create',
|
||||
new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
'src.server.services.initialization_service.SetupService.run',
|
||||
new_callable=AsyncMock, return_value=1
|
||||
) as mock_setup_run:
|
||||
mock_settings.anime_directory = anime_dir
|
||||
|
||||
result = await _scan_folders_to_database()
|
||||
|
||||
assert result == 1
|
||||
call_kwargs = mock_create.call_args.kwargs
|
||||
assert call_kwargs['key'] == ''
|
||||
mock_setup_run.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_folders_skips_existing(self, tmp_path):
|
||||
"""Series with same folder already in DB → skip."""
|
||||
"""Series with same folder already in DB → skip.
|
||||
|
||||
This test verifies the integration between _scan_folders_to_database
|
||||
and SetupService.run(). Since _scan_folders_to_database now delegates
|
||||
to SetupService.run(), we mock SetupService.run() and verify it's called.
|
||||
"""
|
||||
anime_dir = tmp_path / "anime"
|
||||
anime_dir.mkdir()
|
||||
folder_path = anime_dir / "Attack on Titan (2013)"
|
||||
folder_path.mkdir()
|
||||
|
||||
mock_series_app = AsyncMock()
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db = MagicMock()
|
||||
mock_get_db.__aenter__.return_value = mock_db
|
||||
mock_get_db.__aexit__.return_value = None
|
||||
|
||||
with patch(
|
||||
'src.server.services.initialization_service.settings'
|
||||
) as mock_settings, \
|
||||
patch(
|
||||
'src.server.utils.dependencies.get_series_app',
|
||||
return_value=mock_series_app
|
||||
), \
|
||||
patch(
|
||||
'src.server.database.connection.get_db_session',
|
||||
return_value=mock_get_db
|
||||
), \
|
||||
patch(
|
||||
'src.server.services.initialization_service.AnimeSeriesService.get_by_folder',
|
||||
new_callable=AsyncMock, return_value=MagicMock() # existing series
|
||||
), \
|
||||
patch(
|
||||
'src.server.services.initialization_service.AnimeSeriesService.create',
|
||||
new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
'src.server.services.initialization_service.SetupService.run',
|
||||
new_callable=AsyncMock, return_value=0
|
||||
) as mock_setup_run:
|
||||
mock_settings.anime_directory = anime_dir
|
||||
|
||||
result = await _scan_folders_to_database()
|
||||
|
||||
assert result == 0
|
||||
mock_create.assert_not_called()
|
||||
mock_setup_run.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_folders_empty_anime_directory(self):
|
||||
"""No anime directory configured → return 0."""
|
||||
with patch(
|
||||
'src.server.services.initialization_service.settings'
|
||||
) as mock_settings:
|
||||
) as mock_settings, \
|
||||
patch(
|
||||
'src.server.services.initialization_service.SetupService.run',
|
||||
new_callable=AsyncMock, return_value=0
|
||||
) as mock_setup_run:
|
||||
mock_settings.anime_directory = None
|
||||
|
||||
result = await _scan_folders_to_database()
|
||||
|
||||
assert result == 0
|
||||
mock_setup_run.assert_not_called()
|
||||
|
||||
Reference in New Issue
Block a user