Files
Aniworld/tests/unit/test_unresolved_folder_service.py
Lukas ecef21eec4 feat(setup): track unresolved folders for manual key resolution
When SetupService cannot auto-resolve a provider key for an anime folder,
the folder is now tracked in the new 'unresolved_folders' table instead of
being silently skipped. Users can then resolve these via the new API:

- GET /api/setup/unresolved - list unresolved folders with search suggestions
- POST /api/setup/unresolved/{folder}/resolve - provide key to resolve folder

The SetupService.run() now:
- Tracks unresolved folders instead of skipping them
- Re-creates AnimeSeries for previously unresolved folders that are now resolved
- Includes unresolved count in logs

New files:
- src/server/api/setup_endpoints.py - API endpoints for unresolved management
- tests/unit/test_unresolved_folder_service.py - service and model tests

Modified:
- src/server/database/models.py - add UnresolvedFolder model
- src/server/database/service.py - add UnresolvedFolderService
- src/server/services/setup_service.py - track unresolved folders
- src/server/fastapi_app.py - include setup router
2026-06-05 21:07:52 +02:00

244 lines
8.0 KiB
Python

"""Tests for UnresolvedFolderService and UnresolvedFolder model."""
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.server.database.models import UnresolvedFolder
from src.server.database.service import UnresolvedFolderService
class TestUnresolvedFolderModel:
"""Test UnresolvedFolder model."""
def test_is_resolved_false_when_no_key(self):
"""provider_key is None → is_resolved is False."""
folder = UnresolvedFolder(
folder_name="Test (2020)",
title="Test",
year=2020,
provider_key=None,
resolved_at=None,
)
assert folder.is_resolved is False
def test_is_resolved_false_when_key_but_no_timestamp(self):
"""provider_key set but resolved_at is None → is_resolved is False."""
folder = UnresolvedFolder(
folder_name="Test (2020)",
title="Test",
year=2020,
provider_key="test-key",
resolved_at=None,
)
assert folder.is_resolved is False
def test_is_resolved_true_when_both_set(self):
"""Both provider_key and resolved_at set → is_resolved is True."""
folder = UnresolvedFolder(
folder_name="Test (2020)",
title="Test",
year=2020,
provider_key="test-key",
resolved_at=datetime.now(timezone.utc),
)
assert folder.is_resolved is True
def test_validate_folder_name_empty_raises(self):
"""Empty folder_name → raises ValueError during construction."""
with pytest.raises(ValueError, match="Folder name cannot be empty"):
UnresolvedFolder(
folder_name="",
title="Test",
)
def test_validate_folder_name_too_long_raises(self):
"""Folder name > 1000 chars → raises ValueError during construction."""
long_name = "x" * 1001
with pytest.raises(ValueError, match="Folder name must be 1000 characters"):
UnresolvedFolder(
folder_name=long_name,
title="Test",
)
def test_validate_title_empty_raises(self):
"""Empty title → raises ValueError during construction."""
with pytest.raises(ValueError, match="Title cannot be empty"):
UnresolvedFolder(
folder_name="Test (2020)",
title="",
)
class TestUnresolvedFolderService:
"""Test UnresolvedFolderService methods."""
@pytest.mark.asyncio
async def test_create(self):
"""Creates a new unresolved folder record."""
mock_db = AsyncMock()
mock_db.flush = AsyncMock()
mock_db.refresh = AsyncMock()
folder = await UnresolvedFolderService.create(
db=mock_db,
folder_name="Test (2020)",
title="Test",
year=2020,
search_attempts=1,
)
assert folder.folder_name == "Test (2020)"
assert folder.title == "Test"
assert folder.year == 2020
assert folder.search_attempts == 1
mock_db.add.assert_called_once()
mock_db.flush.assert_called_once()
@pytest.mark.asyncio
async def test_get_by_folder_name_found(self):
"""Found → returns UnresolvedFolder."""
mock_db = AsyncMock()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = UnresolvedFolder(
folder_name="Test (2020)",
title="Test",
year=2020,
)
mock_db.execute.return_value = mock_result
folder = await UnresolvedFolderService.get_by_folder_name(mock_db, "Test (2020)")
assert folder is not None
assert folder.folder_name == "Test (2020)"
@pytest.mark.asyncio
async def test_get_by_folder_name_not_found(self):
"""Not found → returns None."""
mock_db = AsyncMock()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = None
mock_db.execute.return_value = mock_result
folder = await UnresolvedFolderService.get_by_folder_name(mock_db, "Unknown")
assert folder is None
@pytest.mark.asyncio
async def test_get_all_unresolved(self):
"""Returns only unresolved folders (no provider_key)."""
mock_db = AsyncMock()
mock_result = MagicMock()
mock_result.scalars.return_value.all.return_value = [
UnresolvedFolder(folder_name="Folder1", title="Title1", year=2020),
UnresolvedFolder(folder_name="Folder2", title="Title2", year=2021),
]
mock_db.execute.return_value = mock_result
folders = await UnresolvedFolderService.get_all_unresolved(mock_db)
assert len(folders) == 2
mock_db.execute.assert_called_once()
@pytest.mark.asyncio
async def test_resolve(self):
"""Marks folder as resolved with provider_key."""
mock_db = AsyncMock()
mock_db.flush = AsyncMock()
mock_db.refresh = AsyncMock()
existing = UnresolvedFolder(
folder_name="Test (2020)",
title="Test",
year=2020,
provider_key=None,
resolved_at=None,
)
with patch.object(
UnresolvedFolderService, 'get_by_folder_name',
new_callable=AsyncMock, return_value=existing
):
result = await UnresolvedFolderService.resolve(
mock_db, "Test (2020)", "test-key"
)
assert result.provider_key == "test-key"
assert result.resolved_at is not None
mock_db.flush.assert_called_once()
@pytest.mark.asyncio
async def test_resolve_not_found(self):
"""Folder not found → returns None."""
mock_db = AsyncMock()
with patch.object(
UnresolvedFolderService, 'get_by_folder_name',
new_callable=AsyncMock, return_value=None
):
result = await UnresolvedFolderService.resolve(
mock_db, "Unknown", "test-key"
)
assert result is None
@pytest.mark.asyncio
async def test_delete(self):
"""Deletes unresolved folder record."""
mock_db = AsyncMock()
mock_db.flush = AsyncMock()
existing = UnresolvedFolder(
folder_name="Test (2020)",
title="Test",
year=2020,
)
with patch.object(
UnresolvedFolderService, 'get_by_folder_name',
new_callable=AsyncMock, return_value=existing
):
result = await UnresolvedFolderService.delete(mock_db, "Test (2020)")
assert result is True
mock_db.delete.assert_called_once_with(existing)
mock_db.flush.assert_called_once()
@pytest.mark.asyncio
async def test_delete_not_found(self):
"""Folder not found → returns False."""
mock_db = AsyncMock()
with patch.object(
UnresolvedFolderService, 'get_by_folder_name',
new_callable=AsyncMock, return_value=None
):
result = await UnresolvedFolderService.delete(mock_db, "Unknown")
assert result is False
@pytest.mark.asyncio
async def test_update_search_result(self):
"""Increments search_attempts and updates last_search_result."""
mock_db = AsyncMock()
mock_db.flush = AsyncMock()
mock_db.refresh = AsyncMock()
existing = UnresolvedFolder(
folder_name="Test (2020)",
title="Test",
year=2020,
search_attempts=1,
last_search_result=None,
)
with patch.object(
UnresolvedFolderService, 'get_by_folder_name',
new_callable=AsyncMock, return_value=existing
):
result = await UnresolvedFolderService.update_search_result(
mock_db, "Test (2020)", '[{"title": "Test"}]'
)
assert result.search_attempts == 2
assert result.last_search_result == '[{"title": "Test"}]'