fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
349
klausur-service/backend/tests/test_mail_service.py
Normal file
349
klausur-service/backend/tests/test_mail_service.py
Normal file
@@ -0,0 +1,349 @@
|
||||
"""
|
||||
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"])
|
||||
Reference in New Issue
Block a user