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>
350 lines
12 KiB
Python
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"])
|