refactor(consent-sdk,dsms-gateway): split ConsentManager, types, and main.py

- consent-sdk/src/types/index.ts: extracted 438 LOC into core.ts, config.ts,
  vendor.ts, api.ts, events.ts, storage.ts, translations.ts; index.ts is now
  a 21-LOC barrel re-exporter
- consent-sdk/src/core/ConsentManager.ts: extracted normalizeConsentInput,
  isConsentExpired, needsConsent, ALL_CATEGORIES, MINIMAL_CATEGORIES into
  consent-manager-helpers.ts; reduced from 467 to 345 LOC
- dsms-gateway/main.py: extracted models → models.py, config → config.py,
  IPFS helpers + verify_token → dependencies.py, route handlers →
  routers/documents.py and routers/node.py; main.py is now a 41-LOC app
  factory; test mock paths updated accordingly (27/27 tests pass)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-18 08:42:32 +02:00
parent 9ecd3b2d84
commit a7fe32fb82
18 changed files with 1115 additions and 1042 deletions

9
dsms-gateway/config.py Normal file
View File

@@ -0,0 +1,9 @@
"""
DSMS Gateway — runtime configuration from environment variables.
"""
import os
IPFS_API_URL = os.getenv("IPFS_API_URL", "http://dsms-node:5001")
IPFS_GATEWAY_URL = os.getenv("IPFS_GATEWAY_URL", "http://dsms-node:8080")
JWT_SECRET = os.getenv("JWT_SECRET", "your-super-secret-jwt-key-change-in-production")

View File

@@ -0,0 +1,76 @@
"""
DSMS Gateway — shared FastAPI dependencies and IPFS helper coroutines.
"""
from typing import Optional
import httpx
from fastapi import Header, HTTPException
from config import IPFS_API_URL
async def verify_token(authorization: Optional[str] = Header(None)) -> dict:
"""Verifiziert JWT Token (vereinfacht für MVP)"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization header fehlt")
# In Produktion: JWT validieren
# Für MVP: Einfache Token-Prüfung
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Ungültiges Token-Format")
return {"valid": True}
async def ipfs_add(content: bytes, pin: bool = True) -> dict:
"""Fügt Inhalt zu IPFS hinzu"""
async with httpx.AsyncClient(timeout=60.0) as client:
files = {"file": ("document", content)}
params = {"pin": str(pin).lower()}
response = await client.post(
f"{IPFS_API_URL}/api/v0/add",
files=files,
params=params
)
if response.status_code != 200:
raise HTTPException(
status_code=502,
detail=f"IPFS Fehler: {response.text}"
)
return response.json()
async def ipfs_cat(cid: str) -> bytes:
"""Liest Inhalt von IPFS"""
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
f"{IPFS_API_URL}/api/v0/cat",
params={"arg": cid}
)
if response.status_code != 200:
raise HTTPException(
status_code=404,
detail=f"Dokument nicht gefunden: {cid}"
)
return response.content
async def ipfs_pin_ls() -> list:
"""Listet alle gepinnten Objekte"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{IPFS_API_URL}/api/v0/pin/ls",
params={"type": "recursive"}
)
if response.status_code != 200:
return []
data = response.json()
return list(data.get("Keys", {}).keys())

View File

