- Add PUT /anime/{key} endpoint for updating anime key, tmdb_id, tvdb_id
- Add NFO diagnostics and repair endpoints (GET/POST /nfo/diagnostics)
- Add edit modal UI with context menu integration
- Add frontend JS modules for context-menu and edit-modal
- Add comprehensive tests for edit, rename, and NFO repair flows
256 lines
8.3 KiB
Python
256 lines
8.3 KiB
Python
"""Tests for anime metadata edit (PUT /api/anime/{anime_key}) endpoint."""
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from src.server.fastapi_app import app
|
|
from src.server.services.auth_service import auth_service
|
|
|
|
|
|
@pytest.fixture
|
|
def reset_auth():
|
|
"""Reset auth state before each test."""
|
|
auth_service._hash = None
|
|
auth_service._failed = {}
|
|
|
|
|
|
@pytest.fixture
|
|
async def client():
|
|
"""Create async test client."""
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
|
yield ac
|
|
|
|
|
|
@pytest.fixture
|
|
async def authenticated_client(client):
|
|
"""Get authenticated client with Bearer token."""
|
|
# Setup auth
|
|
await client.post("/api/auth/setup", json={"master_password": "TestPass123!"})
|
|
response = await client.post(
|
|
"/api/auth/login", json={"password": "TestPass123!"}
|
|
)
|
|
token = response.json()["access_token"]
|
|
client.headers["Authorization"] = f"Bearer {token}"
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_db_session():
|
|
"""Create a mock async database session."""
|
|
session = AsyncMock()
|
|
session.commit = AsyncMock()
|
|
session.flush = AsyncMock()
|
|
session.refresh = AsyncMock()
|
|
return session
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_series_in_db():
|
|
"""Create a mock AnimeSeries DB record."""
|
|
series = MagicMock()
|
|
series.id = 1
|
|
series.key = "test-anime"
|
|
series.name = "Test Anime"
|
|
series.tmdb_id = 1234
|
|
series.tvdb_id = 5678
|
|
series.folder = "Test Anime (2023)"
|
|
return series
|
|
|
|
|
|
@pytest.fixture
|
|
def override_db_dependency(mock_db_session):
|
|
"""Override database session dependency."""
|
|
from src.server.utils.dependencies import get_database_session
|
|
|
|
app.dependency_overrides[get_database_session] = lambda: mock_db_session
|
|
yield mock_db_session
|
|
app.dependency_overrides.pop(get_database_session, None)
|
|
|
|
|
|
class TestUpdateAnimeMetadata:
|
|
"""Tests for PUT /api/anime/{anime_key}."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_tmdb_id_success(
|
|
self, reset_auth, authenticated_client, override_db_dependency, mock_series_in_db
|
|
):
|
|
"""Test successful tmdb_id update."""
|
|
with patch(
|
|
"src.server.api.anime.AnimeSeriesService.get_by_key",
|
|
new_callable=AsyncMock,
|
|
return_value=mock_series_in_db,
|
|
), patch(
|
|
"src.server.api.anime.AnimeSeriesService.update",
|
|
new_callable=AsyncMock,
|
|
) as mock_update:
|
|
mock_series_in_db.tmdb_id = 9999
|
|
mock_update.return_value = mock_series_in_db
|
|
|
|
response = await authenticated_client.put(
|
|
"/api/anime/test-anime",
|
|
json={"tmdb_id": 9999},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["tmdb_id"] == 9999
|
|
assert data["message"] == "Metadata updated successfully"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_tvdb_id_success(
|
|
self, reset_auth, authenticated_client, override_db_dependency, mock_series_in_db
|
|
):
|
|
"""Test successful tvdb_id update."""
|
|
with patch(
|
|
"src.server.api.anime.AnimeSeriesService.get_by_key",
|
|
new_callable=AsyncMock,
|
|
return_value=mock_series_in_db,
|
|
), patch(
|
|
"src.server.api.anime.AnimeSeriesService.update",
|
|
new_callable=AsyncMock,
|
|
) as mock_update:
|
|
mock_series_in_db.tvdb_id = 7777
|
|
mock_update.return_value = mock_series_in_db
|
|
|
|
response = await authenticated_client.put(
|
|
"/api/anime/test-anime",
|
|
json={"tvdb_id": 7777},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["tvdb_id"] == 7777
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_key_success(
|
|
self, reset_auth, authenticated_client, override_db_dependency, mock_series_in_db
|
|
):
|
|
"""Test successful key rename."""
|
|
with patch(
|
|
"src.server.api.anime.AnimeSeriesService.get_by_key",
|
|
new_callable=AsyncMock,
|
|
) as mock_get:
|
|
# First call finds the series, second call checks uniqueness (returns None)
|
|
mock_get.side_effect = [mock_series_in_db, None]
|
|
|
|
mock_series_in_db.key = "new-anime-key"
|
|
with patch(
|
|
"src.server.api.anime.AnimeSeriesService.update",
|
|
new_callable=AsyncMock,
|
|
return_value=mock_series_in_db,
|
|
):
|
|
response = await authenticated_client.put(
|
|
"/api/anime/test-anime",
|
|
json={"key": "new-anime-key"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["key"] == "new-anime-key"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_key_conflict_409(
|
|
self, reset_auth, authenticated_client, override_db_dependency, mock_series_in_db
|
|
):
|
|
"""Test key rename conflict returns 409."""
|
|
existing_series = MagicMock()
|
|
existing_series.key = "existing-key"
|
|
|
|
with patch(
|
|
"src.server.api.anime.AnimeSeriesService.get_by_key",
|
|
new_callable=AsyncMock,
|
|
) as mock_get:
|
|
# First call finds original series, second call finds conflict
|
|
mock_get.side_effect = [mock_series_in_db, existing_series]
|
|
|
|
response = await authenticated_client.put(
|
|
"/api/anime/test-anime",
|
|
json={"key": "existing-key"},
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
assert "already exists" in response.json()["detail"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_key_invalid_chars_422(
|
|
self, reset_auth, authenticated_client, override_db_dependency
|
|
):
|
|
"""Test key with invalid characters returns 422."""
|
|
response = await authenticated_client.put(
|
|
"/api/anime/test-anime",
|
|
json={"key": "Invalid Key With Spaces!"},
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_key_empty_422(
|
|
self, reset_auth, authenticated_client, override_db_dependency
|
|
):
|
|
"""Test empty key returns 422."""
|
|
response = await authenticated_client.put(
|
|
"/api/anime/test-anime",
|
|
json={"key": ""},
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_unauthenticated_401(self, reset_auth, client):
|
|
"""Test unauthenticated access returns 401."""
|
|
response = await client.put(
|
|
"/api/anime/test-anime",
|
|
json={"tmdb_id": 1234},
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_nonexistent_anime_404(
|
|
self, reset_auth, authenticated_client, override_db_dependency
|
|
):
|
|
"""Test update of non-existent anime returns 404."""
|
|
with patch(
|
|
"src.server.api.anime.AnimeSeriesService.get_by_key",
|
|
new_callable=AsyncMock,
|
|
return_value=None,
|
|
):
|
|
response = await authenticated_client.put(
|
|
"/api/anime/nonexistent-key",
|
|
json={"tmdb_id": 1234},
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_no_changes(
|
|
self, reset_auth, authenticated_client, override_db_dependency, mock_series_in_db
|
|
):
|
|
"""Test sending empty body returns no-op response."""
|
|
with patch(
|
|
"src.server.api.anime.AnimeSeriesService.get_by_key",
|
|
new_callable=AsyncMock,
|
|
return_value=mock_series_in_db,
|
|
):
|
|
response = await authenticated_client.put(
|
|
"/api/anime/test-anime",
|
|
json={},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["message"] == "No changes"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_negative_tmdb_id_422(
|
|
self, reset_auth, authenticated_client, override_db_dependency
|
|
):
|
|
"""Test negative TMDB ID returns 422."""
|
|
response = await authenticated_client.put(
|
|
"/api/anime/test-anime",
|
|
json={"tmdb_id": -5},
|
|
)
|
|
|
|
assert response.status_code == 422
|