feat(admin): Provider & Sprache — Eigener-Wert-Eingabe + Tests (v14)
app.js: default_stt/llm/tts_provider und default_language von 'select' auf 'combo' umgestellt, damit neben den vordefinierten Optionen jederzeit ein beliebiger eigener Wert eingegeben werden kann (Zukunftssicherheit). combo-Renderer unterstützt jetzt auch meta.labels (Sprachkürzel → Klarname). tests/test_admin_settings.py: 28 Tests für GET/PUT/DELETE /api/admin/config — Auth, bekannte Werte, Custom-Values, Persistenz, Runtime-Wirkung. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a892896d32
commit
4a4cbe643d
4 changed files with 199 additions and 9 deletions
191
tests/test_admin_settings.py
Normal file
191
tests/test_admin_settings.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
"""Tests für den Admin-Einstellungen-Tab (Provider & Sprache + Laufzeit-Config).
|
||||
|
||||
Getestet: GET /api/admin/config, PUT /api/admin/config/{key},
|
||||
DELETE /api/admin/config/{key} — Auth, Custom-Values, Persistenz.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import app.dependencies as deps
|
||||
from app.main import app
|
||||
from app.config import settings
|
||||
from app.runtime_config import RUNTIME_SETTABLE, invalidate_cache
|
||||
|
||||
client = TestClient(app)
|
||||
ADMIN = "test-admin-key"
|
||||
ADM_HDR = {"X-Admin-Key": ADMIN}
|
||||
|
||||
PROVIDER_KEYS = [
|
||||
"default_stt_provider",
|
||||
"default_llm_provider",
|
||||
"default_tts_provider",
|
||||
"default_language",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(monkeypatch):
|
||||
monkeypatch.setattr(settings, "admin_api_key", ADMIN)
|
||||
monkeypatch.setattr(settings, "auth_enabled", False)
|
||||
invalidate_cache()
|
||||
yield
|
||||
invalidate_cache()
|
||||
|
||||
|
||||
# ── GET /api/admin/config ────────────────────────────────────────────────────
|
||||
|
||||
def test_get_config_returns_all_keys():
|
||||
resp = client.get("/api/admin/config", headers=ADM_HDR)
|
||||
assert resp.status_code == 200
|
||||
keys = {e["key"] for e in resp.json()}
|
||||
assert keys == set(RUNTIME_SETTABLE.keys())
|
||||
|
||||
|
||||
def test_get_config_requires_admin_key():
|
||||
resp = client.get("/api/admin/config")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
def test_get_config_provider_keys_present():
|
||||
resp = client.get("/api/admin/config", headers=ADM_HDR)
|
||||
keys = {e["key"] for e in resp.json()}
|
||||
for k in PROVIDER_KEYS:
|
||||
assert k in keys, f"Schlüssel {k!r} fehlt in /api/admin/config"
|
||||
|
||||
|
||||
def test_get_config_entry_structure():
|
||||
resp = client.get("/api/admin/config", headers=ADM_HDR)
|
||||
entry = next(e for e in resp.json() if e["key"] == "default_stt_provider")
|
||||
assert "label" in entry
|
||||
assert "hint" in entry
|
||||
assert "effective_value" in entry
|
||||
assert "base_value" in entry
|
||||
assert "is_overridden" in entry
|
||||
assert entry["is_overridden"] is False
|
||||
|
||||
|
||||
def test_get_config_not_overridden_initially():
|
||||
resp = client.get("/api/admin/config", headers=ADM_HDR)
|
||||
for entry in resp.json():
|
||||
if entry["key"] in PROVIDER_KEYS:
|
||||
assert not entry["is_overridden"], (
|
||||
f"{entry['key']} sollte initial nicht überschrieben sein"
|
||||
)
|
||||
|
||||
|
||||
# ── PUT /api/admin/config/{key} ──────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.parametrize("key,value", [
|
||||
("default_stt_provider", "openrouter"),
|
||||
("default_stt_provider", "faster-whisper"),
|
||||
("default_llm_provider", "openrouter"),
|
||||
("default_llm_provider", "local-openai-compatible"),
|
||||
("default_tts_provider", "openrouter"),
|
||||
("default_tts_provider", "piper"),
|
||||
("default_tts_provider", "chatterbox"),
|
||||
("default_language", "de"),
|
||||
("default_language", "en"),
|
||||
])
|
||||
def test_put_known_value_accepted(key, value):
|
||||
resp = client.put(f"/api/admin/config/{key}", headers=ADM_HDR, json={"value": value})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["value"] == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key,custom_value", [
|
||||
("default_stt_provider", "my-custom-stt"),
|
||||
("default_llm_provider", "local-openai-compatible-v2"),
|
||||
("default_tts_provider", "future-tts-engine"),
|
||||
("default_language", "pl"),
|
||||
])
|
||||
def test_put_custom_value_accepted(key, custom_value):
|
||||
"""Eigener Wert (nicht in der Dropdown-Liste) muss akzeptiert werden."""
|
||||
resp = client.put(f"/api/admin/config/{key}", headers=ADM_HDR, json={"value": custom_value})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["value"] == custom_value
|
||||
|
||||
|
||||
def test_put_custom_value_reflected_in_get():
|
||||
"""Gespeicherter Custom-Wert erscheint in effective_value."""
|
||||
client.put("/api/admin/config/default_stt_provider", headers=ADM_HDR,
|
||||
json={"value": "my-new-stt"})
|
||||
invalidate_cache()
|
||||
resp = client.get("/api/admin/config", headers=ADM_HDR)
|
||||
entry = next(e for e in resp.json() if e["key"] == "default_stt_provider")
|
||||
assert entry["effective_value"] == "my-new-stt"
|
||||
assert entry["is_overridden"] is True
|
||||
|
||||
|
||||
def test_put_unknown_key_returns_400():
|
||||
resp = client.put("/api/admin/config/nonexistent_key", headers=ADM_HDR,
|
||||
json={"value": "x"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_put_trims_whitespace():
|
||||
resp = client.put("/api/admin/config/default_stt_provider", headers=ADM_HDR,
|
||||
json={"value": " openrouter "})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["value"] == "openrouter"
|
||||
|
||||
|
||||
def test_put_requires_admin_key():
|
||||
resp = client.put("/api/admin/config/default_stt_provider", json={"value": "x"})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
# ── DELETE /api/admin/config/{key} ──────────────────────────────────────────
|
||||
|
||||
def test_delete_removes_override():
|
||||
client.put("/api/admin/config/default_tts_provider", headers=ADM_HDR,
|
||||
json={"value": "future-engine"})
|
||||
invalidate_cache()
|
||||
|
||||
del_resp = client.delete("/api/admin/config/default_tts_provider", headers=ADM_HDR)
|
||||
assert del_resp.status_code == 200
|
||||
assert del_resp.json()["deleted"] == "default_tts_provider"
|
||||
|
||||
invalidate_cache()
|
||||
cfg = {e["key"]: e for e in
|
||||
client.get("/api/admin/config", headers=ADM_HDR).json()}
|
||||
assert not cfg["default_tts_provider"]["is_overridden"]
|
||||
|
||||
|
||||
def test_delete_nonexistent_override_returns_404():
|
||||
resp = client.delete("/api/admin/config/default_tts_provider", headers=ADM_HDR)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_delete_unknown_key_returns_400():
|
||||
resp = client.delete("/api/admin/config/does_not_exist", headers=ADM_HDR)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_delete_requires_admin_key():
|
||||
resp = client.delete("/api/admin/config/default_tts_provider")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
# ── Persistenz: Override wirkt sich auf Runtime aus ──────────────────────────
|
||||
|
||||
def test_override_reflected_in_runtime_settings():
|
||||
from app.runtime_config import runtime_settings
|
||||
client.put("/api/admin/config/default_language", headers=ADM_HDR,
|
||||
json={"value": "fr"})
|
||||
invalidate_cache()
|
||||
assert runtime_settings.default_language == "fr"
|
||||
|
||||
|
||||
def test_delete_restores_base_value():
|
||||
from app.runtime_config import runtime_settings
|
||||
base = str(settings.default_language)
|
||||
|
||||
client.put("/api/admin/config/default_language", headers=ADM_HDR,
|
||||
json={"value": "it"})
|
||||
invalidate_cache()
|
||||
assert runtime_settings.default_language == "it"
|
||||
|
||||
client.delete("/api/admin/config/default_language", headers=ADM_HDR)
|
||||
invalidate_cache()
|
||||
assert str(runtime_settings.default_language) == base
|
||||
Loading…
Add table
Add a link
Reference in a new issue