Files
govoplan-core/tests/test_organizations_api.py
Albrecht Degering 8dd5123aab
Some checks failed
Dependency Audit / dependency-audit (push) Failing after 1m51s
Module Matrix / module-matrix (push) Failing after 13s
Cover identity and organization API cleanup
2026-07-10 18:57:40 +02:00

169 lines
8.2 KiB
Python

from __future__ import annotations
import shutil
import tempfile
import unittest
from pathlib import Path
from fastapi import FastAPI
from fastapi.testclient import TestClient
from govoplan_access.backend.db.models import Account, User
from govoplan_core.auth import ApiPrincipal, get_api_principal
from govoplan_core.core.access import PrincipalRef
from govoplan_core.core.configuration_control import configuration_control_snapshot, create_configuration_change_request
from govoplan_core.db.base import Base
from govoplan_organizations.backend.api.v1.routes import router as organizations_router
from tests.db_isolation import temporary_database
class OrganizationsApiTests(unittest.TestCase):
def _principal(self, scopes: set[str]) -> ApiPrincipal:
account = Account(
id="account-organizations-api",
email="org-admin@example.test",
normalized_email="org-admin@example.test",
display_name="Organization Admin",
)
user = User(
id="user-organizations-api",
tenant_id="tenant-organizations-api",
account_id=account.id,
email=account.email,
display_name=account.display_name,
settings={},
mail_profile_policy={},
)
return ApiPrincipal(
principal=PrincipalRef(
account_id=account.id,
membership_id=user.id,
tenant_id=user.tenant_id,
scopes=frozenset(scopes),
),
account=account,
user=user,
)
def _app(self, scopes: set[str]) -> FastAPI:
app = FastAPI()
app.include_router(organizations_router, prefix="/api/v1")
app.dependency_overrides[get_api_principal] = lambda: self._principal(scopes)
return app
def test_organization_settings_defaults_and_updates_are_tenant_scoped(self) -> None:
root = Path(tempfile.mkdtemp(prefix="govoplan-organizations-api-"))
try:
with temporary_database(f"sqlite:///{root / 'organizations.db'}") as database:
Base.metadata.create_all(bind=database.engine)
app = self._app({"organizations:settings:read", "organizations:settings:write"})
with TestClient(app) as client:
default_response = client.get("/api/v1/organizations/settings")
self.assertEqual(200, default_response.status_code, default_response.text)
self.assertIsNone(default_response.json()["id"])
self.assertTrue(default_response.json()["allow_tenant_model_customization"])
self.assertEqual("standard", default_response.json()["audit_detail_level"])
update_response = client.patch(
"/api/v1/organizations/settings",
json={
"allow_tenant_model_customization": False,
"require_model_change_requests": True,
"audit_detail_level": "full",
"change_retention_days": 365,
},
)
self.assertEqual(200, update_response.status_code, update_response.text)
payload = update_response.json()
self.assertFalse(payload["allow_tenant_model_customization"])
self.assertTrue(payload["require_model_change_requests"])
self.assertEqual("full", payload["audit_detail_level"])
self.assertEqual(365, payload["change_retention_days"])
reload_response = client.get("/api/v1/organizations/settings")
self.assertEqual(200, reload_response.status_code, reload_response.text)
self.assertEqual(payload["id"], reload_response.json()["id"])
self.assertFalse(reload_response.json()["allow_tenant_model_customization"])
finally:
shutil.rmtree(root, ignore_errors=True)
def test_unit_parent_update_rejects_cycles(self) -> None:
root = Path(tempfile.mkdtemp(prefix="govoplan-organizations-cycle-"))
try:
with temporary_database(f"sqlite:///{root / 'organizations.db'}") as database:
Base.metadata.create_all(bind=database.engine)
app = self._app({"organizations:unit:write"})
with TestClient(app) as client:
root_response = client.post("/api/v1/organizations/units", json={"name": "Registry"})
self.assertEqual(201, root_response.status_code, root_response.text)
root_id = root_response.json()["id"]
child_response = client.post(
"/api/v1/organizations/units",
json={"name": "Registry Frontdesk", "parent_id": root_id},
)
self.assertEqual(201, child_response.status_code, child_response.text)
child_id = child_response.json()["id"]
cycle_response = client.patch(
f"/api/v1/organizations/units/{root_id}",
json={"parent_id": child_id},
)
self.assertEqual(422, cycle_response.status_code, cycle_response.text)
self.assertIn("cycle", cycle_response.text)
finally:
shutil.rmtree(root, ignore_errors=True)
def test_required_change_request_policy_blocks_and_allows_unit_writes(self) -> None:
root = Path(tempfile.mkdtemp(prefix="govoplan-organizations-change-request-"))
try:
with temporary_database(f"sqlite:///{root / 'organizations.db'}") as database:
Base.metadata.create_all(bind=database.engine)
app = self._app({"organizations:settings:read", "organizations:settings:write", "organizations:unit:write"})
with TestClient(app) as client:
settings_response = client.patch(
"/api/v1/organizations/settings",
json={"require_model_change_requests": True},
)
self.assertEqual(200, settings_response.status_code, settings_response.text)
blocked_response = client.post("/api/v1/organizations/units", json={"name": "Registry"})
self.assertEqual(409, blocked_response.status_code, blocked_response.text)
self.assertEqual("configuration_request_required", blocked_response.json()["detail"]["code"])
value = {"resource_type": "unit", "operation": "created", "payload": {"name": "Registry"}}
with database.session() as session:
request = create_configuration_change_request(
session,
key="organizations.model",
value=value,
actor_user_id="user-organizations-api",
actor_scopes=("organizations:unit:write",),
dry_run=True,
target={"tenant_id": "tenant-organizations-api", "resource_type": "unit", "operation": "created"},
reason="Test organization unit creation",
)
session.commit()
allowed_response = client.post(
"/api/v1/organizations/units",
json={"name": "Registry", "change_request_id": request["id"]},
)
self.assertEqual(201, allowed_response.status_code, allowed_response.text)
self.assertEqual("Registry", allowed_response.json()["name"])
with database.session() as session:
snapshot = configuration_control_snapshot(session)
stored_request = next(item for item in snapshot["requests"] if item["id"] == request["id"])
self.assertEqual("applied", stored_request["status"])
self.assertTrue(any(item["approval_request_id"] == request["id"] for item in snapshot["history"]))
finally:
shutil.rmtree(root, ignore_errors=True)
if __name__ == "__main__":
unittest.main()