feat: expose bounded organization hierarchy routing
This commit is contained in:
421
tests/test_hierarchy_directory.py
Normal file
421
tests/test_hierarchy_directory.py
Normal file
@@ -0,0 +1,421 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from govoplan_core.core.events import EventBus, event_bus_context
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_organizations.backend.api.v1.routes import _commit
|
||||
from govoplan_organizations.backend.db.models import (
|
||||
OrganizationFunction,
|
||||
OrganizationFunctionType,
|
||||
OrganizationRelation,
|
||||
OrganizationRelationType,
|
||||
OrganizationStructure,
|
||||
OrganizationUnit,
|
||||
OrganizationUnitType,
|
||||
)
|
||||
from govoplan_organizations.backend.directory import (
|
||||
SqlOrganizationDirectory,
|
||||
)
|
||||
|
||||
|
||||
class OrganizationHierarchyDirectoryTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(
|
||||
self.engine,
|
||||
tables=[
|
||||
OrganizationUnitType.__table__,
|
||||
OrganizationStructure.__table__,
|
||||
OrganizationRelationType.__table__,
|
||||
OrganizationUnit.__table__,
|
||||
OrganizationRelation.__table__,
|
||||
OrganizationFunctionType.__table__,
|
||||
OrganizationFunction.__table__,
|
||||
],
|
||||
)
|
||||
self.Session = sessionmaker(
|
||||
bind=self.engine,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
self.session: Session = self.Session()
|
||||
self._seed()
|
||||
self.directory = SqlOrganizationDirectory(
|
||||
session_factory=self.Session,
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.session.close()
|
||||
Base.metadata.drop_all(
|
||||
self.engine,
|
||||
tables=[
|
||||
OrganizationFunction.__table__,
|
||||
OrganizationFunctionType.__table__,
|
||||
OrganizationRelation.__table__,
|
||||
OrganizationUnit.__table__,
|
||||
OrganizationRelationType.__table__,
|
||||
OrganizationStructure.__table__,
|
||||
OrganizationUnitType.__table__,
|
||||
],
|
||||
)
|
||||
self.engine.dispose()
|
||||
|
||||
def _seed(self) -> None:
|
||||
self.unit_type = OrganizationUnitType(
|
||||
id="type-department",
|
||||
tenant_id="tenant-1",
|
||||
slug="department",
|
||||
name="Department",
|
||||
is_active=True,
|
||||
settings={},
|
||||
)
|
||||
self.function_type = OrganizationFunctionType(
|
||||
id="type-intake",
|
||||
tenant_id="tenant-1",
|
||||
slug="intake",
|
||||
name="Intake",
|
||||
organization_unit_type_id=self.unit_type.id,
|
||||
is_active=True,
|
||||
settings={},
|
||||
)
|
||||
self.employer = OrganizationStructure(
|
||||
id="structure-employer",
|
||||
tenant_id="tenant-1",
|
||||
slug="employer",
|
||||
name="Employer hierarchy",
|
||||
structure_kind="hierarchy",
|
||||
is_active=True,
|
||||
settings={},
|
||||
)
|
||||
self.project = OrganizationStructure(
|
||||
id="structure-project",
|
||||
tenant_id="tenant-1",
|
||||
slug="project",
|
||||
name="Project hierarchy",
|
||||
structure_kind="hierarchy",
|
||||
is_active=True,
|
||||
settings={},
|
||||
)
|
||||
self.employer_parent = OrganizationRelationType(
|
||||
id="relation-type-employer",
|
||||
tenant_id="tenant-1",
|
||||
structure_id=self.employer.id,
|
||||
slug="contains",
|
||||
name="Contains",
|
||||
is_hierarchical=True,
|
||||
allow_cycles=False,
|
||||
is_active=True,
|
||||
settings={},
|
||||
)
|
||||
self.project_parent = OrganizationRelationType(
|
||||
id="relation-type-project",
|
||||
tenant_id="tenant-1",
|
||||
structure_id=self.project.id,
|
||||
slug="project-contains",
|
||||
name="Contains",
|
||||
is_hierarchical=True,
|
||||
allow_cycles=False,
|
||||
is_active=True,
|
||||
settings={},
|
||||
)
|
||||
self.root = self._unit("unit-root", "Root")
|
||||
self.child = self._unit("unit-child", "Child")
|
||||
self.grandchild = self._unit("unit-grandchild", "Grandchild")
|
||||
self.project_root = self._unit("unit-project", "Project root")
|
||||
self.other_tenant = self._unit(
|
||||
"unit-other-tenant",
|
||||
"Other tenant",
|
||||
tenant_id="tenant-2",
|
||||
)
|
||||
self.employer_child = OrganizationRelation(
|
||||
id="edge-employer-child",
|
||||
tenant_id="tenant-1",
|
||||
structure_id=self.employer.id,
|
||||
relation_type_id=self.employer_parent.id,
|
||||
source_unit_id=self.root.id,
|
||||
target_unit_id=self.child.id,
|
||||
is_active=True,
|
||||
settings={},
|
||||
)
|
||||
self.employer_grandchild = OrganizationRelation(
|
||||
id="edge-employer-grandchild",
|
||||
tenant_id="tenant-1",
|
||||
structure_id=self.employer.id,
|
||||
relation_type_id=self.employer_parent.id,
|
||||
source_unit_id=self.child.id,
|
||||
target_unit_id=self.grandchild.id,
|
||||
is_active=True,
|
||||
settings={},
|
||||
)
|
||||
self.project_child = OrganizationRelation(
|
||||
id="edge-project-child",
|
||||
tenant_id="tenant-1",
|
||||
structure_id=self.project.id,
|
||||
relation_type_id=self.project_parent.id,
|
||||
source_unit_id=self.project_root.id,
|
||||
target_unit_id=self.child.id,
|
||||
is_active=True,
|
||||
settings={},
|
||||
)
|
||||
self.function = OrganizationFunction(
|
||||
id="function-child-intake",
|
||||
tenant_id="tenant-1",
|
||||
function_type_id=self.function_type.id,
|
||||
organization_unit_id=self.child.id,
|
||||
slug="intake",
|
||||
name="Child intake",
|
||||
is_active=True,
|
||||
settings={},
|
||||
)
|
||||
self.session.add_all(
|
||||
[
|
||||
self.unit_type,
|
||||
self.function_type,
|
||||
self.employer,
|
||||
self.project,
|
||||
self.employer_parent,
|
||||
self.project_parent,
|
||||
self.root,
|
||||
self.child,
|
||||
self.grandchild,
|
||||
self.project_root,
|
||||
self.other_tenant,
|
||||
self.employer_child,
|
||||
self.employer_grandchild,
|
||||
self.project_child,
|
||||
self.function,
|
||||
]
|
||||
)
|
||||
self.session.commit()
|
||||
|
||||
def _unit(
|
||||
self,
|
||||
item_id: str,
|
||||
name: str,
|
||||
*,
|
||||
tenant_id: str = "tenant-1",
|
||||
) -> OrganizationUnit:
|
||||
return OrganizationUnit(
|
||||
id=item_id,
|
||||
tenant_id=tenant_id,
|
||||
unit_type_id=(
|
||||
self.unit_type.id if tenant_id == "tenant-1" else None
|
||||
),
|
||||
slug=item_id,
|
||||
name=name,
|
||||
is_active=True,
|
||||
settings={},
|
||||
)
|
||||
|
||||
def test_parallel_structures_preserve_distinct_ancestor_provenance(
|
||||
self,
|
||||
) -> None:
|
||||
employer = self.directory.resolve_hierarchy_relatives(
|
||||
"tenant-1",
|
||||
(self.child.id,),
|
||||
structure_id=self.employer.id,
|
||||
direction="ancestors",
|
||||
)[0]
|
||||
project = self.directory.resolve_hierarchy_relatives(
|
||||
"tenant-1",
|
||||
(self.child.id,),
|
||||
structure_id=self.project.id,
|
||||
direction="ancestors",
|
||||
)[0]
|
||||
|
||||
self.assertEqual([self.root.id], [item.unit.id for item in employer.matches])
|
||||
self.assertEqual(
|
||||
[self.project_root.id],
|
||||
[item.unit.id for item in project.matches],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.employer.id,
|
||||
employer.matches[0].path[0].structure.id,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.project_parent.id,
|
||||
project.matches[0].path[0].relation_type.id,
|
||||
)
|
||||
|
||||
def test_bounded_paths_report_depth_and_cycles(self) -> None:
|
||||
bounded = self.directory.resolve_hierarchy_relatives(
|
||||
"tenant-1",
|
||||
(self.root.id,),
|
||||
structure_id=self.employer.id,
|
||||
direction="descendants",
|
||||
max_depth=1,
|
||||
)[0]
|
||||
path = self.directory.resolve_hierarchy_paths(
|
||||
"tenant-1",
|
||||
((self.child.id, self.root.id),),
|
||||
structure_id=self.employer.id,
|
||||
direction="ancestors",
|
||||
)[0]
|
||||
|
||||
self.assertEqual([self.child.id], [item.unit.id for item in bounded.matches])
|
||||
self.assertTrue(bounded.depth_limited)
|
||||
self.assertEqual("active", path.status)
|
||||
self.assertEqual(
|
||||
[self.employer_child.id],
|
||||
[edge.id for edge in path.path],
|
||||
)
|
||||
|
||||
self.employer_parent.allow_cycles = True
|
||||
self.session.add(
|
||||
OrganizationRelation(
|
||||
id="edge-cycle",
|
||||
tenant_id="tenant-1",
|
||||
structure_id=self.employer.id,
|
||||
relation_type_id=self.employer_parent.id,
|
||||
source_unit_id=self.grandchild.id,
|
||||
target_unit_id=self.root.id,
|
||||
is_active=True,
|
||||
settings={},
|
||||
)
|
||||
)
|
||||
self.session.commit()
|
||||
cycle = self.directory.resolve_hierarchy_relatives(
|
||||
"tenant-1",
|
||||
(self.root.id,),
|
||||
structure_id=self.employer.id,
|
||||
direction="descendants",
|
||||
)[0]
|
||||
self.assertTrue(cycle.cycle_detected)
|
||||
self.assertTrue(
|
||||
any(item.startswith("cycle_detected:") for item in cycle.diagnostics)
|
||||
)
|
||||
|
||||
def test_function_and_unit_type_resolution_explains_missing_state(
|
||||
self,
|
||||
) -> None:
|
||||
functions = self.directory.resolve_functions_by_type(
|
||||
"tenant-1",
|
||||
self.function_type.id,
|
||||
organization_unit_ids=(self.child.id, "missing-unit"),
|
||||
)
|
||||
missing_type = self.directory.resolve_functions_by_type(
|
||||
"tenant-1",
|
||||
"missing-type",
|
||||
organization_unit_ids=(self.child.id,),
|
||||
)
|
||||
units = self.directory.resolve_units_by_type(
|
||||
"tenant-1",
|
||||
self.unit_type.id,
|
||||
structure_id=self.employer.id,
|
||||
root_unit_id=self.root.id,
|
||||
direction="descendants",
|
||||
)
|
||||
|
||||
self.assertEqual([self.function.id], [item.id for item in functions.matches])
|
||||
self.assertEqual(("missing-unit",), functions.missing_unit_ids)
|
||||
self.assertEqual("missing", missing_type.status)
|
||||
self.assertEqual(
|
||||
{self.root.id, self.child.id, self.grandchild.id},
|
||||
{item.id for item in units.matches},
|
||||
)
|
||||
|
||||
def test_moved_deactivated_and_cross_tenant_units_are_current(
|
||||
self,
|
||||
) -> None:
|
||||
self.employer_child.source_unit_id = self.project_root.id
|
||||
self.child.is_active = False
|
||||
self.session.commit()
|
||||
|
||||
moved = self.directory.resolve_hierarchy_relatives(
|
||||
"tenant-1",
|
||||
(self.child.id,),
|
||||
structure_id=self.employer.id,
|
||||
direction="ancestors",
|
||||
)[0]
|
||||
isolated = self.directory.resolve_hierarchy_relatives(
|
||||
"tenant-1",
|
||||
(self.other_tenant.id,),
|
||||
structure_id=self.employer.id,
|
||||
direction="ancestors",
|
||||
)[0]
|
||||
functions = self.directory.resolve_functions_by_type(
|
||||
"tenant-1",
|
||||
self.function_type.id,
|
||||
organization_unit_ids=(self.child.id,),
|
||||
)
|
||||
|
||||
self.assertEqual("inactive", moved.status)
|
||||
self.assertEqual(
|
||||
[self.project_root.id],
|
||||
[item.unit.id for item in moved.matches],
|
||||
)
|
||||
self.assertEqual("missing", isolated.status)
|
||||
self.assertEqual((self.child.id,), functions.inactive_unit_ids)
|
||||
|
||||
|
||||
class OrganizationLifecycleEventTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(self.engine)
|
||||
self.Session = sessionmaker(
|
||||
bind=self.engine,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
self.session: Session = self.Session()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.session.close()
|
||||
Base.metadata.drop_all(self.engine)
|
||||
self.engine.dispose()
|
||||
|
||||
def test_changes_emit_versioned_commit_coupled_events(self) -> None:
|
||||
events = []
|
||||
bus = EventBus()
|
||||
bus.subscribe("*", events.append)
|
||||
with event_bus_context(bus):
|
||||
unit_type = OrganizationUnitType(
|
||||
tenant_id="tenant-1",
|
||||
slug="department",
|
||||
name="Department",
|
||||
is_active=True,
|
||||
settings={},
|
||||
)
|
||||
self.session.add(unit_type)
|
||||
_commit(self.session, unit_type)
|
||||
unit_type.name = "Office"
|
||||
_commit(self.session, unit_type)
|
||||
unit_type.is_active = False
|
||||
_commit(self.session, unit_type)
|
||||
|
||||
organization_events = [
|
||||
event
|
||||
for event in events
|
||||
if event.module_id == "organizations"
|
||||
]
|
||||
self.assertEqual(
|
||||
[
|
||||
"organizations.unit_type.created.v1",
|
||||
"organizations.unit_type.updated.v1",
|
||||
"organizations.unit_type.deactivated.v1",
|
||||
],
|
||||
[event.type for event in organization_events],
|
||||
)
|
||||
self.assertTrue(unit_type.id)
|
||||
self.assertEqual(
|
||||
1,
|
||||
organization_events[0].payload["schema_version"],
|
||||
)
|
||||
self.assertEqual(
|
||||
unit_type.id,
|
||||
organization_events[0].payload["resource_id"],
|
||||
)
|
||||
self.assertIn(
|
||||
"name",
|
||||
organization_events[1].payload["changed_fields"],
|
||||
)
|
||||
self.assertEqual(
|
||||
"inactive",
|
||||
organization_events[2].payload["status"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user