my_voice_assistant_v2/tests/test_admin_settings.py
Dieter Schlüter 4a4cbe643d 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>
2026-06-19 12:30:52 +02:00

191 lines
6.8 KiB
Python

"""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