Files
Aniworld/tests/api/test_nfo_endpoints.py
AniWorld Dev 9f52ea03fb fix(anime-settings): restore GET /api/nfo/{key}/content for 'View NFO XML' button
The Anime Settings page (src/server/web/static/js/pages/anime-settings.js)
calls GET /api/nfo/{key}/content from its 'View NFO XML' button, but that
endpoint was removed during the NFO refactor (commits 21af502, a8e5487).
The frontend was never updated, so every click on the button 404'd.

Fix:
- Add NfoContentResponse model (key, folder, content, file_size,
  last_modified) to src/server/models/nfo.py.
- Add GET /api/nfo/{key}/content handler to src/server/api/nfo.py that
  reads <anime_directory>/<folder>/tvshow.nfo and returns it as
  {"content": "<xml>", ...} — matching what anime-settings.js
  viewNfoContent() already expects (data.content).
- Expose viewNfoContent on AniWorld.AnimeSettingsManager so it is
  consistent with the other public methods and directly callable from
  tests / other modules.

Tests:
- tests/api/test_nfo_endpoints.py: 4 new tests (auth-required, happy
  path returning XML, 404 on unknown series, 404 on missing tvshow.nfo).
  Also remove the file-local autouse 'reset_auth' fixture that wiped
  the conftest's master-password setup and made any login-based test
  fail with a stale-hash 'invalid credentials' error — that fixture
  was pre-existing and is a no-op now that conftest.py handles reset.
- tests/frontend/unit/anime_settings.test.js: 3 new tests for
  viewNfoContent (URL + auth header, writes <pre>, error toast on
  404) and an assertion in the public-API surface test.
2026-09-04 19:10:27 +02:00

216 lines
7.7 KiB
Python