@@ -3,17 +3,18 @@ DSMS Gateway - REST API für dezentrales Speichersystem
Bietet eine vereinfachte API über IPFS für BreakPilot
"""
import sys
import os
import json
import httpx
import hashlib
from datetime import datetime
from typing import Optional
from fastapi import FastAPI, HTTPException, UploadFile, File, Header, Depends
# Ensure the gateway directory itself is on the path so routers can use flat imports.
sys.path.insert(0, os.path.dirname(__file__))
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import io
from models import DocumentMetadata, StoredDocument, DocumentList # noqa: F401 — re-exported for tests
from routers.documents import router as documents_router
from routers.node import router as node_router
app = FastAPI(
title="DSMS Gateway",
@@ -30,436 +31,9 @@ app.add_middleware(
allow_headers=["*"],
)
# Configuration
IPFS_API_URL = os.getenv("IPFS_API_URL", "http://dsms-node:5001")
IPFS_GATEWAY_URL = os.getenv("IPFS_GATEWAY_URL", "http://dsms-node:8080")
JWT_SECRET = os.getenv("JWT_SECRET", "your-super-secret-jwt-key-change-in-production")
# Models
class DocumentMetadata(BaseModel):
"""Metadaten für gespeicherte Dokumente"""
document_type: str # 'legal_document', 'consent_record', 'audit_log'
document_id: Optional[str] = None
version: Optional[str] = None
language: Optional[str] = "de"
created_at: Optional[str] = None
checksum: Optional[str] = None
encrypted: bool = False
class StoredDocument(BaseModel):
"""Antwort nach erfolgreichem Speichern"""
cid: str # Content Identifier (IPFS Hash)
size: int
metadata: DocumentMetadata
gateway_url: str
timestamp: str
class DocumentList(BaseModel):
"""Liste der gespeicherten Dokumente"""
documents: list
total: int
# Helper Functions
async def verify_token(authorization: Optional[str] = Header(None)) -> dict:
"""Verifiziert JWT Token (vereinfacht für MVP)"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization header fehlt")
# In Produktion: JWT validieren
# Für MVP: Einfache Token-Prüfung
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Ungültiges Token-Format")
return {"valid": True}
async def ipfs_add(content: bytes, pin: bool = True) -> dict:
"""Fügt Inhalt zu IPFS hinzu"""
async with httpx.AsyncClient(timeout=60.0) as client:
files = {"file": ("document", content)}
params = {"pin": str(pin).lower()}
response = await client.post(
f"{IPFS_API_URL}/api/v0/add",
files=files,
params=params
)
if response.status_code != 200:
raise HTTPException(
status_code=502,
detail=f"IPFS Fehler: {response.text}"
)
return response.json()
async def ipfs_cat(cid: str) -> bytes:
"""Liest Inhalt von IPFS"""
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
f"{IPFS_API_URL}/api/v0/cat",
params={"arg": cid}
)
if response.status_code != 200:
raise HTTPException(
status_code=404,
detail=f"Dokument nicht gefunden: {cid}"
)
return response.content
async def ipfs_pin_ls() -> list:
"""Listet alle gepinnten Objekte"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{IPFS_API_URL}/api/v0/pin/ls",
params={"type": "recursive"}
)
if response.status_code != 200:
return []
data = response.json()
return list(data.get("Keys", {}).keys())
# API Endpoints
@app.get("/health")
async def health_check():
"""Health Check für DSMS Gateway"""
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.post(f"{IPFS_API_URL}/api/v0/id")
ipfs_status = response.status_code == 200
except Exception:
ipfs_status = False
return {
"status": "healthy" if ipfs_status else "degraded",
"ipfs_connected": ipfs_status,
"timestamp": datetime.utcnow().isoformat()
}
@app.post("/api/v1/documents", response_model=StoredDocument)
async def store_document(
file: UploadFile = File(...),
document_type: str = "legal_document",
document_id: Optional[str] = None,
version: Optional[str] = None,
language: str = "de",
_auth: dict = Depends(verify_token)
):
"""
Speichert ein Dokument im DSMS.
- **file**: Das zu speichernde Dokument
- **document_type**: Typ des Dokuments (legal_document, consent_record, audit_log)
- **document_id**: Optionale ID des Dokuments
- **version**: Optionale Versionsnummer
- **language**: Sprache (default: de)
"""
content = await file.read()
# Checksum berechnen
checksum = hashlib.sha256(content).hexdigest()
# Metadaten erstellen
metadata = DocumentMetadata(
document_type=document_type,
document_id=document_id,
version=version,
language=language,
created_at=datetime.utcnow().isoformat(),
checksum=checksum,
encrypted=False
)
# Dokument mit Metadaten als JSON verpacken
package = {
"metadata": metadata.model_dump(),
"content_base64": content.hex(), # Hex-encodiert für JSON
"filename": file.filename
}
package_bytes = json.dumps(package).encode()
# Zu IPFS hinzufügen
result = await ipfs_add(package_bytes)
cid = result.get("Hash")
size = int(result.get("Size", 0))
return StoredDocument(
cid=cid,
size=size,
metadata=metadata,
gateway_url=f"{IPFS_GATEWAY_URL}/ipfs/{cid}",
timestamp=datetime.utcnow().isoformat()
)
@app.get("/api/v1/documents/{cid}")
async def get_document(
cid: str,
_auth: dict = Depends(verify_token)
):
"""
Ruft ein Dokument aus dem DSMS ab.
- **cid**: Content Identifier (IPFS Hash)
"""
content = await ipfs_cat(cid)
try:
package = json.loads(content)
metadata = package.get("metadata", {})
original_content = bytes.fromhex(package.get("content_base64", ""))
filename = package.get("filename", "document")
return StreamingResponse(
io.BytesIO(original_content),
media_type="application/octet-stream",
headers={
"Content-Disposition": f'attachment; filename="{filename}"',
"X-DSMS-Document-Type": metadata.get("document_type", "unknown"),
"X-DSMS-Checksum": metadata.get("checksum", ""),
"X-DSMS-Created-At": metadata.get("created_at", "")
}
)
except json.JSONDecodeError:
# Wenn es kein DSMS-Paket ist, gib rohen Inhalt zurück
return StreamingResponse(
io.BytesIO(content),
media_type="application/octet-stream"
)
@app.get("/api/v1/documents/{cid}/metadata")
async def get_document_metadata(
cid: str,
_auth: dict = Depends(verify_token)
):
"""
Ruft nur die Metadaten eines Dokuments ab.
- **cid**: Content Identifier (IPFS Hash)
"""
content = await ipfs_cat(cid)
try:
package = json.loads(content)
return {
"cid": cid,
"metadata": package.get("metadata", {}),
"filename": package.get("filename"),
"size": len(bytes.fromhex(package.get("content_base64", "")))
}
except json.JSONDecodeError:
return {
"cid": cid,
"metadata": {},
"raw_size": len(content)
}
@app.get("/api/v1/documents", response_model=DocumentList)
async def list_documents(
_auth: dict = Depends(verify_token)
):
"""
Listet alle gespeicherten Dokumente auf.
"""
cids = await ipfs_pin_ls()
documents = []
for cid in cids[:100]: # Limit auf 100 für Performance
try:
content = await ipfs_cat(cid)
package = json.loads(content)
documents.append({
"cid": cid,
"metadata": package.get("metadata", {}),
"filename": package.get("filename")
})
except Exception:
# Überspringe nicht-DSMS Objekte
continue
return DocumentList(
documents=documents,
total=len(documents)
)
@app.delete("/api/v1/documents/{cid}")
async def unpin_document(
cid: str,
_auth: dict = Depends(verify_token)
):
"""
Entfernt ein Dokument aus dem lokalen Pin-Set.
Das Dokument bleibt im Netzwerk, wird aber bei GC entfernt.
- **cid**: Content Identifier (IPFS Hash)
"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{IPFS_API_URL}/api/v0/pin/rm",
params={"arg": cid}
)
if response.status_code != 200:
raise HTTPException(
status_code=404,
detail=f"Konnte Pin nicht entfernen: {cid}"
)
return {
"status": "unpinned",
"cid": cid,
"message": "Dokument wird bei nächster Garbage Collection entfernt"
}
@app.post("/api/v1/legal-documents/archive")
async def archive_legal_document(
document_id: str,
version: str,
content: str,
language: str = "de",
_auth: dict = Depends(verify_token)
):
"""
Archiviert eine rechtliche Dokumentversion dauerhaft.
Speziell für AGB, Datenschutzerklärung, etc.
- **document_id**: ID des Legal Documents
- **version**: Versionsnummer
- **content**: HTML/Markdown Inhalt
- **language**: Sprache
"""
# Checksum berechnen
content_bytes = content.encode('utf-8')
checksum = hashlib.sha256(content_bytes).hexdigest()
# Metadaten
metadata = {
"document_type": "legal_document",
"document_id": document_id,
"version": version,
"language": language,
"created_at": datetime.utcnow().isoformat(),
"checksum": checksum,
"content_type": "text/html"
}
# Paket erstellen
package = {
"metadata": metadata,
"content": content,
"archived_at": datetime.utcnow().isoformat()
}
package_bytes = json.dumps(package, ensure_ascii=False).encode('utf-8')
# Zu IPFS hinzufügen
result = await ipfs_add(package_bytes)
cid = result.get("Hash")
return {
"cid": cid,
"document_id": document_id,
"version": version,
"checksum": checksum,
"archived_at": datetime.utcnow().isoformat(),
"verification_url": f"{IPFS_GATEWAY_URL}/ipfs/{cid}"
}
@app.get("/api/v1/verify/{cid}")
async def verify_document(cid: str):
"""
Verifiziert die Integrität eines Dokuments.
Öffentlich zugänglich für Audit-Zwecke.
- **cid**: Content Identifier (IPFS Hash)
"""
try:
content = await ipfs_cat(cid)
package = json.loads(content)
# Checksum verifizieren
stored_checksum = package.get("metadata", {}).get("checksum")
if "content_base64" in package:
original_content = bytes.fromhex(package["content_base64"])
calculated_checksum = hashlib.sha256(original_content).hexdigest()
elif "content" in package:
calculated_checksum = hashlib.sha256(
package["content"].encode('utf-8')
).hexdigest()
else:
calculated_checksum = None
integrity_valid = (
stored_checksum == calculated_checksum
if stored_checksum and calculated_checksum
else None
)
return {
"cid": cid,
"exists": True,
"integrity_valid": integrity_valid,
"metadata": package.get("metadata", {}),
"stored_checksum": stored_checksum,
"calculated_checksum": calculated_checksum,
"verified_at": datetime.utcnow().isoformat()
}
except Exception as e:
return {
"cid": cid,
"exists": False,
"error": str(e),
"verified_at": datetime.utcnow().isoformat()
}
@app.get("/api/v1/node/info")
async def get_node_info():
"""
Gibt Informationen über den DSMS Node zurück.
"""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
# Node ID
id_response = await client.post(f"{IPFS_API_URL}/api/v0/id")
node_info = id_response.json() if id_response.status_code == 200 else {}
# Repo Stats
stat_response = await client.post(f"{IPFS_API_URL}/api/v0/repo/stat")
repo_stats = stat_response.json() if stat_response.status_code == 200 else {}
return {
"node_id": node_info.get("ID"),
"protocol_version": node_info.get("ProtocolVersion"),
"agent_version": node_info.get("AgentVersion"),
"repo_size": repo_stats.get("RepoSize"),
"storage_max": repo_stats.get("StorageMax"),
"num_objects": repo_stats.get("NumObjects"),
"addresses": node_info.get("Addresses", [])[:5] # Erste 5
}
except Exception as e:
return {"error": str(e)}
# Router registration
app.include_router(node_router)
app.include_router(documents_router)
if __name__ == "__main__":

