Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
613 lines
20 KiB
Python
613 lines
20 KiB
Python
"""
|
|
Unit Tests für DSMS Gateway
|
|
Tests für alle API-Endpoints und Hilfsfunktionen
|
|
"""
|
|
|
|
import pytest
|
|
import hashlib
|
|
import json
|
|
from unittest.mock import AsyncMock, patch, MagicMock
|
|
from fastapi.testclient import TestClient
|
|
from httpx import Response
|
|
|
|
# Import der App
|
|
from main import app, DocumentMetadata, StoredDocument, DocumentList
|
|
|
|
|
|
# Test Client
|
|
client = TestClient(app)
|
|
|
|
|
|
# ==================== Fixtures ====================
|
|
|
|
@pytest.fixture
|
|
def valid_auth_header():
|
|
"""Gültiger Authorization Header für Tests"""
|
|
return {"Authorization": "Bearer test-token-12345"}
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_document_metadata():
|
|
"""Beispiel-Metadaten für Tests"""
|
|
return DocumentMetadata(
|
|
document_type="legal_document",
|
|
document_id="doc-123",
|
|
version="1.0",
|
|
language="de",
|
|
created_at="2024-01-01T00:00:00",
|
|
checksum="abc123",
|
|
encrypted=False
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_ipfs_response():
|
|
"""Mock-Antwort von IPFS add"""
|
|
return {
|
|
"Hash": "QmTest1234567890abcdef",
|
|
"Size": "1024"
|
|
}
|
|
|
|
|
|
# ==================== Health Check Tests ====================
|
|
|
|
class TestHealthCheck:
|
|
"""Tests für den Health Check Endpoint"""
|
|
|
|
def test_health_check_ipfs_connected(self):
|
|
"""Test: Health Check wenn IPFS verbunden ist"""
|
|
with patch("main.httpx.AsyncClient") as mock_client:
|
|
mock_instance = AsyncMock()
|
|
mock_instance.post.return_value = MagicMock(status_code=200)
|
|
mock_client.return_value.__aenter__.return_value = mock_instance
|
|
|
|
response = client.get("/health")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "status" in data
|
|
assert "ipfs_connected" in data
|
|
assert "timestamp" in data
|
|
|
|
def test_health_check_ipfs_disconnected(self):
|
|
"""Test: Health Check wenn IPFS nicht erreichbar"""
|
|
with patch("main.httpx.AsyncClient") as mock_client:
|
|
mock_instance = AsyncMock()
|
|
mock_instance.post.side_effect = Exception("Connection failed")
|
|
mock_client.return_value.__aenter__.return_value = mock_instance
|
|
|
|
response = client.get("/health")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "degraded"
|
|
assert data["ipfs_connected"] is False
|
|
|
|
|
|
# ==================== Authorization Tests ====================
|
|
|
|
class TestAuthorization:
|
|
"""Tests für die Autorisierung"""
|
|
|
|
def test_documents_endpoint_without_auth_returns_401(self):
|
|
"""Test: Dokument-Endpoint ohne Auth gibt 401 zurück"""
|
|
response = client.get("/api/v1/documents")
|
|
assert response.status_code == 401
|
|
|
|
def test_documents_endpoint_with_invalid_token_returns_401(self):
|
|
"""Test: Ungültiges Token-Format gibt 401 zurück"""
|
|
response = client.get(
|
|
"/api/v1/documents",
|
|
headers={"Authorization": "InvalidFormat"}
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
def test_documents_endpoint_with_valid_token_format(self, valid_auth_header):
|
|
"""Test: Gültiges Token-Format wird akzeptiert"""
|
|
with patch("main.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls:
|
|
mock_pin_ls.return_value = []
|
|
|
|
response = client.get(
|
|
"/api/v1/documents",
|
|
headers=valid_auth_header
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
# ==================== Document Storage Tests ====================
|
|
|
|
class TestDocumentStorage:
|
|
"""Tests für das Speichern von Dokumenten"""
|
|
|
|
def test_store_document_success(self, valid_auth_header, mock_ipfs_response):
|
|
"""Test: Dokument erfolgreich speichern"""
|
|
with patch("main.ipfs_add", new_callable=AsyncMock) as mock_add:
|
|
mock_add.return_value = mock_ipfs_response
|
|
|
|
test_content = b"Test document content"
|
|
|
|
response = client.post(
|
|
"/api/v1/documents",
|
|
headers=valid_auth_header,
|
|
files={"file": ("test.txt", test_content, "text/plain")},
|
|
data={
|
|
"document_type": "legal_document",
|
|
"document_id": "doc-123",
|
|
"version": "1.0",
|
|
"language": "de"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "cid" in data
|
|
assert data["cid"] == "QmTest1234567890abcdef"
|
|
assert "metadata" in data
|
|
assert "gateway_url" in data
|
|
|
|
def test_store_document_calculates_checksum(self, valid_auth_header, mock_ipfs_response):
|
|
"""Test: Checksum wird korrekt berechnet"""
|
|
with patch("main.ipfs_add", new_callable=AsyncMock) as mock_add:
|
|
mock_add.return_value = mock_ipfs_response
|
|
|
|
test_content = b"Test content for checksum"
|
|
expected_checksum = hashlib.sha256(test_content).hexdigest()
|
|
|
|
response = client.post(
|
|
"/api/v1/documents",
|
|
headers=valid_auth_header,
|
|
files={"file": ("test.txt", test_content, "text/plain")}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["metadata"]["checksum"] == expected_checksum
|
|
|
|
def test_store_document_without_file_returns_422(self, valid_auth_header):
|
|
"""Test: Fehlende Datei gibt 422 zurück"""
|
|
response = client.post(
|
|
"/api/v1/documents",
|
|
headers=valid_auth_header
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
|
|
|
|
# ==================== Document Retrieval Tests ====================
|
|
|
|
class TestDocumentRetrieval:
|
|
"""Tests für das Abrufen von Dokumenten"""
|
|
|
|
def test_get_document_success(self, valid_auth_header):
|
|
"""Test: Dokument erfolgreich abrufen"""
|
|
test_content = b"Original content"
|
|
package = {
|
|
"metadata": {
|
|
"document_type": "legal_document",
|
|
"checksum": hashlib.sha256(test_content).hexdigest()
|
|
},
|
|
"content_base64": test_content.hex(),
|
|
"filename": "test.txt"
|
|
}
|
|
|
|
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
|
mock_cat.return_value = json.dumps(package).encode()
|
|
|
|
response = client.get(
|
|
"/api/v1/documents/QmTestCid123",
|
|
headers=valid_auth_header
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.content == test_content
|
|
|
|
def test_get_document_not_found(self, valid_auth_header):
|
|
"""Test: Nicht existierendes Dokument gibt 404 zurück"""
|
|
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
|
from fastapi import HTTPException
|
|
mock_cat.side_effect = HTTPException(status_code=404, detail="Not found")
|
|
|
|
response = client.get(
|
|
"/api/v1/documents/QmNonExistent",
|
|
headers=valid_auth_header
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
def test_get_document_metadata_success(self, valid_auth_header):
|
|
"""Test: Dokument-Metadaten abrufen"""
|
|
test_content = b"Content"
|
|
package = {
|
|
"metadata": {
|
|
"document_type": "legal_document",
|
|
"document_id": "doc-123",
|
|
"version": "1.0"
|
|
},
|
|
"content_base64": test_content.hex(),
|
|
"filename": "test.txt"
|
|
}
|
|
|
|
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
|
mock_cat.return_value = json.dumps(package).encode()
|
|
|
|
response = client.get(
|
|
"/api/v1/documents/QmTestCid123/metadata",
|
|
headers=valid_auth_header
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["cid"] == "QmTestCid123"
|
|
assert data["metadata"]["document_type"] == "legal_document"
|
|
|
|
|
|
# ==================== Document List Tests ====================
|
|
|
|
class TestDocumentList:
|
|
"""Tests für das Auflisten von Dokumenten"""
|
|
|
|
def test_list_documents_empty(self, valid_auth_header):
|
|
"""Test: Leere Dokumentenliste"""
|
|
with patch("main.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls:
|
|
mock_pin_ls.return_value = []
|
|
|
|
response = client.get(
|
|
"/api/v1/documents",
|
|
headers=valid_auth_header
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["documents"] == []
|
|
assert data["total"] == 0
|
|
|
|
def test_list_documents_with_items(self, valid_auth_header):
|
|
"""Test: Dokumentenliste mit Einträgen"""
|
|
package = {
|
|
"metadata": {"document_type": "legal_document"},
|
|
"content_base64": "68656c6c6f",
|
|
"filename": "test.txt"
|
|
}
|
|
|
|
with patch("main.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls:
|
|
mock_pin_ls.return_value = ["QmCid1", "QmCid2"]
|
|
|
|
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
|
mock_cat.return_value = json.dumps(package).encode()
|
|
|
|
response = client.get(
|
|
"/api/v1/documents",
|
|
headers=valid_auth_header
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] == 2
|
|
|
|
|
|
# ==================== Document Deletion Tests ====================
|
|
|
|
class TestDocumentDeletion:
|
|
"""Tests für das Löschen von Dokumenten"""
|
|
|
|
def test_unpin_document_success(self, valid_auth_header):
|
|
"""Test: Dokument erfolgreich unpinnen"""
|
|
with patch("main.httpx.AsyncClient") as mock_client:
|
|
mock_instance = AsyncMock()
|
|
mock_instance.post.return_value = MagicMock(status_code=200)
|
|
mock_client.return_value.__aenter__.return_value = mock_instance
|
|
|
|
response = client.delete(
|
|
"/api/v1/documents/QmTestCid123",
|
|
headers=valid_auth_header
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "unpinned"
|
|
assert data["cid"] == "QmTestCid123"
|
|
|
|
def test_unpin_document_not_found(self, valid_auth_header):
|
|
"""Test: Nicht existierendes Dokument unpinnen"""
|
|
with patch("main.httpx.AsyncClient") as mock_client:
|
|
mock_instance = AsyncMock()
|
|
mock_instance.post.return_value = MagicMock(status_code=404)
|
|
mock_client.return_value.__aenter__.return_value = mock_instance
|
|
|
|
response = client.delete(
|
|
"/api/v1/documents/QmNonExistent",
|
|
headers=valid_auth_header
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
# ==================== Legal Document Archive Tests ====================
|
|
|
|
class TestLegalDocumentArchive:
|
|
"""Tests für die Legal Document Archivierung"""
|
|
|
|
def test_archive_legal_document_success(self, valid_auth_header, mock_ipfs_response):
|
|
"""Test: Legal Document erfolgreich archivieren"""
|
|
with patch("main.ipfs_add", new_callable=AsyncMock) as mock_add:
|
|
mock_add.return_value = mock_ipfs_response
|
|
|
|
response = client.post(
|
|
"/api/v1/legal-documents/archive",
|
|
headers=valid_auth_header,
|
|
params={
|
|
"document_id": "privacy-policy",
|
|
"version": "2.0",
|
|
"content": "<h1>Datenschutzerklärung</h1>",
|
|
"language": "de"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "cid" in data
|
|
assert data["document_id"] == "privacy-policy"
|
|
assert data["version"] == "2.0"
|
|
assert "checksum" in data
|
|
assert "archived_at" in data
|
|
|
|
def test_archive_legal_document_calculates_checksum(self, valid_auth_header, mock_ipfs_response):
|
|
"""Test: Checksum für HTML-Inhalt korrekt berechnet"""
|
|
content = "<h1>Test Content</h1>"
|
|
expected_checksum = hashlib.sha256(content.encode('utf-8')).hexdigest()
|
|
|
|
with patch("main.ipfs_add", new_callable=AsyncMock) as mock_add:
|
|
mock_add.return_value = mock_ipfs_response
|
|
|
|
response = client.post(
|
|
"/api/v1/legal-documents/archive",
|
|
headers=valid_auth_header,
|
|
params={
|
|
"document_id": "terms",
|
|
"version": "1.0",
|
|
"content": content
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["checksum"] == expected_checksum
|
|
|
|
|
|
# ==================== Document Verification Tests ====================
|
|
|
|
class TestDocumentVerification:
|
|
"""Tests für die Dokumenten-Verifizierung"""
|
|
|
|
def test_verify_document_integrity_valid(self):
|
|
"""Test: Dokument mit gültiger Integrität"""
|
|
content = "Test content"
|
|
checksum = hashlib.sha256(content.encode('utf-8')).hexdigest()
|
|
|
|
package = {
|
|
"metadata": {
|
|
"document_type": "legal_document",
|
|
"checksum": checksum
|
|
},
|
|
"content": content
|
|
}
|
|
|
|
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
|
mock_cat.return_value = json.dumps(package).encode()
|
|
|
|
response = client.get("/api/v1/verify/QmTestCid123")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["exists"] is True
|
|
assert data["integrity_valid"] is True
|
|
assert data["stored_checksum"] == checksum
|
|
assert data["calculated_checksum"] == checksum
|
|
|
|
def test_verify_document_integrity_invalid(self):
|
|
"""Test: Dokument mit ungültiger Integrität (manipuliert)"""
|
|
package = {
|
|
"metadata": {
|
|
"document_type": "legal_document",
|
|
"checksum": "fake_checksum_12345"
|
|
},
|
|
"content": "Actual content"
|
|
}
|
|
|
|
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
|
mock_cat.return_value = json.dumps(package).encode()
|
|
|
|
response = client.get("/api/v1/verify/QmTestCid123")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["exists"] is True
|
|
assert data["integrity_valid"] is False
|
|
|
|
def test_verify_document_not_found(self):
|
|
"""Test: Nicht existierendes Dokument verifizieren"""
|
|
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
|
mock_cat.side_effect = Exception("Not found")
|
|
|
|
response = client.get("/api/v1/verify/QmNonExistent")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["exists"] is False
|
|
assert "error" in data
|
|
|
|
def test_verify_document_public_access(self):
|
|
"""Test: Verifizierung ist öffentlich zugänglich (keine Auth)"""
|
|
package = {
|
|
"metadata": {"checksum": "abc"},
|
|
"content": "test"
|
|
}
|
|
|
|
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
|
mock_cat.return_value = json.dumps(package).encode()
|
|
|
|
# Kein Authorization Header!
|
|
response = client.get("/api/v1/verify/QmTestCid123")
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
# ==================== Node Info Tests ====================
|
|
|
|
class TestNodeInfo:
|
|
"""Tests für Node-Informationen"""
|
|
|
|
def test_get_node_info_success(self):
|
|
"""Test: Node-Informationen abrufen"""
|
|
id_response = {
|
|
"ID": "QmNodeId12345",
|
|
"ProtocolVersion": "ipfs/0.1.0",
|
|
"AgentVersion": "kubo/0.24.0",
|
|
"Addresses": ["/ip4/127.0.0.1/tcp/4001"]
|
|
}
|
|
stat_response = {
|
|
"RepoSize": 1048576,
|
|
"StorageMax": 10737418240,
|
|
"NumObjects": 42
|
|
}
|
|
|
|
with patch("main.httpx.AsyncClient") as mock_client:
|
|
mock_instance = AsyncMock()
|
|
|
|
async def mock_post(url, **kwargs):
|
|
mock_resp = MagicMock()
|
|
if "id" in url:
|
|
mock_resp.status_code = 200
|
|
mock_resp.json.return_value = id_response
|
|
elif "stat" in url:
|
|
mock_resp.status_code = 200
|
|
mock_resp.json.return_value = stat_response
|
|
return mock_resp
|
|
|
|
mock_instance.post = mock_post
|
|
mock_client.return_value.__aenter__.return_value = mock_instance
|
|
|
|
response = client.get("/api/v1/node/info")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["node_id"] == "QmNodeId12345"
|
|
assert data["num_objects"] == 42
|
|
|
|
def test_get_node_info_public_access(self):
|
|
"""Test: Node-Info ist öffentlich zugänglich"""
|
|
with patch("main.httpx.AsyncClient") as mock_client:
|
|
mock_instance = AsyncMock()
|
|
mock_instance.post.return_value = MagicMock(
|
|
status_code=200,
|
|
json=lambda: {}
|
|
)
|
|
mock_client.return_value.__aenter__.return_value = mock_instance
|
|
|
|
# Kein Authorization Header!
|
|
response = client.get("/api/v1/node/info")
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
# ==================== Model Tests ====================
|
|
|
|
class TestModels:
|
|
"""Tests für Pydantic Models"""
|
|
|
|
def test_document_metadata_defaults(self):
|
|
"""Test: DocumentMetadata Default-Werte"""
|
|
metadata = DocumentMetadata(document_type="test")
|
|
|
|
assert metadata.document_type == "test"
|
|
assert metadata.document_id is None
|
|
assert metadata.version is None
|
|
assert metadata.language == "de"
|
|
assert metadata.encrypted is False
|
|
|
|
def test_document_metadata_all_fields(self):
|
|
"""Test: DocumentMetadata mit allen Feldern"""
|
|
metadata = DocumentMetadata(
|
|
document_type="legal_document",
|
|
document_id="doc-123",
|
|
version="1.0",
|
|
language="en",
|
|
created_at="2024-01-01T00:00:00",
|
|
checksum="abc123",
|
|
encrypted=True
|
|
)
|
|
|
|
assert metadata.document_type == "legal_document"
|
|
assert metadata.document_id == "doc-123"
|
|
assert metadata.version == "1.0"
|
|
assert metadata.language == "en"
|
|
assert metadata.encrypted is True
|
|
|
|
def test_stored_document_model(self, sample_document_metadata):
|
|
"""Test: StoredDocument Model"""
|
|
stored = StoredDocument(
|
|
cid="QmTest123",
|
|
size=1024,
|
|
metadata=sample_document_metadata,
|
|
gateway_url="http://localhost:8080/ipfs/QmTest123",
|
|
timestamp="2024-01-01T00:00:00"
|
|
)
|
|
|
|
assert stored.cid == "QmTest123"
|
|
assert stored.size == 1024
|
|
assert stored.metadata.document_type == "legal_document"
|
|
|
|
def test_document_list_model(self):
|
|
"""Test: DocumentList Model"""
|
|
doc_list = DocumentList(
|
|
documents=[{"cid": "Qm1"}, {"cid": "Qm2"}],
|
|
total=2
|
|
)
|
|
|
|
assert doc_list.total == 2
|
|
assert len(doc_list.documents) == 2
|
|
|
|
|
|
# ==================== Integration Tests ====================
|
|
|
|
class TestIntegration:
|
|
"""Integration Tests (erfordern laufenden IPFS Node)"""
|
|
|
|
@pytest.mark.skip(reason="Erfordert laufenden IPFS Node")
|
|
def test_full_document_lifecycle(self, valid_auth_header):
|
|
"""Integration Test: Vollständiger Dokument-Lebenszyklus"""
|
|
# 1. Dokument speichern
|
|
response = client.post(
|
|
"/api/v1/documents",
|
|
headers=valid_auth_header,
|
|
files={"file": ("test.txt", b"Test content", "text/plain")}
|
|
)
|
|
assert response.status_code == 200
|
|
cid = response.json()["cid"]
|
|
|
|
# 2. Dokument abrufen
|
|
response = client.get(
|
|
f"/api/v1/documents/{cid}",
|
|
headers=valid_auth_header
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# 3. Verifizieren
|
|
response = client.get(f"/api/v1/verify/{cid}")
|
|
assert response.status_code == 200
|
|
assert response.json()["integrity_valid"] is True
|
|
|
|
# 4. Unpinnen
|
|
response = client.delete(
|
|
f"/api/v1/documents/{cid}",
|
|
headers=valid_auth_header
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
# ==================== Run Tests ====================
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|