"""Tests for the NFO Management API endpoints.
Covers the live endpoints in src/server/api/nfo.py:
- GET /api/nfo/{key}/diagnostics
- POST /api/nfo/{key}/repair
- GET /api/nfo/{key}/validate
- GET /api/nfo/{key}/content (re-introduced — used by Anime Settings 'View NFO XML')
- GET /api/nfo/needs-repair
- POST /api/nfo/batch/repair
Note: Legacy endpoints referenced in v1.x (e.g. /api/nfo/{id}/check,
/create, /update, /content, /missing, /batch/create) no longer exist
in the codebase — they were replaced by the consolidated diagnostic,
repair, validate, needs-repair, batch/repair endpoints and the new
Anime Settings page (see tests/api/test_anime_settings_endpoints.py).
Auth note: tests/conftest.py's autouse ``reset_auth_and_rate_limits``
fixture configures the master password with ``TestPass123!`` before every
test. The per-file ``reset_auth`` autouse fixture that used to live here
was removed because it wiped the conftest's setup and made any test that
needed an authenticated client fail with a stale-hash login error.
"""
from unittest.mock import AsyncMock, Mock, patch
import pytest
from httpx import ASGITransport, AsyncClient
from src.server.fastapi_app import app
@pytest.fixture
async def client():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
async def _login(client: AsyncClient) -> str:
"""Log in with the master password configured by conftest and
return the bearer token. Sets the ``Authorization`` header on the
client as a side benefit so the caller can ``await client.get(...)``
immediately."""
resp = await client.post(
"/api/auth/login",
json={"password": "TestPass123!"},
)
assert resp.status_code == 200, resp.text
token = resp.json()["access_token"]
client.headers.update({"Authorization": f"Bearer {token}"})
return token
class TestNFOAuthRequirements:
"""All NFO endpoints must require authentication."""
@pytest.mark.asyncio
async def test_get_diagnostics_requires_auth(self, client):
resp = await client.get("/api/nfo/any-key/diagnostics")
assert resp.status_code in (401, 503)
@pytest.mark.asyncio
async def test_post_repair_requires_auth(self, client):
resp = await client.post("/api/nfo/any-key/repair")
assert resp.status_code in (401, 503)
@pytest.mark.asyncio
async def test_get_validate_requires_auth(self, client):
resp = await client.get("/api/nfo/any-key/validate")
assert resp.status_code in (401, 503)
@pytest.mark.asyncio
async def test_get_needs_repair_requires_auth(self, client):
resp = await client.get("/api/nfo/needs-repair")
assert resp.status_code in (401, 503)
@pytest.mark.asyncio
async def test_post_batch_repair_requires_auth(self, client):
resp = await client.post(
"/api/nfo/batch/repair",
json=["key1", "key2"],
)
assert resp.status_code in (401, 503)
@pytest.mark.asyncio
async def test_get_content_requires_auth(self, client):
"""GET /api/nfo/{key}/content (used by the Anime Settings page
'View NFO XML' button) must require authentication."""
resp = await client.get("/api/nfo/any-key/content")
assert resp.status_code in (401, 503)
class TestNFOContentEndpoint:
"""Behavioural tests for GET /api/nfo/{key}/content.
Covers the success path and the two 404 cases (unknown series,
missing tvshow.nfo) the Anime Settings page relies on."""
@pytest.fixture
def mock_anime_service(self):
"""Replace the FastAPI get_anime_service dependency with a mock.
Yields the mock so individual tests can configure ``list_series_with_filters``."""
from src.server.utils import dependencies as deps
service = Mock()
service.list_series_with_filters = AsyncMock(return_value=[])
app.dependency_overrides[deps.get_anime_service] = lambda: service
yield service
app.dependency_overrides.pop(deps.get_anime_service, None)
@pytest.mark.asyncio
async def test_returns_xml_for_series_with_nfo(
self, client, tmp_path, monkeypatch, mock_anime_service
):
"""Happy path: existing tvshow.nfo is returned verbatim inside
the JSON wrapper the JS uses (``data.content``)."""
from src.config import settings as settings_module
# Point settings.anime_directory at a temp dir
monkeypatch.setattr(
settings_module.settings,
"anime_directory",
str(tmp_path),
raising=False,
)
# Build a fake folder + tvshow.nfo on disk
folder = "Naruto (2002)"
series_dir = tmp_path / folder
series_dir.mkdir()
xml = (
"<?xml version='1.0' encoding='UTF-8'?>\n"
"<tvshow><title>Naruto</title><year>2002</year></tvshow>\n"
)
(series_dir / "tvshow.nfo").write_text(xml, encoding="utf-8")
mock_anime_service.list_series_with_filters = AsyncMock(
return_value=[{"key": "naruto", "folder": folder}]
)
await _login(client)
resp = await client.get("/api/nfo/naruto/content")
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["key"] == "naruto"
assert body["folder"] == folder
assert body["content"] == xml
assert body["file_size"] == len(xml.encode("utf-8"))
assert "last_modified" in body
@pytest.mark.asyncio
async def test_404_when_series_unknown(
self, client, tmp_path, monkeypatch, mock_anime_service
):
from src.config import settings as settings_module
monkeypatch.setattr(
settings_module.settings,
"anime_directory",
str(tmp_path),
raising=False,
)
mock_anime_service.list_series_with_filters = AsyncMock(return_value=[])
await _login(client)
resp = await client.get("/api/nfo/missing/content")
assert resp.status_code == 404
assert "not found" in resp.json()["detail"].lower()
@pytest.mark.asyncio
async def test_404_when_nfo_file_missing(
self, client, tmp_path, monkeypatch, mock_anime_service
):
"""Series exists with a configured folder but no tvshow.nfo yet."""
from src.config import settings as settings_module
folder = "Empty"
(tmp_path / folder).mkdir()
monkeypatch.setattr(
settings_module.settings,
"anime_directory",
str(tmp_path),
raising=False,
)
mock_anime_service.list_series_with_filters = AsyncMock(
return_value=[{"key": "empty", "folder": folder}]
)
await _login(client)
resp = await client.get("/api/nfo/empty/content")
assert resp.status_code == 404
assert "tvshow.nfo" in resp.json()["detail"].lower()
class TestNFOEndpointModels:
"""Verify the response models use the renamed classes (regression
test for the rename from NfoDiagnosticsResponse -> NfoSettingsResponse)."""
def test_renamed_settings_response_model_exists(self):
# Confirm the old names are gone
from src.server import models
from src.server.models.nfo import (
NfoRepairResponse,
NfoSeriesSettings,
NfoSettingsResponse,
)
nfo_module = models.nfo
assert hasattr(nfo_module, "NfoSettingsResponse")
assert hasattr(nfo_module, "NfoSeriesSettings")
assert hasattr(nfo_module, "NfoRepairResponse")
# The diagnostic prefix should NOT be present anymore
assert not hasattr(nfo_module, "NfoDiagnosticsResponse")
assert not hasattr(nfo_module, "NfoSeriesDiagnostics")