Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
408 lines
13 KiB
Python
408 lines
13 KiB
Python
"""
|
|
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"])
|