Files
Aniworld/tests/api/test_nfo_endpoints.py
Lukas 94f4cc69c4 feat: Task 5 - Add NFO Management API Endpoints (85% complete)
- Create NFO API models (11 Pydantic models)
- Implement 8 REST API endpoints for NFO management
- Register NFO router in FastAPI app
- Create 18 comprehensive API tests
- Add detailed status documentation

Endpoints:
- GET /api/nfo/{id}/check - Check NFO/media status
- POST /api/nfo/{id}/create - Create NFO & media
- PUT /api/nfo/{id}/update - Update NFO
- GET /api/nfo/{id}/content - Get NFO content
- GET /api/nfo/{id}/media/status - Media status
- POST /api/nfo/{id}/media/download - Download media
- POST /api/nfo/batch/create - Batch operations
- GET /api/nfo/missing - List missing NFOs

Remaining: Refactor to use series_app dependency pattern
2026-01-15 20:06:37 +01:00

496 lines
16 KiB
Python

"""Tests for NFO API endpoints.
This module tests all NFO management REST API endpoints.
"""
import pytest
from httpx import ASGITransport, AsyncClient
from unittest.mock import AsyncMock, Mock, patch
from src.server.fastapi_app import app
from src.server.services.auth_service import auth_service
from src.server.models.nfo import (
MediaFilesStatus,
NFOCheckResponse,
NFOCreateResponse,
)
@pytest.fixture(autouse=True)
def reset_auth():
"""Reset authentication state before each test."""
original_hash = auth_service._hash
auth_service._hash = None
auth_service._failed.clear()
yield
auth_service._hash = original_hash
auth_service._failed.clear()
@pytest.fixture
async def client():
"""Create an 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):
"""Create an authenticated test client with token."""
# Setup master password
await client.post(
"/api/auth/setup",
json={"master_password": "TestPassword123!"}
)
# Login to get token
response = await client.post(
"/api/auth/login",
json={"password": "TestPassword123!"}
)
token = response.json()["access_token"]
# Add token to default headers
client.headers.update({"Authorization": f"Bearer {token}"})
yield client
@pytest.fixture
def mock_anime_service():
"""Create mock anime service."""
service = Mock()
serie = Mock()
serie.key = "test-anime"
serie.folder = "Test Anime (2024)"
serie.name = "Test Anime"
service.get_series_list = Mock(return_value=[serie])
return service
@pytest.fixture
def mock_nfo_service():
"""Create mock NFO service."""
service = Mock()
service.check_nfo_exists = AsyncMock(return_value=False)
service.create_tvshow_nfo = AsyncMock(return_value="/path/to/tvshow.nfo")
service.update_tvshow_nfo = AsyncMock(return_value="/path/to/tvshow.nfo")
return service
class TestNFOCheckEndpoint:
"""Tests for GET /api/nfo/{serie_id}/check endpoint."""
@pytest.mark.asyncio
async def test_check_nfo_requires_auth(self, client):
"""Test that check endpoint requires authentication."""
response = await client.get("/api/nfo/test-anime/check")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_check_nfo_series_not_found(
self,
authenticated_client,
mock_anime_service,
mock_nfo_service
):
"""Test check endpoint with non-existent series."""
mock_anime_service.get_series_list = Mock(return_value=[])
with patch(
'src.server.api.nfo.get_anime_service',
return_value=mock_anime_service
), patch(
'src.server.api.nfo.get_nfo_service',
return_value=mock_nfo_service
):
response = await authenticated_client.get(
"/api/nfo/nonexistent/check"
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_check_nfo_success(
self,
authenticated_client,
mock_anime_service,
mock_nfo_service,
tmp_path
):
"""Test successful NFO check."""
with patch('src.server.api.nfo.settings') as mock_settings, \
patch(
'src.server.api.nfo.get_anime_service',
return_value=mock_anime_service
), \
patch(
'src.server.api.nfo.get_nfo_service',
return_value=mock_nfo_service
):
mock_settings.anime_directory = str(tmp_path)
response = await authenticated_client.get(
"/api/nfo/test-anime/check"
)
assert response.status_code == 200
data = response.json()
assert data["serie_id"] == "test-anime"
assert data["serie_folder"] == "Test Anime (2024)"
assert data["has_nfo"] is False
class TestNFOCreateEndpoint:
"""Tests for POST /api/nfo/{serie_id}/create endpoint."""
@pytest.mark.asyncio
async def test_create_nfo_requires_auth(self, client):
"""Test that create endpoint requires authentication."""
response = await client.post(
"/api/nfo/test-anime/create",
json={}
)
assert response.status_code == 401
@pytest.mark.asyncio
async def test_create_nfo_success(
self,
authenticated_client,
mock_anime_service,
mock_nfo_service,
tmp_path
):
"""Test successful NFO creation."""
with patch('src.server.api.nfo.settings') as mock_settings, \
patch(
'src.server.api.nfo.get_anime_service',
return_value=mock_anime_service
), \
patch(
'src.server.api.nfo.get_nfo_service',
return_value=mock_nfo_service
):
mock_settings.anime_directory = str(tmp_path)
response = await authenticated_client.post(
"/api/nfo/test-anime/create",
json={
"download_poster": True,
"download_logo": True,
"download_fanart": True
}
)
assert response.status_code == 200
data = response.json()
assert data["serie_id"] == "test-anime"
assert "NFO and media files created successfully" in data["message"]
@pytest.mark.asyncio
async def test_create_nfo_already_exists(
self,
authenticated_client,
mock_anime_service,
mock_nfo_service,
tmp_path
):
"""Test NFO creation when NFO already exists."""
mock_nfo_service.check_nfo_exists = AsyncMock(return_value=True)
with patch('src.server.api.nfo.settings') as mock_settings, \
patch(
'src.server.api.nfo.get_anime_service',
return_value=mock_anime_service
), \
patch(
'src.server.api.nfo.get_nfo_service',
return_value=mock_nfo_service
):
mock_settings.anime_directory = str(tmp_path)
response = await authenticated_client.post(
"/api/nfo/test-anime/create",
json={"overwrite_existing": False}
)
assert response.status_code == 409
@pytest.mark.asyncio
async def test_create_nfo_with_year(
self,
authenticated_client,
mock_anime_service,
mock_nfo_service,
tmp_path
):
"""Test NFO creation with year parameter."""
with patch('src.server.api.nfo.settings') as mock_settings, \
patch(
'src.server.api.nfo.get_anime_service',
return_value=mock_anime_service
), \
patch(
'src.server.api.nfo.get_nfo_service',
return_value=mock_nfo_service
):
mock_settings.anime_directory = str(tmp_path)
response = await authenticated_client.post(
"/api/nfo/test-anime/create",
json={
"year": 2024,
"download_poster": True
}
)
assert response.status_code == 200
# Verify year was passed to service
mock_nfo_service.create_tvshow_nfo.assert_called_once()
call_kwargs = mock_nfo_service.create_tvshow_nfo.call_args[1]
assert call_kwargs["year"] == 2024
class TestNFOUpdateEndpoint:
"""Tests for PUT /api/nfo/{serie_id}/update endpoint."""
@pytest.mark.asyncio
async def test_update_nfo_requires_auth(self, client):
"""Test that update endpoint requires authentication."""
response = await client.put("/api/nfo/test-anime/update")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_update_nfo_not_found(
self,
authenticated_client,
mock_anime_service,
mock_nfo_service,
tmp_path
):
"""Test update when NFO doesn't exist."""
mock_nfo_service.check_nfo_exists = AsyncMock(return_value=False)
with patch('src.server.api.nfo.settings') as mock_settings, \
patch(
'src.server.api.nfo.get_anime_service',
return_value=mock_anime_service
), \
patch(
'src.server.api.nfo.get_nfo_service',
return_value=mock_nfo_service
):
mock_settings.anime_directory = str(tmp_path)
response = await authenticated_client.put(
"/api/nfo/test-anime/update"
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_update_nfo_success(
self,
authenticated_client,
mock_anime_service,
mock_nfo_service,
tmp_path
):
"""Test successful NFO update."""
mock_nfo_service.check_nfo_exists = AsyncMock(return_value=True)
with patch('src.server.api.nfo.settings') as mock_settings, \
patch(
'src.server.api.nfo.get_anime_service',
return_value=mock_anime_service
), \
patch(
'src.server.api.nfo.get_nfo_service',
return_value=mock_nfo_service
):
mock_settings.anime_directory = str(tmp_path)
response = await authenticated_client.put(
"/api/nfo/test-anime/update?download_media=true"
)
assert response.status_code == 200
data = response.json()
assert "NFO updated successfully" in data["message"]
class TestNFOContentEndpoint:
"""Tests for GET /api/nfo/{serie_id}/content endpoint."""
@pytest.mark.asyncio
async def test_get_content_requires_auth(self, client):
"""Test that content endpoint requires authentication."""
response = await client.get("/api/nfo/test-anime/content")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_get_content_nfo_not_found(
self,
authenticated_client,
mock_anime_service,
mock_nfo_service,
tmp_path
):
"""Test get content when NFO doesn't exist."""
with patch('src.server.api.nfo.settings') as mock_settings, \
patch(
'src.server.api.nfo.get_anime_service',
return_value=mock_anime_service
), \
patch(
'src.server.api.nfo.get_nfo_service',
return_value=mock_nfo_service
):
mock_settings.anime_directory = str(tmp_path)
response = await authenticated_client.get(
"/api/nfo/test-anime/content"
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_get_content_success(
self,
authenticated_client,
mock_anime_service,
mock_nfo_service,
tmp_path
):
"""Test successful content retrieval."""
# Create NFO file
anime_dir = tmp_path / "Test Anime (2024)"
anime_dir.mkdir()
nfo_file = anime_dir / "tvshow.nfo"
nfo_file.write_text("<tvshow><title>Test</title></tvshow>")
with patch('src.server.api.nfo.settings') as mock_settings, \
patch(
'src.server.api.nfo.get_anime_service',
return_value=mock_anime_service
), \
patch(
'src.server.api.nfo.get_nfo_service',
return_value=mock_nfo_service
):
mock_settings.anime_directory = str(tmp_path)
response = await authenticated_client.get(
"/api/nfo/test-anime/content"
)
assert response.status_code == 200
data = response.json()
assert "<tvshow>" in data["content"]
assert data["file_size"] > 0
class TestNFOMissingEndpoint:
"""Tests for GET /api/nfo/missing endpoint."""
@pytest.mark.asyncio
async def test_get_missing_requires_auth(self, client):
"""Test that missing endpoint requires authentication."""
response = await client.get("/api/nfo/missing")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_get_missing_success(
self,
authenticated_client,
mock_anime_service,
mock_nfo_service,
tmp_path
):
"""Test getting list of series without NFO."""
with patch('src.server.api.nfo.settings') as mock_settings, \
patch(
'src.server.api.nfo.get_anime_service',
return_value=mock_anime_service
), \
patch(
'src.server.api.nfo.get_nfo_service',
return_value=mock_nfo_service
):
mock_settings.anime_directory = str(tmp_path)
response = await authenticated_client.get("/api/nfo/missing")
assert response.status_code == 200
data = response.json()
assert "total_series" in data
assert "missing_nfo_count" in data
assert "series" in data
class TestNFOBatchCreateEndpoint:
"""Tests for POST /api/nfo/batch/create endpoint."""
@pytest.mark.asyncio
async def test_batch_create_requires_auth(self, client):
"""Test that batch create endpoint requires authentication."""
response = await client.post(
"/api/nfo/batch/create",
json={"serie_ids": ["test1", "test2"]}
)
assert response.status_code == 401
@pytest.mark.asyncio
async def test_batch_create_success(
self,
authenticated_client,
mock_anime_service,
mock_nfo_service,
tmp_path
):
"""Test successful batch NFO creation."""
with patch('src.server.api.nfo.settings') as mock_settings, \
patch(
'src.server.api.nfo.get_anime_service',
return_value=mock_anime_service
), \
patch(
'src.server.api.nfo.get_nfo_service',
return_value=mock_nfo_service
):
mock_settings.anime_directory = str(tmp_path)
response = await authenticated_client.post(
"/api/nfo/batch/create",
json={
"serie_ids": ["test-anime"],
"download_media": True,
"skip_existing": False,
"max_concurrent": 3
}
)
assert response.status_code == 200
data = response.json()
assert data["total"] == 1
assert "successful" in data
assert "results" in data
class TestNFOServiceDependency:
"""Tests for NFO service dependency."""
@pytest.mark.asyncio
async def test_nfo_service_unavailable_without_api_key(
self,
authenticated_client
):
"""Test NFO endpoints fail gracefully without TMDB API key."""
with patch('src.server.api.nfo.settings') as mock_settings:
mock_settings.tmdb_api_key = None
response = await authenticated_client.get(
"/api/nfo/test-anime/check"
)
assert response.status_code == 503
assert "not configured" in response.json()["detail"]