Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 33s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s
Jeder Tenant kann jetzt mehrere Compliance-Projekte anlegen (z.B. verschiedene Produkte, Tochterunternehmen). CompanyProfile ist pro Projekt kopierbar und danach unabhaengig editierbar. Multi-Tab-Support via separater BroadcastChannel und localStorage Keys pro Projekt. - Migration 039: compliance_projects Tabelle, sdk_states.project_id - Backend: FastAPI CRUD-Routes fuer Projekte mit Tenant-Isolation - Frontend: ProjectSelector UI, SDKProvider mit projectId, URL ?project= - State API: UPSERT auf (tenant_id, project_id) mit Abwaertskompatibilitaet - Tests: pytest fuer Model-Validierung, Row-Konvertierung, Tenant-Isolation - Docs: MKDocs Seite, CLAUDE.md, Backend README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
190 lines
6.3 KiB
Python
190 lines
6.3 KiB
Python
"""Tests for Compliance Project routes (project_routes.py)."""
|
|
|
|
import json
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch, PropertyMock
|
|
from datetime import datetime, timezone
|
|
|
|
from compliance.api.project_routes import (
|
|
CreateProjectRequest,
|
|
UpdateProjectRequest,
|
|
_row_to_response,
|
|
)
|
|
|
|
|
|
class TestCreateProjectRequest:
|
|
"""Tests for request model validation."""
|
|
|
|
def test_default_values(self):
|
|
req = CreateProjectRequest(name="Test Project")
|
|
assert req.name == "Test Project"
|
|
assert req.description == ""
|
|
assert req.customer_type == "new"
|
|
assert req.copy_from_project_id is None
|
|
|
|
def test_full_values(self):
|
|
req = CreateProjectRequest(
|
|
name="KI-Produkt X",
|
|
description="DSGVO-Compliance fuer Produkt X",
|
|
customer_type="existing",
|
|
copy_from_project_id="uuid-123",
|
|
)
|
|
assert req.name == "KI-Produkt X"
|
|
assert req.description == "DSGVO-Compliance fuer Produkt X"
|
|
assert req.customer_type == "existing"
|
|
assert req.copy_from_project_id == "uuid-123"
|
|
|
|
def test_name_required(self):
|
|
with pytest.raises(Exception):
|
|
CreateProjectRequest()
|
|
|
|
def test_serialization(self):
|
|
req = CreateProjectRequest(name="Test")
|
|
data = req.model_dump()
|
|
assert data["name"] == "Test"
|
|
assert data["customer_type"] == "new"
|
|
|
|
|
|
class TestUpdateProjectRequest:
|
|
"""Tests for update model."""
|
|
|
|
def test_empty_update(self):
|
|
req = UpdateProjectRequest()
|
|
assert req.name is None
|
|
assert req.description is None
|
|
|
|
def test_partial_update(self):
|
|
req = UpdateProjectRequest(name="New Name")
|
|
assert req.name == "New Name"
|
|
assert req.description is None
|
|
|
|
|
|
class TestRowToResponse:
|
|
"""Tests for DB row to response conversion."""
|
|
|
|
def _make_row(self, **overrides):
|
|
now = datetime.now(timezone.utc)
|
|
defaults = {
|
|
"id": "uuid-project-1",
|
|
"tenant_id": "uuid-tenant-1",
|
|
"name": "Test Project",
|
|
"description": "A test project",
|
|
"customer_type": "new",
|
|
"status": "active",
|
|
"project_version": 1,
|
|
"completion_percentage": 0,
|
|
"created_at": now,
|
|
"updated_at": now,
|
|
}
|
|
defaults.update(overrides)
|
|
mock = MagicMock()
|
|
for key, value in defaults.items():
|
|
setattr(mock, key, value)
|
|
return mock
|
|
|
|
def test_basic_conversion(self):
|
|
row = self._make_row()
|
|
result = _row_to_response(row)
|
|
assert result["id"] == "uuid-project-1"
|
|
assert result["tenant_id"] == "uuid-tenant-1"
|
|
assert result["name"] == "Test Project"
|
|
assert result["description"] == "A test project"
|
|
assert result["customer_type"] == "new"
|
|
assert result["status"] == "active"
|
|
assert result["project_version"] == 1
|
|
assert result["completion_percentage"] == 0
|
|
|
|
def test_null_description(self):
|
|
row = self._make_row(description=None)
|
|
result = _row_to_response(row)
|
|
assert result["description"] == ""
|
|
|
|
def test_null_customer_type(self):
|
|
row = self._make_row(customer_type=None)
|
|
result = _row_to_response(row)
|
|
assert result["customer_type"] == "new"
|
|
|
|
def test_null_status(self):
|
|
row = self._make_row(status=None)
|
|
result = _row_to_response(row)
|
|
assert result["status"] == "active"
|
|
|
|
def test_created_at_iso(self):
|
|
dt = datetime(2026, 3, 9, 12, 0, 0, tzinfo=timezone.utc)
|
|
row = self._make_row(created_at=dt)
|
|
result = _row_to_response(row)
|
|
assert "2026-03-09" in result["created_at"]
|
|
|
|
def test_archived_project(self):
|
|
row = self._make_row(status="archived", completion_percentage=75)
|
|
result = _row_to_response(row)
|
|
assert result["status"] == "archived"
|
|
assert result["completion_percentage"] == 75
|
|
|
|
|
|
class TestCreateProjectCopiesProfile:
|
|
"""Tests that creating a project with copy_from_project_id works."""
|
|
|
|
def test_copy_request_model(self):
|
|
req = CreateProjectRequest(
|
|
name="Tochter GmbH",
|
|
customer_type="existing",
|
|
copy_from_project_id="source-uuid-123",
|
|
)
|
|
assert req.copy_from_project_id == "source-uuid-123"
|
|
|
|
def test_no_copy_request_model(self):
|
|
req = CreateProjectRequest(name="Brand New")
|
|
assert req.copy_from_project_id is None
|
|
|
|
|
|
class TestTenantIsolation:
|
|
"""Tests verifying tenant isolation is enforced in query patterns."""
|
|
|
|
def test_list_query_includes_tenant_filter(self):
|
|
"""Verify that our SQL queries always filter by tenant_id."""
|
|
import inspect
|
|
from compliance.api.project_routes import list_projects
|
|
source = inspect.getsource(list_projects)
|
|
assert "tenant_id" in source
|
|
assert "WHERE" in source
|
|
|
|
def test_get_query_includes_tenant_filter(self):
|
|
import inspect
|
|
from compliance.api.project_routes import get_project
|
|
source = inspect.getsource(get_project)
|
|
assert "tenant_id" in source
|
|
assert "project_id" in source
|
|
|
|
def test_archive_query_includes_tenant_filter(self):
|
|
import inspect
|
|
from compliance.api.project_routes import archive_project
|
|
source = inspect.getsource(archive_project)
|
|
assert "tenant_id" in source
|
|
|
|
def test_update_query_includes_tenant_filter(self):
|
|
import inspect
|
|
from compliance.api.project_routes import update_project
|
|
source = inspect.getsource(update_project)
|
|
assert "tenant_id" in source
|
|
|
|
|
|
class TestStateIsolation:
|
|
"""Tests verifying state isolation between projects."""
|
|
|
|
def test_create_project_creates_sdk_state(self):
|
|
"""Verify create_project function inserts into sdk_states."""
|
|
import inspect
|
|
from compliance.api.project_routes import create_project
|
|
source = inspect.getsource(create_project)
|
|
assert "sdk_states" in source
|
|
assert "project_id" in source
|
|
|
|
def test_create_project_copies_company_profile(self):
|
|
"""Verify create_project copies companyProfile when copy_from specified."""
|
|
import inspect
|
|
from compliance.api.project_routes import create_project
|
|
source = inspect.getsource(create_project)
|
|
assert "copy_from_project_id" in source
|
|
assert "companyProfile" in source
|