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:
932
backend/tests/test_messenger_api.py
Normal file
932
backend/tests/test_messenger_api.py
Normal file
@@ -0,0 +1,932 @@
|
||||
"""
|
||||
Tests fuer die Messenger API.
|
||||
|
||||
Testet:
|
||||
- Kontakt CRUD Operationen
|
||||
- Konversations-Management
|
||||
- Nachrichten-Versand
|
||||
- CSV Import/Export
|
||||
- Vorlagen-Verwaltung
|
||||
- Gruppen-Verwaltung
|
||||
- Statistiken
|
||||
|
||||
Hinweis: Diese Tests nutzen den Docker-Container.
|
||||
Starten Sie den Backend-Container vor dem Testen:
|
||||
docker compose up -d backend
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
# Backend URL (Docker)
|
||||
BASE_URL = "http://localhost:8000/api/messenger"
|
||||
|
||||
|
||||
def skip_if_backend_unavailable():
|
||||
"""Skip tests if backend is not running."""
|
||||
try:
|
||||
requests.get(f"{BASE_URL}/contacts", timeout=2)
|
||||
except requests.exceptions.RequestException:
|
||||
pytest.skip("Backend not available - start with 'docker compose up -d backend'")
|
||||
|
||||
|
||||
def generate_unique_email():
|
||||
"""Generiert eine eindeutige Email-Adresse fuer Tests."""
|
||||
return f"test_{uuid.uuid4().hex[:8]}@example.com"
|
||||
|
||||
|
||||
class TestMessengerContacts:
|
||||
"""Tests fuer Kontakt-Endpoints."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def check_backend(self):
|
||||
skip_if_backend_unavailable()
|
||||
|
||||
def test_get_contacts(self):
|
||||
"""Test: Kontaktliste abrufen."""
|
||||
response = requests.get(f"{BASE_URL}/contacts")
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
def test_create_and_delete_contact(self):
|
||||
"""Test: Kontakt erstellen und loeschen."""
|
||||
contact_data = {
|
||||
"name": "Test Familie Mueller",
|
||||
"email": generate_unique_email(),
|
||||
"phone": "+49 123 456789",
|
||||
"student_name": "Max Mueller",
|
||||
"class_name": "10a"
|
||||
}
|
||||
|
||||
# Erstellen
|
||||
response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert data["name"] == "Test Familie Mueller"
|
||||
assert "id" in data
|
||||
assert "created_at" in data
|
||||
assert "updated_at" in data
|
||||
|
||||
# Loeschen
|
||||
contact_id = data["id"]
|
||||
delete_response = requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
assert delete_response.status_code == 200
|
||||
assert delete_response.json()["status"] == "deleted"
|
||||
|
||||
def test_update_contact(self):
|
||||
"""Test: Kontakt aktualisieren."""
|
||||
# Erstellen
|
||||
contact_data = {"name": "Original Name", "email": generate_unique_email()}
|
||||
create_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = create_response.json()["id"]
|
||||
|
||||
# Aktualisieren
|
||||
update_data = {"name": "Aktualisierter Name", "email": generate_unique_email()}
|
||||
response = requests.put(f"{BASE_URL}/contacts/{contact_id}", json=update_data)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Aktualisierter Name"
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
def test_get_single_contact(self):
|
||||
"""Test: Einzelnen Kontakt abrufen."""
|
||||
# Erstellen
|
||||
contact_data = {"name": "Single Test", "email": generate_unique_email()}
|
||||
create_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = create_response.json()["id"]
|
||||
|
||||
# Abrufen
|
||||
response = requests.get(f"{BASE_URL}/contacts/{contact_id}")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Single Test"
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
def test_get_nonexistent_contact(self):
|
||||
"""Test: Nicht existierenden Kontakt abrufen gibt 404."""
|
||||
response = requests.get(f"{BASE_URL}/contacts/nonexistent-uuid")
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_filter_contacts_by_role(self):
|
||||
"""Test: Kontakte nach Rolle filtern."""
|
||||
# Kontakt mit Rolle erstellen
|
||||
email = generate_unique_email()
|
||||
contact_data = {"name": "Teacher Test", "email": email, "role": "teacher"}
|
||||
create_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = create_response.json()["id"]
|
||||
|
||||
# Nach Rolle filtern
|
||||
response = requests.get(f"{BASE_URL}/contacts", params={"role": "teacher"})
|
||||
assert response.status_code == 200
|
||||
contacts = response.json()
|
||||
assert all(c.get("role") == "teacher" for c in contacts)
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
def test_filter_contacts_by_class(self):
|
||||
"""Test: Kontakte nach Klasse filtern."""
|
||||
email = generate_unique_email()
|
||||
contact_data = {"name": "Class Test", "email": email, "class_name": "12b"}
|
||||
create_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = create_response.json()["id"]
|
||||
|
||||
# Nach Klasse filtern
|
||||
response = requests.get(f"{BASE_URL}/contacts", params={"class_name": "12b"})
|
||||
assert response.status_code == 200
|
||||
contacts = response.json()
|
||||
assert all(c.get("class_name") == "12b" for c in contacts)
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
def test_search_contacts(self):
|
||||
"""Test: Kontakte durchsuchen."""
|
||||
unique_name = f"Suchtest_{uuid.uuid4().hex[:8]}"
|
||||
email = generate_unique_email()
|
||||
contact_data = {"name": unique_name, "email": email}
|
||||
create_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = create_response.json()["id"]
|
||||
|
||||
# Suchen
|
||||
response = requests.get(f"{BASE_URL}/contacts", params={"search": unique_name})
|
||||
assert response.status_code == 200
|
||||
contacts = response.json()
|
||||
assert len(contacts) >= 1
|
||||
assert any(c["name"] == unique_name for c in contacts)
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
def test_create_contact_with_tags(self):
|
||||
"""Test: Kontakt mit Tags erstellen."""
|
||||
contact_data = {
|
||||
"name": "Tags Test",
|
||||
"email": generate_unique_email(),
|
||||
"tags": ["Elternbeirat", "Foerderverein"]
|
||||
}
|
||||
response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
assert response.status_code == 200
|
||||
assert "Elternbeirat" in response.json()["tags"]
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{response.json()['id']}")
|
||||
|
||||
def test_duplicate_email_rejected(self):
|
||||
"""Test: Doppelte Email-Adresse wird abgelehnt."""
|
||||
email = generate_unique_email()
|
||||
contact_data = {"name": "First Contact", "email": email}
|
||||
response1 = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
assert response1.status_code == 200
|
||||
contact_id = response1.json()["id"]
|
||||
|
||||
# Zweiter Kontakt mit gleicher Email
|
||||
contact_data2 = {"name": "Second Contact", "email": email}
|
||||
response2 = requests.post(f"{BASE_URL}/contacts", json=contact_data2)
|
||||
assert response2.status_code == 400
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
|
||||
class TestMessengerConversations:
|
||||
"""Tests fuer Konversations-Endpoints."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def check_backend(self):
|
||||
skip_if_backend_unavailable()
|
||||
|
||||
def test_get_conversations(self):
|
||||
"""Test: Konversationsliste abrufen."""
|
||||
response = requests.get(f"{BASE_URL}/conversations")
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
def test_create_conversation_with_contact(self):
|
||||
"""Test: Konversation mit Kontakt erstellen."""
|
||||
# Kontakt erstellen
|
||||
contact_data = {"name": "Conv Test Contact", "email": generate_unique_email()}
|
||||
contact_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = contact_response.json()["id"]
|
||||
|
||||
# Konversation erstellen
|
||||
response = requests.post(f"{BASE_URL}/conversations", params={"contact_id": contact_id})
|
||||
assert response.status_code == 200
|
||||
conv = response.json()
|
||||
assert conv["name"] == "Conv Test Contact"
|
||||
assert contact_id in conv["participant_ids"]
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/conversations/{conv['id']}")
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
def test_create_conversation_returns_existing(self):
|
||||
"""Test: Erneutes Erstellen gibt bestehende Konversation zurueck."""
|
||||
# Kontakt erstellen
|
||||
contact_data = {"name": "Existing Conv Test", "email": generate_unique_email()}
|
||||
contact_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = contact_response.json()["id"]
|
||||
|
||||
# Erste Konversation
|
||||
response1 = requests.post(f"{BASE_URL}/conversations", params={"contact_id": contact_id})
|
||||
conv_id1 = response1.json()["id"]
|
||||
|
||||
# Zweite Anfrage sollte gleiche Konversation zurueckgeben
|
||||
response2 = requests.post(f"{BASE_URL}/conversations", params={"contact_id": contact_id})
|
||||
conv_id2 = response2.json()["id"]
|
||||
|
||||
assert conv_id1 == conv_id2
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/conversations/{conv_id1}")
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
def test_create_conversation_without_params_fails(self):
|
||||
"""Test: Konversation ohne Parameter gibt Fehler."""
|
||||
response = requests.post(f"{BASE_URL}/conversations")
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_delete_conversation(self):
|
||||
"""Test: Konversation loeschen."""
|
||||
# Kontakt und Konversation erstellen
|
||||
contact_data = {"name": "Delete Conv Test", "email": generate_unique_email()}
|
||||
contact_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = contact_response.json()["id"]
|
||||
|
||||
conv_response = requests.post(f"{BASE_URL}/conversations", params={"contact_id": contact_id})
|
||||
conv_id = conv_response.json()["id"]
|
||||
|
||||
# Loeschen
|
||||
delete_response = requests.delete(f"{BASE_URL}/conversations/{conv_id}")
|
||||
assert delete_response.status_code == 200
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
|
||||
class TestMessengerMessages:
|
||||
"""Tests fuer Nachrichten-Endpoints."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def check_backend(self):
|
||||
skip_if_backend_unavailable()
|
||||
|
||||
def test_send_and_get_messages(self):
|
||||
"""Test: Nachricht senden und abrufen."""
|
||||
# Kontakt und Konversation erstellen
|
||||
contact_data = {"name": "Message Test", "email": generate_unique_email()}
|
||||
contact_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = contact_response.json()["id"]
|
||||
|
||||
conv_response = requests.post(f"{BASE_URL}/conversations", params={"contact_id": contact_id})
|
||||
conv_id = conv_response.json()["id"]
|
||||
|
||||
# Nachricht senden
|
||||
msg_data = {"content": "Test Nachricht"}
|
||||
send_response = requests.post(f"{BASE_URL}/conversations/{conv_id}/messages", json=msg_data)
|
||||
assert send_response.status_code == 200
|
||||
msg = send_response.json()
|
||||
assert msg["content"] == "Test Nachricht"
|
||||
assert msg["sender_id"] == "self"
|
||||
|
||||
# Nachrichten abrufen
|
||||
get_response = requests.get(f"{BASE_URL}/conversations/{conv_id}/messages")
|
||||
assert get_response.status_code == 200
|
||||
messages = get_response.json()
|
||||
assert len(messages) >= 1
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/conversations/{conv_id}")
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
def test_message_updates_conversation(self):
|
||||
"""Test: Nachricht aktualisiert Konversation."""
|
||||
# Setup
|
||||
contact_data = {"name": "Update Conv Test", "email": generate_unique_email()}
|
||||
contact_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = contact_response.json()["id"]
|
||||
|
||||
conv_response = requests.post(f"{BASE_URL}/conversations", params={"contact_id": contact_id})
|
||||
conv_id = conv_response.json()["id"]
|
||||
|
||||
# Nachricht senden
|
||||
msg_data = {"content": "Aktualisiert letzte Nachricht"}
|
||||
requests.post(f"{BASE_URL}/conversations/{conv_id}/messages", json=msg_data)
|
||||
|
||||
# Konversation pruefen
|
||||
conv = requests.get(f"{BASE_URL}/conversations/{conv_id}").json()
|
||||
assert "Aktualisiert" in conv.get("last_message", "")
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/conversations/{conv_id}")
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
def test_mark_message_as_read(self):
|
||||
"""Test: Nachricht als gelesen markieren."""
|
||||
# Setup
|
||||
contact_data = {"name": "Read Test", "email": generate_unique_email()}
|
||||
contact_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = contact_response.json()["id"]
|
||||
|
||||
conv_response = requests.post(f"{BASE_URL}/conversations", params={"contact_id": contact_id})
|
||||
conv_id = conv_response.json()["id"]
|
||||
|
||||
msg_response = requests.post(
|
||||
f"{BASE_URL}/conversations/{conv_id}/messages",
|
||||
json={"content": "Read me"}
|
||||
)
|
||||
msg_id = msg_response.json()["id"]
|
||||
|
||||
# Als gelesen markieren
|
||||
read_response = requests.put(f"{BASE_URL}/messages/{msg_id}/read")
|
||||
assert read_response.status_code == 200
|
||||
assert read_response.json()["status"] == "read"
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/conversations/{conv_id}")
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
def test_mark_all_messages_read(self):
|
||||
"""Test: Alle Nachrichten einer Konversation als gelesen markieren."""
|
||||
# Setup
|
||||
contact_data = {"name": "Read All Test", "email": generate_unique_email()}
|
||||
contact_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = contact_response.json()["id"]
|
||||
|
||||
conv_response = requests.post(f"{BASE_URL}/conversations", params={"contact_id": contact_id})
|
||||
conv_id = conv_response.json()["id"]
|
||||
|
||||
# Mehrere Nachrichten senden
|
||||
requests.post(f"{BASE_URL}/conversations/{conv_id}/messages", json={"content": "Msg 1"})
|
||||
requests.post(f"{BASE_URL}/conversations/{conv_id}/messages", json={"content": "Msg 2"})
|
||||
|
||||
# Alle als gelesen markieren
|
||||
response = requests.put(f"{BASE_URL}/conversations/{conv_id}/read-all")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "all_read"
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/conversations/{conv_id}")
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
def test_messages_pagination(self):
|
||||
"""Test: Nachrichten mit Limit abrufen."""
|
||||
# Setup
|
||||
contact_data = {"name": "Pagination Test", "email": generate_unique_email()}
|
||||
contact_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = contact_response.json()["id"]
|
||||
|
||||
conv_response = requests.post(f"{BASE_URL}/conversations", params={"contact_id": contact_id})
|
||||
conv_id = conv_response.json()["id"]
|
||||
|
||||
# Mehrere Nachrichten senden
|
||||
for i in range(5):
|
||||
requests.post(f"{BASE_URL}/conversations/{conv_id}/messages", json={"content": f"Msg {i}"})
|
||||
|
||||
# Mit Limit abrufen
|
||||
response = requests.get(f"{BASE_URL}/conversations/{conv_id}/messages", params={"limit": 3})
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) <= 3
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/conversations/{conv_id}")
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
|
||||
class TestMessengerTemplates:
|
||||
"""Tests fuer Vorlagen-Endpoints."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def check_backend(self):
|
||||
skip_if_backend_unavailable()
|
||||
|
||||
def test_get_templates(self):
|
||||
"""Test: Vorlagenliste abrufen."""
|
||||
response = requests.get(f"{BASE_URL}/templates")
|
||||
assert response.status_code == 200
|
||||
templates = response.json()
|
||||
assert isinstance(templates, list)
|
||||
# Standard-Vorlagen sollten vorhanden sein
|
||||
assert len(templates) >= 1
|
||||
|
||||
def test_create_and_delete_template(self):
|
||||
"""Test: Vorlage erstellen und loeschen."""
|
||||
# Erstellen (Query-Parameter)
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/templates",
|
||||
params={
|
||||
"name": "Test Vorlage",
|
||||
"content": "Test mit [PLATZHALTER]",
|
||||
"category": "test"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
template = response.json()
|
||||
assert template["name"] == "Test Vorlage"
|
||||
assert "[PLATZHALTER]" in template["content"]
|
||||
|
||||
# Loeschen
|
||||
delete_response = requests.delete(f"{BASE_URL}/templates/{template['id']}")
|
||||
assert delete_response.status_code == 200
|
||||
|
||||
def test_default_templates_exist(self):
|
||||
"""Test: Standard-Vorlagen sind vorhanden."""
|
||||
response = requests.get(f"{BASE_URL}/templates")
|
||||
templates = response.json()
|
||||
|
||||
# Mindestens eine Vorlage sollte existieren
|
||||
assert len(templates) >= 1
|
||||
|
||||
# Jede Vorlage hat die erforderlichen Felder
|
||||
for template in templates:
|
||||
assert "id" in template
|
||||
assert "name" in template
|
||||
assert "content" in template
|
||||
assert "category" in template
|
||||
|
||||
|
||||
class TestMessengerGroups:
|
||||
"""Tests fuer Gruppen-Endpoints."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def check_backend(self):
|
||||
skip_if_backend_unavailable()
|
||||
|
||||
def test_get_groups(self):
|
||||
"""Test: Gruppenliste abrufen."""
|
||||
response = requests.get(f"{BASE_URL}/groups")
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
def test_create_and_delete_group(self):
|
||||
"""Test: Gruppe erstellen und loeschen."""
|
||||
group_data = {
|
||||
"name": f"Test Klasse {uuid.uuid4().hex[:4]}",
|
||||
"description": "Test Elterngruppe",
|
||||
"group_type": "class",
|
||||
"member_ids": []
|
||||
}
|
||||
|
||||
# Erstellen
|
||||
response = requests.post(f"{BASE_URL}/groups", json=group_data)
|
||||
assert response.status_code == 200
|
||||
group = response.json()
|
||||
assert "Test Klasse" in group["name"]
|
||||
assert group["group_type"] == "class"
|
||||
|
||||
# Loeschen
|
||||
delete_response = requests.delete(f"{BASE_URL}/groups/{group['id']}")
|
||||
assert delete_response.status_code == 200
|
||||
|
||||
def test_update_group_members(self):
|
||||
"""Test: Gruppen-Mitglieder aktualisieren."""
|
||||
# Kontakte erstellen
|
||||
contact1 = requests.post(
|
||||
f"{BASE_URL}/contacts",
|
||||
json={"name": "Member 1", "email": generate_unique_email()}
|
||||
).json()
|
||||
contact2 = requests.post(
|
||||
f"{BASE_URL}/contacts",
|
||||
json={"name": "Member 2", "email": generate_unique_email()}
|
||||
).json()
|
||||
|
||||
# Gruppe erstellen
|
||||
group = requests.post(
|
||||
f"{BASE_URL}/groups",
|
||||
json={"name": "Members Test", "description": "Test", "member_ids": []}
|
||||
).json()
|
||||
|
||||
# Mitglieder hinzufuegen
|
||||
member_ids = [contact1["id"], contact2["id"]]
|
||||
response = requests.put(
|
||||
f"{BASE_URL}/groups/{group['id']}/members",
|
||||
json=member_ids
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert contact1["id"] in response.json()["member_ids"]
|
||||
assert contact2["id"] in response.json()["member_ids"]
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/groups/{group['id']}")
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact1['id']}")
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact2['id']}")
|
||||
|
||||
def test_create_group_conversation(self):
|
||||
"""Test: Gruppenkonversation erstellen."""
|
||||
# Gruppe erstellen
|
||||
group = requests.post(
|
||||
f"{BASE_URL}/groups",
|
||||
json={"name": "Group Conv Test", "description": "Test"}
|
||||
).json()
|
||||
|
||||
# Konversation mit Gruppe erstellen
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/conversations",
|
||||
params={"group_id": group["id"]}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
conv = response.json()
|
||||
assert conv["is_group"] == True
|
||||
assert conv["group_id"] == group["id"]
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/conversations/{conv['id']}")
|
||||
requests.delete(f"{BASE_URL}/groups/{group['id']}")
|
||||
|
||||
|
||||
class TestMessengerCSV:
|
||||
"""Tests fuer CSV Import/Export."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def check_backend(self):
|
||||
skip_if_backend_unavailable()
|
||||
|
||||
def test_export_csv(self):
|
||||
"""Test: Kontakte als CSV exportieren."""
|
||||
response = requests.get(f"{BASE_URL}/contacts/export/csv")
|
||||
assert response.status_code == 200
|
||||
assert "text/csv" in response.headers.get("content-type", "")
|
||||
|
||||
# CSV-Inhalt pruefen
|
||||
content = response.text
|
||||
assert "name" in content.lower()
|
||||
assert "email" in content.lower()
|
||||
|
||||
def test_import_csv_semicolon(self):
|
||||
"""Test: Kontakte aus CSV mit Semikolon-Trennung importieren."""
|
||||
unique_email = generate_unique_email()
|
||||
csv_content = f"Name;Email;Telefon;Schueler;Klasse\nCSV Test Import;{unique_email};0123;CSV Schueler;5b"
|
||||
|
||||
files = {"file": ("test.csv", csv_content, "text/csv")}
|
||||
response = requests.post(f"{BASE_URL}/contacts/import", files=files)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert "imported" in result
|
||||
assert result["imported"] >= 1
|
||||
|
||||
# Cleanup - importierten Kontakt loeschen
|
||||
if result.get("contacts"):
|
||||
for contact in result["contacts"]:
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact['id']}")
|
||||
|
||||
def test_import_csv_german_columns(self):
|
||||
"""Test: CSV mit deutschen Spaltennamen importieren."""
|
||||
unique_email = generate_unique_email()
|
||||
csv_content = f"Name;E-Mail;Telefon;Kind;Klasse;Notizen\nDeutsche Spalten;{unique_email};0123;Kind Name;7c;Test Notizen"
|
||||
|
||||
files = {"file": ("german.csv", csv_content, "text/csv")}
|
||||
response = requests.post(f"{BASE_URL}/contacts/import", files=files)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["imported"] >= 1
|
||||
|
||||
# Cleanup
|
||||
if result.get("contacts"):
|
||||
for contact in result["contacts"]:
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact['id']}")
|
||||
|
||||
def test_import_csv_skip_duplicates(self):
|
||||
"""Test: CSV Import ueberspringt doppelte Emails."""
|
||||
# Ersten Kontakt erstellen
|
||||
email = generate_unique_email()
|
||||
requests.post(f"{BASE_URL}/contacts", json={"name": "Original", "email": email})
|
||||
|
||||
# CSV mit gleicher Email importieren
|
||||
csv_content = f"Name;Email\nDuplikat;{email}"
|
||||
files = {"file": ("dup.csv", csv_content, "text/csv")}
|
||||
response = requests.post(f"{BASE_URL}/contacts/import", files=files)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["skipped"] >= 1
|
||||
assert any("existiert bereits" in err for err in result.get("errors", []))
|
||||
|
||||
def test_import_csv_missing_name(self):
|
||||
"""Test: CSV Import meldet fehlende Namen."""
|
||||
csv_content = "Name;Email\n;noname@test.com"
|
||||
files = {"file": ("noname.csv", csv_content, "text/csv")}
|
||||
response = requests.post(f"{BASE_URL}/contacts/import", files=files)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["skipped"] >= 1
|
||||
assert any("Name fehlt" in err for err in result.get("errors", []))
|
||||
|
||||
def test_import_non_csv_rejected(self):
|
||||
"""Test: Nicht-CSV Dateien werden abgelehnt."""
|
||||
files = {"file": ("test.txt", "not a csv", "text/plain")}
|
||||
response = requests.post(f"{BASE_URL}/contacts/import", files=files)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestMessengerStats:
|
||||
"""Tests fuer Statistik-Endpoint."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def check_backend(self):
|
||||
skip_if_backend_unavailable()
|
||||
|
||||
def test_get_stats(self):
|
||||
"""Test: Statistiken abrufen."""
|
||||
response = requests.get(f"{BASE_URL}/stats")
|
||||
assert response.status_code == 200
|
||||
|
||||
stats = response.json()
|
||||
assert "total_contacts" in stats
|
||||
assert "total_groups" in stats
|
||||
assert "total_conversations" in stats
|
||||
assert "total_messages" in stats
|
||||
assert "unread_messages" in stats
|
||||
assert "contacts_by_role" in stats
|
||||
|
||||
def test_stats_reflect_data(self):
|
||||
"""Test: Statistiken spiegeln Daten wider."""
|
||||
# Initiale Stats
|
||||
initial_stats = requests.get(f"{BASE_URL}/stats").json()
|
||||
initial_contacts = initial_stats["total_contacts"]
|
||||
|
||||
# Kontakt hinzufuegen
|
||||
contact = requests.post(
|
||||
f"{BASE_URL}/contacts",
|
||||
json={"name": "Stats Test", "email": generate_unique_email()}
|
||||
).json()
|
||||
|
||||
# Neue Stats
|
||||
new_stats = requests.get(f"{BASE_URL}/stats").json()
|
||||
assert new_stats["total_contacts"] == initial_contacts + 1
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact['id']}")
|
||||
|
||||
|
||||
class TestMessengerEdgeCases:
|
||||
"""Tests fuer Randfaelle und Fehlerbehandlung."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def check_backend(self):
|
||||
skip_if_backend_unavailable()
|
||||
|
||||
def test_update_nonexistent_contact(self):
|
||||
"""Test: Update nicht existierendem Kontakt gibt 404."""
|
||||
response = requests.put(
|
||||
f"{BASE_URL}/contacts/nonexistent-uuid",
|
||||
json={"name": "Test"}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_message_to_nonexistent_conversation(self):
|
||||
"""Test: Nachricht an nicht existierende Konversation gibt 404."""
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/conversations/nonexistent-uuid/messages",
|
||||
json={"content": "Test"}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_create_conversation_nonexistent_contact(self):
|
||||
"""Test: Konversation mit nicht existierendem Kontakt gibt 404."""
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/conversations",
|
||||
params={"contact_id": "nonexistent-uuid"}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_create_conversation_nonexistent_group(self):
|
||||
"""Test: Konversation mit nicht existierender Gruppe gibt 404."""
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/conversations",
|
||||
params={"group_id": "nonexistent-uuid"}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_mark_nonexistent_message_read(self):
|
||||
"""Test: Nicht existierende Nachricht als gelesen markieren gibt 404."""
|
||||
response = requests.put(f"{BASE_URL}/messages/nonexistent-uuid/read")
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_contact_without_email(self):
|
||||
"""Test: Kontakt ohne Email erstellen ist erlaubt."""
|
||||
contact_data = {"name": "Nur Name"}
|
||||
response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["email"] is None
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{response.json()['id']}")
|
||||
|
||||
def test_empty_message_rejected(self):
|
||||
"""Test: Leere Nachricht wird abgelehnt."""
|
||||
# Setup
|
||||
contact = requests.post(
|
||||
f"{BASE_URL}/contacts",
|
||||
json={"name": "Empty Msg Test", "email": generate_unique_email()}
|
||||
).json()
|
||||
conv = requests.post(
|
||||
f"{BASE_URL}/conversations",
|
||||
params={"contact_id": contact["id"]}
|
||||
).json()
|
||||
|
||||
# Leere Nachricht sollte Fehler geben
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/conversations/{conv['id']}/messages",
|
||||
json={"content": ""}
|
||||
)
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/conversations/{conv['id']}")
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact['id']}")
|
||||
|
||||
|
||||
class TestMessengerMatrixFeatures:
|
||||
"""Tests fuer Matrix-ID und bevorzugten Kanal."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def check_backend(self):
|
||||
skip_if_backend_unavailable()
|
||||
|
||||
def test_create_contact_with_matrix_id(self):
|
||||
"""Test: Kontakt mit Matrix-ID erstellen."""
|
||||
contact_data = {
|
||||
"name": "Matrix User",
|
||||
"email": generate_unique_email(),
|
||||
"matrix_id": "@testuser:matrix.org"
|
||||
}
|
||||
response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["matrix_id"] == "@testuser:matrix.org"
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{data['id']}")
|
||||
|
||||
def test_create_contact_with_preferred_channel(self):
|
||||
"""Test: Kontakt mit bevorzugtem Kanal erstellen."""
|
||||
contact_data = {
|
||||
"name": "Channel Test",
|
||||
"email": generate_unique_email(),
|
||||
"preferred_channel": "matrix"
|
||||
}
|
||||
response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["preferred_channel"] == "matrix"
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{data['id']}")
|
||||
|
||||
def test_update_contact_matrix_id(self):
|
||||
"""Test: Matrix-ID eines Kontakts aktualisieren."""
|
||||
# Kontakt erstellen
|
||||
contact_data = {"name": "Update Matrix Test", "email": generate_unique_email()}
|
||||
create_response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
contact_id = create_response.json()["id"]
|
||||
|
||||
# Matrix-ID hinzufuegen
|
||||
update_data = {"matrix_id": "@updated:matrix.org"}
|
||||
response = requests.put(f"{BASE_URL}/contacts/{contact_id}", json=update_data)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["matrix_id"] == "@updated:matrix.org"
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact_id}")
|
||||
|
||||
def test_default_preferred_channel_is_email(self):
|
||||
"""Test: Standard bevorzugter Kanal ist 'email'."""
|
||||
contact_data = {"name": "Default Channel Test", "email": generate_unique_email()}
|
||||
response = requests.post(f"{BASE_URL}/contacts", json=contact_data)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["preferred_channel"] == "email"
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{data['id']}")
|
||||
|
||||
def test_csv_export_includes_matrix_fields(self):
|
||||
"""Test: CSV-Export enthaelt Matrix-ID und bevorzugten Kanal."""
|
||||
# Kontakt mit Matrix-Feldern erstellen
|
||||
contact_data = {
|
||||
"name": "CSV Matrix Test",
|
||||
"email": generate_unique_email(),
|
||||
"matrix_id": "@csvtest:matrix.org",
|
||||
"preferred_channel": "matrix"
|
||||
}
|
||||
contact = requests.post(f"{BASE_URL}/contacts", json=contact_data).json()
|
||||
|
||||
# CSV exportieren
|
||||
response = requests.get(f"{BASE_URL}/contacts/export/csv")
|
||||
assert response.status_code == 200
|
||||
csv_content = response.text.lower()
|
||||
assert "matrix" in csv_content
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact['id']}")
|
||||
|
||||
|
||||
class TestMessengerEmailFeatures:
|
||||
"""Tests fuer Email-Versand bei Nachrichten."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def check_backend(self):
|
||||
skip_if_backend_unavailable()
|
||||
|
||||
def test_send_message_with_email(self):
|
||||
"""Test: Nachricht mit Email-Versand senden."""
|
||||
# Kontakt mit Email erstellen
|
||||
contact_data = {
|
||||
"name": "Email Test Contact",
|
||||
"email": generate_unique_email()
|
||||
}
|
||||
contact = requests.post(f"{BASE_URL}/contacts", json=contact_data).json()
|
||||
|
||||
# Konversation erstellen
|
||||
conv = requests.post(
|
||||
f"{BASE_URL}/conversations",
|
||||
params={"contact_id": contact["id"]}
|
||||
).json()
|
||||
|
||||
# Nachricht mit send_email=true senden
|
||||
msg_data = {
|
||||
"content": "Diese Nachricht wird per Email gesendet.",
|
||||
"send_email": True
|
||||
}
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/conversations/{conv['id']}/messages",
|
||||
json=msg_data
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["content"] == "Diese Nachricht wird per Email gesendet."
|
||||
# Email-Status wird zurueckgegeben
|
||||
assert "email_sent" in data
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/conversations/{conv['id']}")
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact['id']}")
|
||||
|
||||
def test_send_message_without_email(self):
|
||||
"""Test: Nachricht ohne Email-Versand senden."""
|
||||
# Kontakt erstellen
|
||||
contact = requests.post(
|
||||
f"{BASE_URL}/contacts",
|
||||
json={"name": "No Email Test", "email": generate_unique_email()}
|
||||
).json()
|
||||
|
||||
# Konversation erstellen
|
||||
conv = requests.post(
|
||||
f"{BASE_URL}/conversations",
|
||||
params={"contact_id": contact["id"]}
|
||||
).json()
|
||||
|
||||
# Nachricht ohne send_email senden (Standard: false)
|
||||
msg_data = {"content": "Normale Nachricht ohne Email."}
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/conversations/{conv['id']}/messages",
|
||||
json=msg_data
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
# Email sollte nicht gesendet worden sein
|
||||
assert data.get("email_sent", False) == False
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/conversations/{conv['id']}")
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact['id']}")
|
||||
|
||||
def test_send_message_email_to_contact_without_email_address(self):
|
||||
"""Test: Email-Versand an Kontakt ohne Email-Adresse."""
|
||||
# Kontakt ohne Email erstellen
|
||||
contact = requests.post(
|
||||
f"{BASE_URL}/contacts",
|
||||
json={"name": "No Email Address"}
|
||||
).json()
|
||||
|
||||
# Konversation erstellen
|
||||
conv = requests.post(
|
||||
f"{BASE_URL}/conversations",
|
||||
params={"contact_id": contact["id"]}
|
||||
).json()
|
||||
|
||||
# Nachricht mit send_email=true senden
|
||||
msg_data = {
|
||||
"content": "Versuch Email zu senden ohne Adresse.",
|
||||
"send_email": True
|
||||
}
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/conversations/{conv['id']}/messages",
|
||||
json=msg_data
|
||||
)
|
||||
assert response.status_code == 200
|
||||
# Nachricht sollte gespeichert werden, aber Email schlaegt fehl
|
||||
data = response.json()
|
||||
assert data["email_sent"] == False
|
||||
|
||||
# Cleanup
|
||||
requests.delete(f"{BASE_URL}/conversations/{conv['id']}")
|
||||
requests.delete(f"{BASE_URL}/contacts/{contact['id']}")
|
||||
Reference in New Issue
Block a user