"""Superadmin Übungs-Anreicherung — Auth, Merge-Logik, Status, Analyze.""" from __future__ import annotations import os from unittest.mock import MagicMock, patch import pytest from fastapi.testclient import TestClient os.environ.setdefault("SKIP_DB_MIGRATE", "1") from auth import require_auth from exercise_enrichment import ( apply_exercise_enrichment, compute_skill_diff, estimate_llm_calls, merge_skills, persist_merged_skills, validate_exercise_for_enrichment, ) from main import app @pytest.fixture def client() -> TestClient: return TestClient(app) @pytest.fixture(autouse=True) def _clear_overrides(): yield app.dependency_overrides.pop(require_auth, None) def test_candidates_requires_superadmin(client: TestClient) -> None: def _admin(): return {"profile_id": 1, "role": "admin"} app.dependency_overrides[require_auth] = _admin r = client.get("/api/admin/exercise-enrichment/candidates", headers={"X-Auth-Token": "t"}) assert r.status_code == 403 def test_preview_requires_superadmin(client: TestClient) -> None: def _trainer(): return {"profile_id": 1, "role": "trainer"} app.dependency_overrides[require_auth] = _trainer r = client.post( "/api/admin/exercise-enrichment/preview", headers={"X-Auth-Token": "t", "Content-Type": "application/json"}, json={"exercise_ids": [1], "modes": {"skills": True}}, ) assert r.status_code == 403 @patch("routers.exercise_enrichment_admin.get_db") def test_candidates_ok_for_superadmin(mock_get_db, client: TestClient) -> None: def _super(): return {"profile_id": 1, "role": "superadmin"} app.dependency_overrides[require_auth] = _super mock_cm = MagicMock() mock_conn = MagicMock() mock_cm.__enter__.return_value = mock_conn mock_cm.__exit__.return_value = False mock_cur = MagicMock() mock_cur.fetchone.side_effect = [{"c": 2}, None] mock_cur.fetchall.return_value = [ { "id": 10, "title": "Kata Basics", "status": "draft", "visibility": "private", "summary": "", "updated_at": "2026-05-23T10:00:00", "primary_focus_name": "Karate", "skill_count": 0, "ai_suggested_skill_count": 0, } ] mock_get_db.return_value = mock_cm with patch("routers.exercise_enrichment_admin.get_cursor", return_value=mock_cur): r = client.get( "/api/admin/exercise-enrichment/candidates?status=draft&without_skills=true", headers={"X-Auth-Token": "t"}, ) assert r.status_code == 200 body = r.json() assert body["total"] == 2 assert len(body["items"]) == 1 assert body["items"][0]["id"] == 10 def test_analyze_returns_llm_estimate(client: TestClient) -> None: def _super(): return {"profile_id": 1, "role": "superadmin"} app.dependency_overrides[require_auth] = _super r = client.post( "/api/admin/exercise-enrichment/analyze", headers={"X-Auth-Token": "t", "Content-Type": "application/json"}, json={ "exercise_ids": [1, 2, 3], "modes": {"skills": True, "summary": True, "instructions": False}, }, ) assert r.status_code == 200 body = r.json() assert body["exercise_count"] == 3 est = body["estimated_llm_calls"] assert est["total"] == 6 assert est["skills"] == 3 assert est["summary"] == 3 def test_validate_exercise_requires_title_and_content() -> None: assert ( validate_exercise_for_enrichment({"title": "", "goal": "
x
"}, want_skills=True) == "Titel fehlt" ) assert ( validate_exercise_for_enrichment({"title": "Foo", "goal": "", "execution": ""}, want_skills=True) == "Mindestens Ziel oder Durchführung muss Inhalt liefern (für Skills/Kurzfassung)" ) assert validate_exercise_for_enrichment({"title": "Foo", "goal": "Ziel
"}, want_skills=True) is None def test_merge_skills_additive_keeps_manual() -> None: existing = [ { "skill_id": 1, "skill_name": "Manual", "intensity": "hoch", "required_level": "aufbau", "target_level": "fortgeschritten", "is_primary": True, "ai_suggested": False, } ] suggested = [ { "skill_id": 1, "skill_name": "Manual AI", "intensity": "niedrig", "required_level": "basis", "target_level": "grundlagen", "is_primary": False, }, { "skill_id": 2, "skill_name": "New AI", "intensity": "mittel", "required_level": "grundlagen", "target_level": "aufbau", "is_primary": False, }, ] merged = merge_skills(existing, suggested, "additive") assert len(merged) == 2 manual = next(s for s in merged if s["skill_id"] == 1) assert manual["intensity"] == "hoch" assert manual["ai_suggested"] is False ai_new = next(s for s in merged if s["skill_id"] == 2) assert ai_new["ai_suggested"] is True assert ai_new["intensity"] == "mittel" def test_merge_skills_replace_all_marks_ai() -> None: existing = [ {"skill_id": 1, "skill_name": "M", "intensity": "hoch", "ai_suggested": False}, ] suggested = [ {"skill_id": 3, "skill_name": "New AI", "intensity": "niedrig", "required_level": "basis", "target_level": "aufbau"}, ] merged = merge_skills(existing, suggested, "replace_all") assert len(merged) == 1 assert merged[0]["skill_id"] == 3 assert merged[0]["ai_suggested"] is True assert merged[0]["intensity"] == "niedrig" def test_compute_skill_diff_added_and_removed() -> None: before = [{"skill_id": 1, "skill_name": "A", "intensity": "mittel", "ai_suggested": False}] after = [ {"skill_id": 1, "skill_name": "A", "intensity": "mittel", "ai_suggested": False}, {"skill_id": 2, "skill_name": "B", "intensity": "hoch", "ai_suggested": True}, ] diff = compute_skill_diff(before, after) assert len(diff["added"]) == 1 assert diff["added"][0]["skill_id"] == 2 @patch("exercise_enrichment.enrich_exercise_detail") def test_apply_sets_in_review(mock_enrich) -> None: mock_enrich.return_value = { "id": 5, "title": "Test", "goal": "G
", "execution": "", "status": "draft", "skills": [], } mock_cur = MagicMock() row = apply_exercise_enrichment( mock_cur, 5, merged_skills=[ { "skill_id": 9, "skill_name": "Kick", "intensity": "mittel", "required_level": "grundlagen", "target_level": "aufbau", "is_primary": True, "ai_suggested": True, } ], merge_mode="replace_all", set_status="in_review", apply_skills=True, ) assert row["ok"] is True assert row["status"] == "in_review" assert mock_cur.execute.call_count >= 2 def test_apply_rejects_approved_status() -> None: mock_cur = MagicMock() with patch( "exercise_enrichment.enrich_exercise_detail", return_value={ "title": "T", "goal": "G
", "status": "draft", "skills": [], }, ): row = apply_exercise_enrichment( mock_cur, 1, merged_skills=[{"skill_id": 1, "intensity": "mittel", "ai_suggested": True}], set_status="approved", apply_skills=True, ) assert row["ok"] is False assert "approved" in row["error"] def test_persist_merged_skills_uses_upsert() -> None: mock_cur = MagicMock() persist_merged_skills( mock_cur, 7, [ { "skill_id": 3, "intensity": "mittel", "required_level": "grundlagen", "target_level": "aufbau", "is_primary": False, "ai_suggested": True, } ], "replace_all", ) sql = mock_cur.execute.call_args_list[-1][0][0] assert "INSERT INTO exercise_skills" in sql def test_estimate_llm_calls_breakdown() -> None: est = estimate_llm_calls( exercise_count=100, want_skills=True, want_summary=False, want_instructions=True, ) assert est["total"] == 200 assert est["per_exercise"] == 2