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