Files
breakpilot-lehrer/klausur-service/backend/tests/test_mail_service.py
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

350 lines
12 KiB
Python

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