Replaces the read-only 'NFO Diagnostics' page with a full per-anime
Settings page reached from the right-click context menu on series cards.
Users can now view and edit key, name, folder, tmdb_id, tvdb_id and site
for each anime; changes are persisted to the DB and optionally written
back to the NFO file or used to regenerate it.
Backend
- Rename NfoDiagnosticsResponse -> NfoSettingsResponse,
NfoSeriesDiagnostics -> NfoSeriesSettings
- Rename get_nfo_diagnostics -> get_nfo_settings,
repair_nfo -> repair_nfo_settings
- Fix nfo.py bug: repair was calling non-existent
update_series_nfo_status(); now uses update_nfo_status() and an
explicit AnimeSeriesService.update(nfo_path=...)
- New endpoints on /api/anime/{key}:
GET /settings -> AnimeSettingsResponse
PUT /settings -> AnimeSettingsResponse
(body: name/folder/tmdb_id/tvdb_id/site,
options: apply_to_nfo, rename_disk)
POST /regenerate-nfo -> AnimeSettingsRegenerateNfoResponse
- New Pydantic models: AnimeSettingsResponse,
AnimeSettingsUpdateRequest, AnimeSettingsRegenerateNfoResponse
- /anime/settings page route; /settings/nfo now 301-redirects to it
Frontend
- New AniWorld.AnimeSettingsManager JS module (single-page form,
no tabs) with public API init/loadSeries/saveSettings/regenerateNfo/
validateField/populateForm/showSaveSuccess/showError
- New anime-settings.html template + anime-settings.css
- Right-click menu: data-action 'nfo-diagnostics' replaced by
'anime-settings' (label 'Anime Settings'), navigates to
/anime/settings?key=...
- Library 'Open NFO Diagnostics' link renamed to 'Open Anime Settings'
Bug fix
- context-menu click handler was calling hide() BEFORE building the
navigation URL, which cleared currentSeriesKey to null and produced
/anime/settings?key=null. Captures the key into a local const first.
Regression-locked by tests/frontend/unit/context_menu.test.js.
Tests
- 21 new pytest tests in tests/api/test_anime_settings_endpoints.py
(GET/PUT/regenerate-nfo, auth, validation, nfo-repair bug regression)
- tests/api/test_nfo_endpoints.py trimmed to 6 focused tests
- 31 new Vitest unit tests for AnimeSettingsManager
- 5 new Vitest unit tests for ContextMenu (incl. source-invariant
regression guard for the hide()-before-key bug)
- 5 new Playwright E2E tests covering right-click, direct nav,
legacy /settings/nfo redirect, and context-menu labels
- New vitest.config.js (environment: happy-dom)
Docs
- Docs/API.md: new section 'Anime Settings Endpoints'
- Docs/CHANGELOG.md: documents the rename and the context-menu bug fix
Verified
- pytest: 27/27 (21 new + 6 trimmed nfo)
- vitest: 36/36 (31 anime-settings + 5 context-menu)
- playwright e2e: 5/5
106 lines
3.5 KiB
Python
106 lines
3.5 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/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).
|
|
"""
|
|
from unittest.mock import AsyncMock, Mock, 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(autouse=True)
|
|
def reset_auth():
|
|
auth_service._hash = None
|
|
auth_service._failed.clear()
|
|
yield
|
|
auth_service._hash = None
|
|
auth_service._failed.clear()
|
|
|
|
|
|
@pytest.fixture
|
|
async def 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):
|
|
await client.post(
|
|
"/api/auth/setup",
|
|
json={"master_password": "TestPassword123!"},
|
|
)
|
|
resp = await client.post(
|
|
"/api/auth/login",
|
|
json={"password": "TestPassword123!"},
|
|
)
|
|
token = resp.json()["access_token"]
|
|
client.headers.update({"Authorization": f"Bearer {token}"})
|
|
yield client
|
|
|
|
|
|
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)
|
|
|
|
|
|
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") |