""" 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": "

Datenschutzerklärung

", "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 = "

Test Content

" 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"])