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:
407
backend/tests/test_consent_client.py
Normal file
407
backend/tests/test_consent_client.py
Normal file
@@ -0,0 +1,407 @@
|
||||
"""
|
||||
Tests für den Consent Client
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from consent_client import (
|
||||
generate_jwt_token,
|
||||
generate_demo_token,
|
||||
DocumentType,
|
||||
ConsentStatus,
|
||||
DocumentVersion,
|
||||
ConsentClient,
|
||||
JWT_SECRET,
|
||||
)
|
||||
|
||||
|
||||
class TestJWTTokenGeneration:
|
||||
"""Tests für JWT Token Generierung"""
|
||||
|
||||
def test_generate_jwt_token_default(self):
|
||||
"""Test JWT generation with default values"""
|
||||
token = generate_jwt_token()
|
||||
|
||||
assert token is not None
|
||||
assert isinstance(token, str)
|
||||
assert len(token) > 0
|
||||
|
||||
# Decode and verify
|
||||
decoded = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
assert "user_id" in decoded
|
||||
assert decoded["email"] == "demo@breakpilot.app"
|
||||
assert decoded["role"] == "user"
|
||||
assert "exp" in decoded
|
||||
assert "iat" in decoded
|
||||
|
||||
def test_generate_jwt_token_custom_values(self):
|
||||
"""Test JWT generation with custom values"""
|
||||
user_id = "test-user-123"
|
||||
email = "test@example.com"
|
||||
role = "admin"
|
||||
|
||||
token = generate_jwt_token(
|
||||
user_id=user_id,
|
||||
email=email,
|
||||
role=role,
|
||||
expires_hours=48
|
||||
)
|
||||
|
||||
decoded = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
assert decoded["user_id"] == user_id
|
||||
assert decoded["email"] == email
|
||||
assert decoded["role"] == role
|
||||
|
||||
def test_generate_jwt_token_expiration(self):
|
||||
"""Test that token expiration is set correctly"""
|
||||
token = generate_jwt_token(expires_hours=1)
|
||||
decoded = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
|
||||
exp = datetime.utcfromtimestamp(decoded["exp"])
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Should expire in approximately 1 hour
|
||||
time_diff = exp - now
|
||||
assert time_diff.total_seconds() > 3500 # At least 58 minutes
|
||||
assert time_diff.total_seconds() < 3700 # At most 62 minutes
|
||||
|
||||
def test_generate_demo_token(self):
|
||||
"""Test demo token generation"""
|
||||
token = generate_demo_token()
|
||||
|
||||
decoded = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
assert decoded["user_id"].startswith("demo-user-")
|
||||
assert decoded["email"] == "demo@breakpilot.app"
|
||||
assert decoded["role"] == "user"
|
||||
|
||||
def test_tokens_are_unique(self):
|
||||
"""Test that generated tokens are unique"""
|
||||
tokens = [generate_demo_token() for _ in range(10)]
|
||||
assert len(set(tokens)) == 10 # All tokens should be unique
|
||||
|
||||
def test_jwt_token_signature(self):
|
||||
"""Test that token signature is valid"""
|
||||
token = generate_jwt_token()
|
||||
|
||||
# Should not raise exception
|
||||
jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
|
||||
# Should raise exception with wrong secret
|
||||
with pytest.raises(jwt.InvalidSignatureError):
|
||||
jwt.decode(token, "wrong-secret", algorithms=["HS256"])
|
||||
|
||||
|
||||
class TestDocumentType:
|
||||
"""Tests für DocumentType Enum"""
|
||||
|
||||
def test_document_types(self):
|
||||
"""Test all document types exist"""
|
||||
assert DocumentType.TERMS.value == "terms"
|
||||
assert DocumentType.PRIVACY.value == "privacy"
|
||||
assert DocumentType.COOKIES.value == "cookies"
|
||||
assert DocumentType.COMMUNITY.value == "community"
|
||||
|
||||
def test_document_type_is_string(self):
|
||||
"""Test that document types can be used as strings"""
|
||||
assert str(DocumentType.TERMS) == "DocumentType.TERMS"
|
||||
assert DocumentType.TERMS.value == "terms"
|
||||
|
||||
|
||||
class TestConsentStatus:
|
||||
"""Tests für ConsentStatus Dataclass"""
|
||||
|
||||
def test_consent_status_basic(self):
|
||||
"""Test basic ConsentStatus creation"""
|
||||
status = ConsentStatus(has_consent=True)
|
||||
|
||||
assert status.has_consent is True
|
||||
assert status.current_version_id is None
|
||||
assert status.consented_version is None
|
||||
assert status.needs_update is False
|
||||
assert status.consented_at is None
|
||||
|
||||
def test_consent_status_full(self):
|
||||
"""Test ConsentStatus with all fields"""
|
||||
status = ConsentStatus(
|
||||
has_consent=True,
|
||||
current_version_id="version-123",
|
||||
consented_version="1.0.0",
|
||||
needs_update=False,
|
||||
consented_at="2024-01-01T00:00:00Z"
|
||||
)
|
||||
|
||||
assert status.has_consent is True
|
||||
assert status.current_version_id == "version-123"
|
||||
assert status.consented_version == "1.0.0"
|
||||
assert status.needs_update is False
|
||||
assert status.consented_at == "2024-01-01T00:00:00Z"
|
||||
|
||||
|
||||
class TestDocumentVersion:
|
||||
"""Tests für DocumentVersion Dataclass"""
|
||||
|
||||
def test_document_version_creation(self):
|
||||
"""Test DocumentVersion creation"""
|
||||
version = DocumentVersion(
|
||||
id="doc-version-123",
|
||||
document_id="doc-123",
|
||||
version="1.0.0",
|
||||
language="de",
|
||||
title="Test Document",
|
||||
content="<p>Test content</p>",
|
||||
summary="Test summary"
|
||||
)
|
||||
|
||||
assert version.id == "doc-version-123"
|
||||
assert version.document_id == "doc-123"
|
||||
assert version.version == "1.0.0"
|
||||
assert version.language == "de"
|
||||
assert version.title == "Test Document"
|
||||
assert version.content == "<p>Test content</p>"
|
||||
assert version.summary == "Test summary"
|
||||
|
||||
|
||||
class TestConsentClient:
|
||||
"""Tests für ConsentClient"""
|
||||
|
||||
def test_client_initialization(self):
|
||||
"""Test client initialization"""
|
||||
client = ConsentClient()
|
||||
# In Docker: consent-service:8081, locally: localhost:8081
|
||||
assert client.base_url in ("http://localhost:8081", "http://consent-service:8081")
|
||||
assert "/api/v1" in client.api_url
|
||||
|
||||
def test_client_custom_url(self):
|
||||
"""Test client with custom URL"""
|
||||
client = ConsentClient(base_url="https://custom.example.com/")
|
||||
assert client.base_url == "https://custom.example.com"
|
||||
assert client.api_url == "https://custom.example.com/api/v1"
|
||||
|
||||
def test_get_headers(self):
|
||||
"""Test header generation"""
|
||||
client = ConsentClient()
|
||||
token = "test-token-123"
|
||||
|
||||
headers = client._get_headers(token)
|
||||
|
||||
assert headers["Authorization"] == "Bearer test-token-123"
|
||||
assert headers["Content-Type"] == "application/json"
|
||||
|
||||
|
||||
class TestConsentClientAsync:
|
||||
"""Async tests für ConsentClient"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_consent_success(self):
|
||||
"""Test successful consent check"""
|
||||
client = ConsentClient()
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"has_consent": True,
|
||||
"current_version_id": "version-123",
|
||||
"consented_version": "1.0.0",
|
||||
"needs_update": False,
|
||||
"consented_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.get.return_value = mock_response
|
||||
mock_instance.__aenter__.return_value = mock_instance
|
||||
mock_instance.__aexit__.return_value = None
|
||||
mock_client.return_value = mock_instance
|
||||
|
||||
status = await client.check_consent(
|
||||
jwt_token="test-token",
|
||||
document_type=DocumentType.TERMS
|
||||
)
|
||||
|
||||
assert status.has_consent is True
|
||||
assert status.current_version_id == "version-123"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_consent_not_found(self):
|
||||
"""Test consent check when user has no consent"""
|
||||
client = ConsentClient()
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 404
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.get.return_value = mock_response
|
||||
mock_instance.__aenter__.return_value = mock_instance
|
||||
mock_instance.__aexit__.return_value = None
|
||||
mock_client.return_value = mock_instance
|
||||
|
||||
status = await client.check_consent(
|
||||
jwt_token="test-token",
|
||||
document_type=DocumentType.TERMS
|
||||
)
|
||||
|
||||
assert status.has_consent is False
|
||||
assert status.needs_update is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_consent_connection_error(self):
|
||||
"""Test consent check when service is unavailable"""
|
||||
import httpx
|
||||
|
||||
client = ConsentClient()
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.get.side_effect = httpx.RequestError("Connection error")
|
||||
mock_instance.__aenter__.return_value = mock_instance
|
||||
mock_instance.__aexit__.return_value = None
|
||||
mock_client.return_value = mock_instance
|
||||
|
||||
status = await client.check_consent(
|
||||
jwt_token="test-token",
|
||||
document_type=DocumentType.TERMS
|
||||
)
|
||||
|
||||
# Should not block user when service is unavailable
|
||||
assert status.has_consent is True
|
||||
assert status.needs_update is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_success(self):
|
||||
"""Test successful health check"""
|
||||
client = ConsentClient()
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.get.return_value = mock_response
|
||||
mock_instance.__aenter__.return_value = mock_instance
|
||||
mock_instance.__aexit__.return_value = None
|
||||
mock_client.return_value = mock_instance
|
||||
|
||||
is_healthy = await client.health_check()
|
||||
assert is_healthy is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_failure(self):
|
||||
"""Test failed health check"""
|
||||
import httpx
|
||||
|
||||
client = ConsentClient()
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.get.side_effect = httpx.RequestError("Connection refused")
|
||||
mock_instance.__aenter__.return_value = mock_instance
|
||||
mock_instance.__aexit__.return_value = None
|
||||
mock_client.return_value = mock_instance
|
||||
|
||||
is_healthy = await client.health_check()
|
||||
assert is_healthy is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_give_consent_success(self):
|
||||
"""Test successful consent submission"""
|
||||
client = ConsentClient()
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 201
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.post.return_value = mock_response
|
||||
mock_instance.__aenter__.return_value = mock_instance
|
||||
mock_instance.__aexit__.return_value = None
|
||||
mock_client.return_value = mock_instance
|
||||
|
||||
success = await client.give_consent(
|
||||
jwt_token="test-token",
|
||||
document_type="terms",
|
||||
version_id="version-123",
|
||||
consented=True
|
||||
)
|
||||
|
||||
assert success is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_give_consent_failure(self):
|
||||
"""Test failed consent submission"""
|
||||
client = ConsentClient()
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 400
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.post.return_value = mock_response
|
||||
mock_instance.__aenter__.return_value = mock_instance
|
||||
mock_instance.__aexit__.return_value = None
|
||||
mock_client.return_value = mock_instance
|
||||
|
||||
success = await client.give_consent(
|
||||
jwt_token="test-token",
|
||||
document_type="terms",
|
||||
version_id="version-123",
|
||||
consented=True
|
||||
)
|
||||
|
||||
assert success is False
|
||||
|
||||
|
||||
class TestValidation:
|
||||
"""Tests für Validierungslogik"""
|
||||
|
||||
def test_valid_document_types(self):
|
||||
"""Test that only valid document types are accepted"""
|
||||
valid_types = ["terms", "privacy", "cookies", "community"]
|
||||
|
||||
for doc_type in DocumentType:
|
||||
assert doc_type.value in valid_types
|
||||
|
||||
def test_jwt_expiration_validation(self):
|
||||
"""Test that expired tokens are rejected"""
|
||||
# Create token that expired 1 hour ago
|
||||
expired_payload = {
|
||||
"user_id": "test-user",
|
||||
"email": "test@example.com",
|
||||
"role": "user",
|
||||
"exp": datetime.utcnow() - timedelta(hours=1),
|
||||
"iat": datetime.utcnow() - timedelta(hours=2),
|
||||
}
|
||||
|
||||
expired_token = jwt.encode(expired_payload, JWT_SECRET, algorithm="HS256")
|
||||
|
||||
with pytest.raises(jwt.ExpiredSignatureError):
|
||||
jwt.decode(expired_token, JWT_SECRET, algorithms=["HS256"])
|
||||
|
||||
|
||||
# Performance Tests
|
||||
class TestPerformance:
|
||||
"""Performance tests"""
|
||||
|
||||
def test_token_generation_performance(self):
|
||||
"""Test that token generation is fast"""
|
||||
import time
|
||||
|
||||
start = time.time()
|
||||
for _ in range(100):
|
||||
generate_jwt_token()
|
||||
elapsed = time.time() - start
|
||||
|
||||
# Should generate 100 tokens in less than 1 second
|
||||
assert elapsed < 1.0, f"Token generation too slow: {elapsed}s for 100 tokens"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user