32
dsms-gateway/models.py Normal file
View File

@@ -0,0 +1,32 @@
"""
DSMS Gateway — Pydantic request/response models.
"""
from typing import Optional
from pydantic import BaseModel
class DocumentMetadata(BaseModel):
"""Metadaten für gespeicherte Dokumente"""
document_type: str # 'legal_document', 'consent_record', 'audit_log'
document_id: Optional[str] = None
version: Optional[str] = None
language: Optional[str] = "de"
created_at: Optional[str] = None
checksum: Optional[str] = None
encrypted: bool = False
class StoredDocument(BaseModel):
"""Antwort nach erfolgreichem Speichern"""
cid: str # Content Identifier (IPFS Hash)
size: int
metadata: DocumentMetadata
gateway_url: str
timestamp: str
class DocumentList(BaseModel):
"""Liste der gespeicherten Dokumente"""
documents: list
total: int

View File

View File

@@ -0,0 +1,256 @@
"""
Documents router — handles /api/v1/documents and /api/v1/legal-documents endpoints.
"""
import hashlib
import json
import io
from datetime import datetime
from typing import Optional
import httpx
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.responses import StreamingResponse
from models import DocumentList, DocumentMetadata, StoredDocument
from dependencies import verify_token, ipfs_add, ipfs_cat, ipfs_pin_ls
from config import IPFS_API_URL, IPFS_GATEWAY_URL
router = APIRouter()
@router.post("/api/v1/documents", response_model=StoredDocument)
async def store_document(
file: UploadFile = File(...),
document_type: str = "legal_document",
document_id: Optional[str] = None,
version: Optional[str] = None,
language: str = "de",
_auth: dict = Depends(verify_token)
):
"""
Speichert ein Dokument im DSMS.
- **file**: Das zu speichernde Dokument
- **document_type**: Typ des Dokuments (legal_document, consent_record, audit_log)
- **document_id**: Optionale ID des Dokuments
- **version**: Optionale Versionsnummer
- **language**: Sprache (default: de)
"""
content = await file.read()
# Checksum berechnen
checksum = hashlib.sha256(content).hexdigest()
# Metadaten erstellen
metadata = DocumentMetadata(
document_type=document_type,
document_id=document_id,
version=version,
language=language,
created_at=datetime.utcnow().isoformat(),
checksum=checksum,
encrypted=False
)
# Dokument mit Metadaten als JSON verpacken
package = {
"metadata": metadata.model_dump(),
"content_base64": content.hex(), # Hex-encodiert für JSON
"filename": file.filename
}
package_bytes = json.dumps(package).encode()
# Zu IPFS hinzufügen
result = await ipfs_add(package_bytes)
cid = result.get("Hash")
size = int(result.get("Size", 0))
return StoredDocument(
cid=cid,
size=size,
metadata=metadata,
gateway_url=f"{IPFS_GATEWAY_URL}/ipfs/{cid}",
timestamp=datetime.utcnow().isoformat()
)
@router.get("/api/v1/documents/{cid}")
async def get_document(
cid: str,
_auth: dict = Depends(verify_token)
):
"""
Ruft ein Dokument aus dem DSMS ab.
- **cid**: Content Identifier (IPFS Hash)
"""
content = await ipfs_cat(cid)
try:
package = json.loads(content)
metadata = package.get("metadata", {})
original_content = bytes.fromhex(package.get("content_base64", ""))
filename = package.get("filename", "document")
return StreamingResponse(
io.BytesIO(original_content),
media_type="application/octet-stream",
headers={
"Content-Disposition": f'attachment; filename="{filename}"',
"X-DSMS-Document-Type": metadata.get("document_type", "unknown"),
"X-DSMS-Checksum": metadata.get("checksum", ""),
"X-DSMS-Created-At": metadata.get("created_at", "")
}
)
except json.JSONDecodeError:
# Wenn es kein DSMS-Paket ist, gib rohen Inhalt zurück
return StreamingResponse(
io.BytesIO(content),
media_type="application/octet-stream"
)
@router.get("/api/v1/documents/{cid}/metadata")
async def get_document_metadata(
cid: str,
_auth: dict = Depends(verify_token)
):
"""
Ruft nur die Metadaten eines Dokuments ab.
- **cid**: Content Identifier (IPFS Hash)
"""
content = await ipfs_cat(cid)
try:
package = json.loads(content)
return {
"cid": cid,
"metadata": package.get("metadata", {}),
"filename": package.get("filename"),
"size": len(bytes.fromhex(package.get("content_base64", "")))
}
except json.JSONDecodeError:
return {
"cid": cid,
"metadata": {},
"raw_size": len(content)
}
@router.get("/api/v1/documents", response_model=DocumentList)
async def list_documents(
_auth: dict = Depends(verify_token)
):
"""
Listet alle gespeicherten Dokumente auf.
"""
cids = await ipfs_pin_ls()
documents = []
for cid in cids[:100]: # Limit auf 100 für Performance
try:
content = await ipfs_cat(cid)
package = json.loads(content)
documents.append({
"cid": cid,
"metadata": package.get("metadata", {}),
"filename": package.get("filename")
})
except Exception:
# Überspringe nicht-DSMS Objekte
continue
return DocumentList(
documents=documents,
total=len(documents)
)
@router.delete("/api/v1/documents/{cid}")
async def unpin_document(
cid: str,
_auth: dict = Depends(verify_token)
):
"""
Entfernt ein Dokument aus dem lokalen Pin-Set.
Das Dokument bleibt im Netzwerk, wird aber bei GC entfernt.
- **cid**: Content Identifier (IPFS Hash)
"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{IPFS_API_URL}/api/v0/pin/rm",
params={"arg": cid}
)
if response.status_code != 200:
raise HTTPException(
status_code=404,
detail=f"Konnte Pin nicht entfernen: {cid}"
)
return {
"status": "unpinned",
"cid": cid,
"message": "Dokument wird bei nächster Garbage Collection entfernt"
}
@router.post("/api/v1/legal-documents/archive")
async def archive_legal_document(
document_id: str,
version: str,
content: str,
language: str = "de",
_auth: dict = Depends(verify_token)
):
"""
Archiviert eine rechtliche Dokumentversion dauerhaft.
Speziell für AGB, Datenschutzerklärung, etc.
- **document_id**: ID des Legal Documents
- **version**: Versionsnummer
- **content**: HTML/Markdown Inhalt
- **language**: Sprache
"""
# Checksum berechnen
content_bytes = content.encode('utf-8')
checksum = hashlib.sha256(content_bytes).hexdigest()
# Metadaten
metadata = {
"document_type": "legal_document",
"document_id": document_id,
"version": version,
"language": language,
"created_at": datetime.utcnow().isoformat(),
"checksum": checksum,
"content_type": "text/html"
}
# Paket erstellen
package = {
"metadata": metadata,
"content": content,
"archived_at": datetime.utcnow().isoformat()
}
package_bytes = json.dumps(package, ensure_ascii=False).encode('utf-8')
# Zu IPFS hinzufügen
result = await ipfs_add(package_bytes)
cid = result.get("Hash")
return {
"cid": cid,
"document_id": document_id,
"version": version,
"checksum": checksum,
"archived_at": datetime.utcnow().isoformat(),
"verification_url": f"{IPFS_GATEWAY_URL}/ipfs/{cid}"
}

View File

@@ -0,0 +1,109 @@
"""
Node router — handles /health, /api/v1/verify/{cid}, and /api/v1/node/info endpoints.
"""
import hashlib
import json
from datetime import datetime
import httpx
from fastapi import APIRouter
from dependencies import ipfs_cat
from config import IPFS_API_URL
router = APIRouter()
@router.get("/health")
async def health_check():
"""Health Check für DSMS Gateway"""
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.post(f"{IPFS_API_URL}/api/v0/id")
ipfs_status = response.status_code == 200
except Exception:
ipfs_status = False
return {
"status": "healthy" if ipfs_status else "degraded",
"ipfs_connected": ipfs_status,
"timestamp": datetime.utcnow().isoformat()
}
@router.get("/api/v1/verify/{cid}")
async def verify_document(cid: str):
"""
Verifiziert die Integrität eines Dokuments.
Öffentlich zugänglich für Audit-Zwecke.
- **cid**: Content Identifier (IPFS Hash)
"""
try:
content = await ipfs_cat(cid)
package = json.loads(content)
# Checksum verifizieren
stored_checksum = package.get("metadata", {}).get("checksum")
if "content_base64" in package:
original_content = bytes.fromhex(package["content_base64"])
calculated_checksum = hashlib.sha256(original_content).hexdigest()
elif "content" in package:
calculated_checksum = hashlib.sha256(
package["content"].encode('utf-8')
).hexdigest()
else:
calculated_checksum = None
integrity_valid = (
stored_checksum == calculated_checksum
if stored_checksum and calculated_checksum
else None
)
return {
"cid": cid,
"exists": True,
"integrity_valid": integrity_valid,
"metadata": package.get("metadata", {}),
"stored_checksum": stored_checksum,
"calculated_checksum": calculated_checksum,
"verified_at": datetime.utcnow().isoformat()
}
except Exception as e:
return {
"cid": cid,
"exists": False,
"error": str(e),
"verified_at": datetime.utcnow().isoformat()
}
@router.get("/api/v1/node/info")
async def get_node_info():
"""
Gibt Informationen über den DSMS Node zurück.
"""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
# Node ID
id_response = await client.post(f"{IPFS_API_URL}/api/v0/id")
node_info = id_response.json() if id_response.status_code == 200 else {}
# Repo Stats
stat_response = await client.post(f"{IPFS_API_URL}/api/v0/repo/stat")
repo_stats = stat_response.json() if stat_response.status_code == 200 else {}
return {
"node_id": node_info.get("ID"),
"protocol_version": node_info.get("ProtocolVersion"),
"agent_version": node_info.get("AgentVersion"),
"repo_size": repo_stats.get("RepoSize"),
"storage_max": repo_stats.get("StorageMax"),
"num_objects": repo_stats.get("NumObjects"),
"addresses": node_info.get("Addresses", [])[:5] # Erste 5
}
except Exception as e:
return {"error": str(e)}

View File

@@ -56,7 +56,7 @@ class TestHealthCheck:
def test_health_check_ipfs_connected(self):
"""Test: Health Check wenn IPFS verbunden ist"""
with patch("main.httpx.AsyncClient") as mock_client:
with patch("routers.node.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
@@ -71,7 +71,7 @@ class TestHealthCheck:
def test_health_check_ipfs_disconnected(self):
"""Test: Health Check wenn IPFS nicht erreichbar"""
with patch("main.httpx.AsyncClient") as mock_client:
with patch("routers.node.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
@@ -104,7 +104,7 @@ class TestAuthorization:
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:
with patch("routers.documents.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls:
mock_pin_ls.return_value = []
response = client.get(
@@ -122,7 +122,7 @@ class TestDocumentStorage:
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:
with patch("routers.documents.ipfs_add", new_callable=AsyncMock) as mock_add:
mock_add.return_value = mock_ipfs_response
test_content = b"Test document content"
@@ -148,7 +148,7 @@ class TestDocumentStorage:
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:
with patch("routers.documents.ipfs_add", new_callable=AsyncMock) as mock_add:
mock_add.return_value = mock_ipfs_response
test_content = b"Test content for checksum"
@@ -191,7 +191,7 @@ class TestDocumentRetrieval:
"filename": "test.txt"
}
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
with patch("routers.documents.ipfs_cat", new_callable=AsyncMock) as mock_cat:
mock_cat.return_value = json.dumps(package).encode()
response = client.get(
@@ -204,7 +204,7 @@ class TestDocumentRetrieval:
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:
with patch("routers.documents.ipfs_cat", new_callable=AsyncMock) as mock_cat:
from fastapi import HTTPException
mock_cat.side_effect = HTTPException(status_code=404, detail="Not found")
@@ -228,7 +228,7 @@ class TestDocumentRetrieval:
"filename": "test.txt"
}
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
with patch("routers.documents.ipfs_cat", new_callable=AsyncMock) as mock_cat:
mock_cat.return_value = json.dumps(package).encode()
response = client.get(
@@ -249,7 +249,7 @@ class TestDocumentList:
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:
with patch("routers.documents.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls:
mock_pin_ls.return_value = []
response = client.get(
@@ -270,10 +270,10 @@ class TestDocumentList:
"filename": "test.txt"
}
with patch("main.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls:
with patch("routers.documents.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:
with patch("routers.documents.ipfs_cat", new_callable=AsyncMock) as mock_cat:
mock_cat.return_value = json.dumps(package).encode()
response = client.get(
@@ -293,7 +293,7 @@ class TestDocumentDeletion:
def test_unpin_document_success(self, valid_auth_header):
"""Test: Dokument erfolgreich unpinnen"""
with patch("main.httpx.AsyncClient") as mock_client:
with patch("routers.documents.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
@@ -310,7 +310,7 @@ class TestDocumentDeletion:
def test_unpin_document_not_found(self, valid_auth_header):
"""Test: Nicht existierendes Dokument unpinnen"""
with patch("main.httpx.AsyncClient") as mock_client:
with patch("routers.documents.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
@@ -330,7 +330,7 @@ class TestLegalDocumentArchive:
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:
with patch("routers.documents.ipfs_add", new_callable=AsyncMock) as mock_add:
mock_add.return_value = mock_ipfs_response
response = client.post(
@@ -357,7 +357,7 @@ class TestLegalDocumentArchive:
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:
with patch("routers.documents.ipfs_add", new_callable=AsyncMock) as mock_add:
mock_add.return_value = mock_ipfs_response
response = client.post(
@@ -393,7 +393,7 @@ class TestDocumentVerification:
"content": content
}
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
with patch("routers.node.ipfs_cat", new_callable=AsyncMock) as mock_cat:
mock_cat.return_value = json.dumps(package).encode()
response = client.get("/api/v1/verify/QmTestCid123")
@@ -415,7 +415,7 @@ class TestDocumentVerification:
"content": "Actual content"
}
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
with patch("routers.node.ipfs_cat", new_callable=AsyncMock) as mock_cat:
mock_cat.return_value = json.dumps(package).encode()
response = client.get("/api/v1/verify/QmTestCid123")
@@ -427,7 +427,7 @@ class TestDocumentVerification:
def test_verify_document_not_found(self):
"""Test: Nicht existierendes Dokument verifizieren"""
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
with patch("routers.node.ipfs_cat", new_callable=AsyncMock) as mock_cat:
mock_cat.side_effect = Exception("Not found")
response = client.get("/api/v1/verify/QmNonExistent")
@@ -444,7 +444,7 @@ class TestDocumentVerification:
"content": "test"
}
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
with patch("routers.node.ipfs_cat", new_callable=AsyncMock) as mock_cat:
mock_cat.return_value = json.dumps(package).encode()
# Kein Authorization Header!
@@ -472,7 +472,7 @@ class TestNodeInfo:
"NumObjects": 42
}
with patch("main.httpx.AsyncClient") as mock_client:
with patch("routers.node.httpx.AsyncClient") as mock_client:
mock_instance = AsyncMock()
async def mock_post(url, **kwargs):
@@ -497,7 +497,7 @@ class TestNodeInfo:
def test_get_node_info_public_access(self):
"""Test: Node-Info ist öffentlich zugänglich"""
with patch("main.httpx.AsyncClient") as mock_client:
with patch("routers.node.httpx.AsyncClient") as mock_client:
mock_instance = AsyncMock()
mock_instance.post.return_value = MagicMock(
status_code=200,