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>
358 lines
12 KiB
Python
358 lines
12 KiB
Python
"""
|
|
Tests for Meeting Consent API
|
|
|
|
Tests for DSGVO-compliant consent management for meeting recordings.
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from fastapi import FastAPI
|
|
|
|
# Import the router
|
|
from meeting_consent_api import router as consent_router
|
|
|
|
app = FastAPI()
|
|
app.include_router(consent_router)
|
|
client = TestClient(app)
|
|
|
|
|
|
class TestConsentRequest:
|
|
"""Tests for requesting recording consent."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self):
|
|
"""Clear consent store before each test."""
|
|
from meeting_consent_api import _consent_store, _participant_consents
|
|
_consent_store.clear()
|
|
_participant_consents.clear()
|
|
|
|
def test_request_consent_success(self):
|
|
"""Test requesting consent for a meeting."""
|
|
response = client.post(
|
|
"/api/meeting-consent/request",
|
|
json={
|
|
"meeting_id": "test-meeting-123",
|
|
"consent_type": "opt_in",
|
|
"participant_count": 3
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["meeting_id"] == "test-meeting-123"
|
|
assert data["consent_type"] == "opt_in"
|
|
assert data["participant_count"] == 3
|
|
assert data["all_consented"] is False
|
|
assert data["can_record"] is False
|
|
assert data["status"] == "pending"
|
|
|
|
def test_request_consent_duplicate_rejected(self):
|
|
"""Test that duplicate consent requests are rejected."""
|
|
# First request
|
|
client.post(
|
|
"/api/meeting-consent/request",
|
|
json={"meeting_id": "dup-meeting", "consent_type": "opt_in"}
|
|
)
|
|
|
|
# Second request should fail
|
|
response = client.post(
|
|
"/api/meeting-consent/request",
|
|
json={"meeting_id": "dup-meeting", "consent_type": "opt_in"}
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
assert "already exists" in response.json()["detail"]
|
|
|
|
def test_request_consent_default_values(self):
|
|
"""Test consent request uses default values."""
|
|
response = client.post(
|
|
"/api/meeting-consent/request",
|
|
json={"meeting_id": "default-meeting"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["consent_type"] == "opt_in"
|
|
|
|
|
|
class TestConsentStatus:
|
|
"""Tests for checking consent status."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self):
|
|
"""Create test consent before each test."""
|
|
from meeting_consent_api import _consent_store, _participant_consents
|
|
_consent_store.clear()
|
|
_participant_consents.clear()
|
|
|
|
client.post(
|
|
"/api/meeting-consent/request",
|
|
json={
|
|
"meeting_id": "status-test-meeting",
|
|
"consent_type": "opt_in",
|
|
"participant_count": 2
|
|
}
|
|
)
|
|
|
|
def test_get_consent_status_existing(self):
|
|
"""Test getting status for existing consent."""
|
|
response = client.get("/api/meeting-consent/status-test-meeting")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["meeting_id"] == "status-test-meeting"
|
|
assert data["status"] == "pending"
|
|
|
|
def test_get_consent_status_not_requested(self):
|
|
"""Test getting status for meeting without consent request."""
|
|
response = client.get("/api/meeting-consent/nonexistent-meeting")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "not_requested"
|
|
assert data["can_record"] is False
|
|
|
|
|
|
class TestParticipantConsent:
|
|
"""Tests for recording individual participant consent."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self):
|
|
"""Create test consent with 2 participants."""
|
|
from meeting_consent_api import _consent_store, _participant_consents
|
|
_consent_store.clear()
|
|
_participant_consents.clear()
|
|
|
|
client.post(
|
|
"/api/meeting-consent/request",
|
|
json={
|
|
"meeting_id": "participant-test",
|
|
"consent_type": "opt_in",
|
|
"participant_count": 2
|
|
}
|
|
)
|
|
|
|
def test_record_participant_consent_positive(self):
|
|
"""Test recording positive consent from participant."""
|
|
response = client.post(
|
|
"/api/meeting-consent/participant-test/participant",
|
|
json={
|
|
"participant_id": "user-1",
|
|
"participant_name": "Alice",
|
|
"consented": True
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["consented_count"] == 1
|
|
assert data["all_consented"] is False
|
|
|
|
def test_record_participant_consent_negative(self):
|
|
"""Test recording negative consent from participant."""
|
|
response = client.post(
|
|
"/api/meeting-consent/participant-test/participant",
|
|
json={
|
|
"participant_id": "user-1",
|
|
"consented": False
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["consented"] is False
|
|
|
|
def test_all_participants_consented_auto_approves(self):
|
|
"""Test that recording is approved when all participants consent."""
|
|
# First participant
|
|
client.post(
|
|
"/api/meeting-consent/participant-test/participant",
|
|
json={"participant_id": "user-1", "consented": True}
|
|
)
|
|
|
|
# Second participant (should trigger approval)
|
|
response = client.post(
|
|
"/api/meeting-consent/participant-test/participant",
|
|
json={"participant_id": "user-2", "consented": True}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["all_consented"] is True
|
|
assert data["can_record"] is True
|
|
|
|
def test_record_consent_meeting_not_found(self):
|
|
"""Test recording consent for non-existent meeting."""
|
|
response = client.post(
|
|
"/api/meeting-consent/nonexistent/participant",
|
|
json={"participant_id": "user-1", "consented": True}
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
def test_update_existing_participant_consent(self):
|
|
"""Test updating consent for same participant."""
|
|
# Initial consent
|
|
client.post(
|
|
"/api/meeting-consent/participant-test/participant",
|
|
json={"participant_id": "user-1", "consented": True}
|
|
)
|
|
|
|
# Update to negative
|
|
response = client.post(
|
|
"/api/meeting-consent/participant-test/participant",
|
|
json={"participant_id": "user-1", "consented": False}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["consented"] is False
|
|
|
|
|
|
class TestConsentWithdrawal:
|
|
"""Tests for withdrawing consent."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self):
|
|
"""Create approved consent."""
|
|
from meeting_consent_api import _consent_store, _participant_consents
|
|
_consent_store.clear()
|
|
_participant_consents.clear()
|
|
|
|
# Create and approve consent
|
|
client.post(
|
|
"/api/meeting-consent/request",
|
|
json={
|
|
"meeting_id": "withdraw-test",
|
|
"consent_type": "opt_in",
|
|
"participant_count": 1
|
|
}
|
|
)
|
|
client.post(
|
|
"/api/meeting-consent/withdraw-test/participant",
|
|
json={"participant_id": "user-1", "consented": True}
|
|
)
|
|
|
|
def test_withdraw_consent(self):
|
|
"""Test withdrawing consent for a meeting."""
|
|
response = client.post(
|
|
"/api/meeting-consent/withdraw-test/withdraw",
|
|
json={"reason": "Changed my mind"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "withdrawn"
|
|
|
|
def test_withdraw_consent_stops_recording_capability(self):
|
|
"""Test that withdrawal stops recording capability."""
|
|
# Withdraw
|
|
client.post(
|
|
"/api/meeting-consent/withdraw-test/withdraw",
|
|
json={}
|
|
)
|
|
|
|
# Check status
|
|
response = client.get("/api/meeting-consent/withdraw-test")
|
|
data = response.json()
|
|
assert data["status"] == "withdrawn" or data["status"] == "not_requested"
|
|
|
|
def test_withdraw_consent_not_found(self):
|
|
"""Test withdrawing consent for non-existent meeting."""
|
|
response = client.post(
|
|
"/api/meeting-consent/nonexistent/withdraw",
|
|
json={}
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestAnnouncedRecording:
|
|
"""Tests for announced recording mode."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self):
|
|
"""Clear store before each test."""
|
|
from meeting_consent_api import _consent_store, _participant_consents
|
|
_consent_store.clear()
|
|
_participant_consents.clear()
|
|
|
|
def test_announce_recording(self):
|
|
"""Test announcing a recording."""
|
|
response = client.post(
|
|
"/api/meeting-consent/announce?meeting_id=announced-meeting&announced_by=Teacher"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["consent_type"] == "announced"
|
|
assert data["can_record"] is True
|
|
assert data["announced_by"] == "Teacher"
|
|
|
|
def test_announce_recording_duplicate_rejected(self):
|
|
"""Test that duplicate announcements are rejected."""
|
|
# First announcement
|
|
client.post(
|
|
"/api/meeting-consent/announce?meeting_id=dup-announce&announced_by=Teacher"
|
|
)
|
|
|
|
# Second announcement
|
|
response = client.post(
|
|
"/api/meeting-consent/announce?meeting_id=dup-announce&announced_by=Teacher"
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
|
|
|
|
class TestParticipantsList:
|
|
"""Tests for listing participant consents."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self):
|
|
"""Create test consent with participants."""
|
|
from meeting_consent_api import _consent_store, _participant_consents
|
|
_consent_store.clear()
|
|
_participant_consents.clear()
|
|
|
|
client.post(
|
|
"/api/meeting-consent/request",
|
|
json={"meeting_id": "list-test", "participant_count": 2}
|
|
)
|
|
client.post(
|
|
"/api/meeting-consent/list-test/participant",
|
|
json={"participant_id": "user-1-uuid-12345678", "consented": True}
|
|
)
|
|
client.post(
|
|
"/api/meeting-consent/list-test/participant",
|
|
json={"participant_id": "user-2-uuid-87654321", "consented": False}
|
|
)
|
|
|
|
def test_get_participants_list(self):
|
|
"""Test getting list of participant consents."""
|
|
response = client.get("/api/meeting-consent/list-test/participants")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["participants"]) == 2
|
|
|
|
def test_participants_list_anonymized(self):
|
|
"""Test that participant IDs are anonymized."""
|
|
response = client.get("/api/meeting-consent/list-test/participants")
|
|
data = response.json()
|
|
|
|
# IDs should be truncated to last 8 chars
|
|
for p in data["participants"]:
|
|
assert len(p["participant_id"]) == 8
|
|
|
|
|
|
class TestHealthCheck:
|
|
"""Tests for health check endpoint."""
|
|
|
|
def test_health_check(self):
|
|
"""Test health check returns healthy status."""
|
|
response = client.get("/api/meeting-consent/health")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "healthy"
|