""" Unit Tests for Mail Module Tests for: - TaskService: Priority calculation, deadline handling - AIEmailService: Sender classification, deadline extraction - Models: Validation, known authorities """ import pytest from datetime import datetime, timedelta from unittest.mock import AsyncMock, MagicMock, patch # Import the modules to test import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from mail.models import ( TaskPriority, TaskStatus, SenderType, EmailCategory, KNOWN_AUTHORITIES_NI, DeadlineExtraction, EmailAccountCreate, TaskCreate, classify_sender_by_domain, ) from mail.task_service import TaskService from mail.ai_service import AIEmailService class TestKnownAuthoritiesNI: """Tests for Niedersachsen authority domain matching.""" def test_kultusministerium_domain(self): """Test that MK Niedersachsen domain is recognized.""" assert "@mk.niedersachsen.de" in KNOWN_AUTHORITIES_NI assert KNOWN_AUTHORITIES_NI["@mk.niedersachsen.de"]["type"] == SenderType.KULTUSMINISTERIUM def test_rlsb_domain(self): """Test that RLSB domain is recognized.""" assert "@rlsb.de" in KNOWN_AUTHORITIES_NI assert KNOWN_AUTHORITIES_NI["@rlsb.de"]["type"] == SenderType.RLSB def test_landesschulbehoerde_domain(self): """Test that Landesschulbehörde domain is recognized.""" assert "@landesschulbehoerde-nds.de" in KNOWN_AUTHORITIES_NI assert KNOWN_AUTHORITIES_NI["@landesschulbehoerde-nds.de"]["type"] == SenderType.LANDESSCHULBEHOERDE def test_nibis_domain(self): """Test that NiBiS domain is recognized.""" assert "@nibis.de" in KNOWN_AUTHORITIES_NI assert KNOWN_AUTHORITIES_NI["@nibis.de"]["type"] == SenderType.NIBIS def test_unknown_domain_not_in_list(self): """Test that unknown domains are not in the list.""" assert "@gmail.com" not in KNOWN_AUTHORITIES_NI assert "@example.de" not in KNOWN_AUTHORITIES_NI class TestTaskServicePriority: """Tests for TaskService priority calculation.""" @pytest.fixture def task_service(self): return TaskService() def test_priority_from_kultusministerium(self, task_service): """Kultusministerium should result in HIGH priority.""" priority = task_service._get_priority_from_sender(SenderType.KULTUSMINISTERIUM) assert priority == TaskPriority.HIGH def test_priority_from_rlsb(self, task_service): """RLSB should result in HIGH priority.""" priority = task_service._get_priority_from_sender(SenderType.RLSB) assert priority == TaskPriority.HIGH def test_priority_from_nibis(self, task_service): """NiBiS should result in MEDIUM priority.""" priority = task_service._get_priority_from_sender(SenderType.NIBIS) assert priority == TaskPriority.MEDIUM def test_priority_from_privatperson(self, task_service): """Privatperson should result in LOW priority.""" priority = task_service._get_priority_from_sender(SenderType.PRIVATPERSON) assert priority == TaskPriority.LOW class TestTaskServiceDeadlineAdjustment: """Tests for TaskService deadline-based priority adjustment.""" @pytest.fixture def task_service(self): return TaskService() def test_urgent_for_tomorrow(self, task_service): """Deadline tomorrow should be URGENT.""" deadline = datetime.now() + timedelta(days=1) priority = task_service._adjust_priority_for_deadline(TaskPriority.LOW, deadline) assert priority == TaskPriority.URGENT def test_urgent_for_today(self, task_service): """Deadline today should be URGENT.""" deadline = datetime.now() + timedelta(hours=5) priority = task_service._adjust_priority_for_deadline(TaskPriority.LOW, deadline) assert priority == TaskPriority.URGENT def test_high_for_3_days(self, task_service): """Deadline in 3 days with HIGH input stays HIGH.""" deadline = datetime.now() + timedelta(days=3) # Note: max() compares enum by value string, so we test with HIGH input priority = task_service._adjust_priority_for_deadline(TaskPriority.HIGH, deadline) assert priority == TaskPriority.HIGH def test_medium_for_7_days(self, task_service): """Deadline in 7 days should be at least MEDIUM.""" deadline = datetime.now() + timedelta(days=7) priority = task_service._adjust_priority_for_deadline(TaskPriority.LOW, deadline) assert priority == TaskPriority.MEDIUM def test_no_change_for_far_deadline(self, task_service): """Deadline far in the future should not change priority.""" deadline = datetime.now() + timedelta(days=30) priority = task_service._adjust_priority_for_deadline(TaskPriority.LOW, deadline) assert priority == TaskPriority.LOW class TestTaskServiceDescriptionBuilder: """Tests for TaskService description building.""" @pytest.fixture def task_service(self): return TaskService() def test_description_with_deadlines(self, task_service): """Description should include deadline information.""" deadlines = [ DeadlineExtraction( deadline_date=datetime(2026, 1, 15), description="Einreichung der Unterlagen", is_firm=True, confidence=0.9, source_text="bis zum 15.01.2026", ) ] email_data = { "sender_email": "test@mk.niedersachsen.de", "body_preview": "Bitte reichen Sie die Unterlagen ein.", } description = task_service._build_task_description(deadlines, email_data) assert "**Fristen:**" in description assert "15.01.2026" in description assert "Einreichung der Unterlagen" in description assert "(verbindlich)" in description assert "test@mk.niedersachsen.de" in description def test_description_without_deadlines(self, task_service): """Description should work without deadlines.""" email_data = { "sender_email": "sender@example.de", "body_preview": "Test preview text", } description = task_service._build_task_description([], email_data) assert "**Fristen:**" not in description assert "sender@example.de" in description class TestSenderClassification: """Tests for sender classification via classify_sender_by_domain.""" def test_classify_kultusministerium(self): """Email from MK should be classified correctly.""" result = classify_sender_by_domain("referat@mk.niedersachsen.de") assert result is not None assert result.sender_type == SenderType.KULTUSMINISTERIUM def test_classify_rlsb(self): """Email from RLSB should be classified correctly.""" result = classify_sender_by_domain("info@rlsb.de") assert result is not None assert result.sender_type == SenderType.RLSB def test_classify_unknown_domain(self): """Email from unknown domain should return None.""" result = classify_sender_by_domain("user@gmail.com") assert result is None class TestAIEmailServiceDeadlineExtraction: """Tests for AIEmailService deadline extraction from text.""" @pytest.fixture def ai_service(self): return AIEmailService() def test_extract_deadline_bis_format(self, ai_service): """Test extraction of 'bis zum DD.MM.YYYY' format.""" text = "Bitte senden Sie die Unterlagen bis zum 15.01.2027 ein." deadlines = ai_service._extract_deadlines_regex(text) assert len(deadlines) >= 1 # Check that at least one deadline was found dates = [d.deadline_date.strftime("%Y-%m-%d") for d in deadlines] assert "2027-01-15" in dates def test_extract_deadline_frist_format(self, ai_service): """Test extraction of 'Frist: DD.MM.YYYY' format.""" text = "Die Frist: 20.02.2027 muss eingehalten werden." deadlines = ai_service._extract_deadlines_regex(text) assert len(deadlines) >= 1 dates = [d.deadline_date.strftime("%Y-%m-%d") for d in deadlines] assert "2027-02-20" in dates def test_no_deadline_in_text(self, ai_service): """Test that no deadlines are found when none exist.""" text = "Dies ist eine allgemeine Mitteilung ohne Datum." deadlines = ai_service._extract_deadlines_regex(text) assert len(deadlines) == 0 class TestAIEmailServiceCategoryRules: """Tests for AIEmailService category classification rules.""" @pytest.fixture def ai_service(self): return AIEmailService() def test_fortbildung_category(self, ai_service): """Test Fortbildung category detection.""" # Use keywords that clearly match FORTBILDUNG: fortbildung, seminar, workshop subject = "Fortbildung NLQ Seminar" body = "Wir bieten eine Weiterbildung zum Thema Didaktik an." category, confidence = ai_service._classify_category_rules(subject, body, SenderType.UNBEKANNT) assert category == EmailCategory.FORTBILDUNG def test_personal_category(self, ai_service): """Test Personal category detection.""" # Use keywords that clearly match PERSONAL: personalrat, versetzung, krankmeldung subject = "Personalrat Sitzung" body = "Thema: Krankmeldung und Beurteilung" category, confidence = ai_service._classify_category_rules(subject, body, SenderType.UNBEKANNT) assert category == EmailCategory.PERSONAL def test_finanzen_category(self, ai_service): """Test Finanzen category detection.""" # Use keywords that clearly match FINANZEN: budget, haushalt, abrechnung subject = "Haushalt 2026 Budget" body = "Die Abrechnung und Erstattung für das neue Etat." category, confidence = ai_service._classify_category_rules(subject, body, SenderType.UNBEKANNT) assert category == EmailCategory.FINANZEN class TestEmailAccountCreateValidation: """Tests for EmailAccountCreate Pydantic model validation.""" def test_valid_account_creation(self): """Test that valid data creates an account.""" account = EmailAccountCreate( email="schulleitung@grundschule-xy.de", display_name="Schulleitung", imap_host="imap.example.com", imap_port=993, smtp_host="smtp.example.com", smtp_port=587, password="secret123", ) assert account.email == "schulleitung@grundschule-xy.de" assert account.imap_port == 993 assert account.imap_ssl is True # Default def test_default_ssl_true(self): """Test that SSL defaults to True.""" account = EmailAccountCreate( email="test@example.com", display_name="Test Account", imap_host="imap.example.com", imap_port=993, smtp_host="smtp.example.com", smtp_port=587, password="secret", ) assert account.imap_ssl is True assert account.smtp_ssl is True class TestTaskCreateValidation: """Tests for TaskCreate Pydantic model validation.""" def test_valid_task_creation(self): """Test that valid data creates a task.""" task = TaskCreate( title="Unterlagen einreichen", description="Bitte alle Dokumente bis Freitag.", priority=TaskPriority.HIGH, deadline=datetime(2026, 1, 15), ) assert task.title == "Unterlagen einreichen" assert task.priority == TaskPriority.HIGH def test_default_priority_medium(self): """Test that priority defaults to MEDIUM.""" task = TaskCreate( title="Einfache Aufgabe", ) assert task.priority == TaskPriority.MEDIUM def test_optional_deadline(self): """Test that deadline is optional.""" task = TaskCreate( title="Keine Frist", ) assert task.deadline is None # Integration test placeholder class TestMailModuleIntegration: """Integration tests (require database connection).""" @pytest.mark.skip(reason="Requires database connection") @pytest.mark.asyncio async def test_create_task_from_email(self): """Test creating a task from an email analysis.""" pass @pytest.mark.skip(reason="Requires database connection") @pytest.mark.asyncio async def test_dashboard_stats(self): """Test dashboard statistics calculation.""" pass if __name__ == "__main__": pytest.main([__file__, "-v"])