""" Tests für den Communication Service. Testet die KI-gestützte Lehrer-Eltern-Kommunikation mit GFK-Prinzipien. """ import pytest from unittest.mock import AsyncMock, patch, MagicMock from llm_gateway.services.communication_service import ( CommunicationService, CommunicationType, CommunicationTone, LegalReference, GFKPrinciple, get_communication_service, fetch_legal_references_from_db, parse_db_references_to_legal_refs, FALLBACK_LEGAL_REFERENCES, GFK_PRINCIPLES, ) class TestCommunicationType: """Tests für CommunicationType Enum.""" def test_all_communication_types_exist(self): """Test alle erwarteten Kommunikationstypen existieren.""" expected_types = [ "general_info", "behavior", "academic", "attendance", "meeting_invite", "positive_feedback", "concern", "conflict", "special_needs", ] actual_types = [ct.value for ct in CommunicationType] assert set(expected_types) == set(actual_types) def test_communication_type_is_string_enum(self): """Test CommunicationType ist String Enum.""" assert CommunicationType.BEHAVIOR == "behavior" assert CommunicationType.ACADEMIC.value == "academic" class TestCommunicationTone: """Tests für CommunicationTone Enum.""" def test_all_tones_exist(self): """Test alle Tonalitäten existieren.""" expected_tones = ["formal", "professional", "warm", "concerned", "appreciative"] actual_tones = [t.value for t in CommunicationTone] assert set(expected_tones) == set(actual_tones) class TestLegalReference: """Tests für LegalReference Dataclass.""" def test_legal_reference_creation(self): """Test LegalReference erstellen.""" ref = LegalReference( law="SchulG NRW", paragraph="§ 42", title="Pflichten der Eltern", summary="Eltern unterstützen die Schule.", relevance="Kooperationsaufforderungen", ) assert ref.law == "SchulG NRW" assert ref.paragraph == "§ 42" assert "Eltern" in ref.title class TestGFKPrinciple: """Tests für GFKPrinciple Dataclass.""" def test_gfk_principle_creation(self): """Test GFKPrinciple erstellen.""" principle = GFKPrinciple( principle="Beobachtung", description="Konkrete Handlungen beschreiben", example="Ich habe bemerkt...", ) assert principle.principle == "Beobachtung" assert "beschreiben" in principle.description class TestFallbackLegalReferences: """Tests für die Fallback-Referenzen.""" def test_default_references_exist(self): """Test DEFAULT Referenzen existieren.""" assert "DEFAULT" in FALLBACK_LEGAL_REFERENCES assert "elternpflichten" in FALLBACK_LEGAL_REFERENCES["DEFAULT"] assert "schulpflicht" in FALLBACK_LEGAL_REFERENCES["DEFAULT"] def test_fallback_references_are_legal_reference(self): """Test Fallback Referenzen sind LegalReference Objekte.""" ref = FALLBACK_LEGAL_REFERENCES["DEFAULT"]["elternpflichten"] assert isinstance(ref, LegalReference) assert ref.law == "Landesschulgesetz" class TestGFKPrinciples: """Tests für GFK-Prinzipien.""" def test_four_gfk_principles_exist(self): """Test alle 4 GFK-Prinzipien existieren.""" assert len(GFK_PRINCIPLES) == 4 principles = [p.principle for p in GFK_PRINCIPLES] assert "Beobachtung" in principles assert "Gefühle" in principles assert "Bedürfnisse" in principles assert "Bitten" in principles class TestCommunicationService: """Tests für CommunicationService Klasse.""" def test_service_initialization(self): """Test Service wird korrekt initialisiert.""" service = CommunicationService() assert service.fallback_references is not None assert service.gfk_principles is not None assert service.templates is not None assert service._cached_references == {} def test_get_legal_references_sync(self): """Test synchrone get_legal_references Methode (Fallback).""" service = CommunicationService() refs = service.get_legal_references("NRW", "elternpflichten") assert len(refs) > 0 assert isinstance(refs[0], LegalReference) def test_get_fallback_references(self): """Test _get_fallback_references Methode.""" service = CommunicationService() refs = service._get_fallback_references("DEFAULT", "elternpflichten") assert len(refs) == 1 assert refs[0].law == "Landesschulgesetz" def test_get_gfk_guidance(self): """Test get_gfk_guidance gibt GFK-Prinzipien zurück.""" service = CommunicationService() guidance = service.get_gfk_guidance(CommunicationType.BEHAVIOR) assert len(guidance) == 4 assert all(isinstance(g, GFKPrinciple) for g in guidance) def test_get_template(self): """Test get_template gibt Vorlage zurück.""" service = CommunicationService() template = service.get_template(CommunicationType.MEETING_INVITE) assert "subject" in template assert "opening" in template assert "closing" in template assert "Einladung" in template["subject"] def test_get_template_fallback(self): """Test get_template Fallback zu GENERAL_INFO.""" service = CommunicationService() # Unbekannter Typ sollte auf GENERAL_INFO fallen template = service.get_template(CommunicationType.GENERAL_INFO) assert "Information" in template["subject"] class TestCommunicationServiceAsync: """Async Tests für CommunicationService.""" @pytest.mark.asyncio async def test_get_legal_references_async_with_db(self): """Test async get_legal_references_async mit DB-Daten.""" service = CommunicationService() # Mock die fetch_legal_references_from_db Funktion mock_docs = [ { "law_name": "SchulG NRW", "title": "Schulgesetz NRW", "paragraphs": [ {"nr": "§ 42", "title": "Pflichten der Eltern"}, {"nr": "§ 41", "title": "Schulpflicht"}, ], } ] with patch( "llm_gateway.services.communication_service.fetch_legal_references_from_db", new_callable=AsyncMock, ) as mock_fetch: mock_fetch.return_value = mock_docs refs = await service.get_legal_references_async("NRW", "elternpflichten") assert len(refs) > 0 mock_fetch.assert_called_once_with("NRW") @pytest.mark.asyncio async def test_get_legal_references_async_fallback(self): """Test async get_legal_references_async Fallback wenn DB leer.""" service = CommunicationService() with patch( "llm_gateway.services.communication_service.fetch_legal_references_from_db", new_callable=AsyncMock, ) as mock_fetch: mock_fetch.return_value = [] # Leere DB refs = await service.get_legal_references_async("NRW", "elternpflichten") # Sollte Fallback nutzen assert len(refs) > 0 assert refs[0].law == "Landesschulgesetz" @pytest.mark.asyncio async def test_get_legal_references_async_caching(self): """Test dass Ergebnisse gecached werden.""" service = CommunicationService() mock_docs = [ { "law_name": "SchulG NRW", "paragraphs": [{"nr": "§ 42", "title": "Pflichten der Eltern"}], } ] with patch( "llm_gateway.services.communication_service.fetch_legal_references_from_db", new_callable=AsyncMock, ) as mock_fetch: mock_fetch.return_value = mock_docs # Erster Aufruf await service.get_legal_references_async("NRW", "elternpflichten") # Zweiter Aufruf sollte Cache nutzen await service.get_legal_references_async("NRW", "elternpflichten") # fetch sollte nur einmal aufgerufen werden assert mock_fetch.call_count == 1 class TestFetchLegalReferencesFromDB: """Tests für fetch_legal_references_from_db Funktion.""" @pytest.mark.asyncio async def test_fetch_success(self): """Test erfolgreicher API-Aufruf.""" mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "documents": [ {"law_name": "SchulG NRW", "paragraphs": []}, ] } with patch("httpx.AsyncClient") as mock_client: mock_instance = AsyncMock() mock_instance.get.return_value = mock_response mock_instance.__aenter__.return_value = mock_instance mock_instance.__aexit__.return_value = None mock_client.return_value = mock_instance docs = await fetch_legal_references_from_db("NRW") assert len(docs) == 1 assert docs[0]["law_name"] == "SchulG NRW" @pytest.mark.asyncio async def test_fetch_api_error(self): """Test API-Fehler gibt leere Liste zurück.""" mock_response = MagicMock() mock_response.status_code = 500 with patch("httpx.AsyncClient") as mock_client: mock_instance = AsyncMock() mock_instance.get.return_value = mock_response mock_instance.__aenter__.return_value = mock_instance mock_instance.__aexit__.return_value = None mock_client.return_value = mock_instance docs = await fetch_legal_references_from_db("NRW") assert docs == [] @pytest.mark.asyncio async def test_fetch_network_error(self): """Test Netzwerkfehler gibt leere Liste zurück.""" import httpx with patch("httpx.AsyncClient") as mock_client: mock_instance = AsyncMock() mock_instance.get.side_effect = httpx.ConnectError("Connection failed") mock_instance.__aenter__.return_value = mock_instance mock_instance.__aexit__.return_value = None mock_client.return_value = mock_instance docs = await fetch_legal_references_from_db("NRW") assert docs == [] class TestParseDbReferencesToLegalRefs: """Tests für parse_db_references_to_legal_refs Funktion.""" def test_parse_with_matching_paragraphs(self): """Test Parsing mit passenden Paragraphen.""" db_docs = [ { "law_name": "SchulG NRW", "title": "Schulgesetz", "paragraphs": [ {"nr": "§ 42", "title": "Pflichten der Eltern"}, {"nr": "§ 1", "title": "Bildungsauftrag"}, ], } ] refs = parse_db_references_to_legal_refs(db_docs, "elternpflichten") assert len(refs) > 0 # § 42 sollte für elternpflichten relevant sein assert any("42" in r.paragraph for r in refs) def test_parse_without_paragraphs(self): """Test Parsing ohne Paragraphen.""" db_docs = [ { "law_name": "SchulG NRW", "title": "Schulgesetz", "paragraphs": [], } ] refs = parse_db_references_to_legal_refs(db_docs, "elternpflichten") # Sollte trotzdem Referenz erstellen assert len(refs) == 1 assert refs[0].law == "SchulG NRW" def test_parse_empty_docs(self): """Test Parsing mit leerer Dokumentenliste.""" refs = parse_db_references_to_legal_refs([], "elternpflichten") assert refs == [] class TestBuildSystemPrompt: """Tests für build_system_prompt Methode.""" def test_build_system_prompt_contains_gfk(self): """Test System-Prompt enthält GFK-Prinzipien.""" service = CommunicationService() prompt = service.build_system_prompt( CommunicationType.BEHAVIOR, "NRW", CommunicationTone.PROFESSIONAL, ) # Prüfe Großbuchstaben-Varianten (wie im Prompt verwendet) assert "BEOBACHTUNG" in prompt assert "GEFÜHLE" in prompt assert "BEDÜRFNISSE" in prompt assert "BITTEN" in prompt def test_build_system_prompt_contains_tone(self): """Test System-Prompt enthält Tonalität.""" service = CommunicationService() prompt = service.build_system_prompt( CommunicationType.BEHAVIOR, "NRW", CommunicationTone.WARM, ) assert "warmherzig" in prompt.lower() class TestBuildUserPrompt: """Tests für build_user_prompt Methode.""" def test_build_user_prompt_with_context(self): """Test User-Prompt mit Kontext.""" service = CommunicationService() prompt = service.build_user_prompt( CommunicationType.BEHAVIOR, { "student_name": "Max", "parent_name": "Frau Müller", "situation": "Max stört häufig den Unterricht.", "additional_info": "Bereits 3x ermahnt.", }, ) assert "Max" in prompt assert "Frau Müller" in prompt assert "stört" in prompt assert "ermahnt" in prompt class TestValidateCommunication: """Tests für validate_communication Methode.""" def test_validate_good_communication(self): """Test Validierung einer guten Kommunikation.""" service = CommunicationService() text = """ Sehr geehrte Frau Müller, ich habe bemerkt, dass Max in letzter Zeit häufiger abwesend war. Ich möchte Sie gerne zu einem Gespräch einladen, da mir eine gute Zusammenarbeit sehr wichtig ist. Wären Sie bereit, nächste Woche zu einem Termin zu kommen? Mit freundlichen Grüßen """ result = service.validate_communication(text) assert result["is_valid"] is True assert len(result["positive_elements"]) > 0 assert result["gfk_score"] > 0.5 def test_validate_bad_communication(self): """Test Validierung einer problematischen Kommunikation.""" service = CommunicationService() text = """ Sehr geehrte Frau Müller, Sie müssen endlich etwas tun! Das Kind ist faul und respektlos. Sie sollten mehr kontrollieren. """ result = service.validate_communication(text) assert result["is_valid"] is False assert len(result["issues"]) > 0 assert len(result["suggestions"]) > 0 class TestGetAllCommunicationTypes: """Tests für get_all_communication_types Methode.""" def test_returns_all_types(self): """Test gibt alle Typen zurück.""" service = CommunicationService() types = service.get_all_communication_types() assert len(types) == 9 assert all("value" in t and "label" in t for t in types) class TestGetAllTones: """Tests für get_all_tones Methode.""" def test_returns_all_tones(self): """Test gibt alle Tonalitäten zurück.""" service = CommunicationService() tones = service.get_all_tones() assert len(tones) == 5 assert all("value" in t and "label" in t for t in tones) class TestGetStates: """Tests für get_states Methode.""" def test_returns_all_16_bundeslaender(self): """Test gibt alle 16 Bundesländer zurück.""" service = CommunicationService() states = service.get_states() assert len(states) == 16 # Prüfe einige state_values = [s["value"] for s in states] assert "NRW" in state_values assert "BY" in state_values assert "BE" in state_values class TestGetCommunicationService: """Tests für Singleton-Pattern.""" def test_singleton_pattern(self): """Test dass get_communication_service immer dieselbe Instanz zurückgibt.""" service1 = get_communication_service() service2 = get_communication_service() assert service1 is service2 def test_returns_communication_service(self): """Test dass CommunicationService zurückgegeben wird.""" service = get_communication_service() assert isinstance(service, CommunicationService)