149 lines
5.7 KiB
Python
149 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from govoplan_core.core.change_sequence import ChangeSequenceEntry
|
|
from govoplan_core.core.events import EventBus, PlatformEvent, event_bus_context
|
|
from govoplan_core.db.base import Base
|
|
from govoplan_core.db.session import configure_database, reset_database
|
|
from govoplan_identity.backend.db import models as identity_models # noqa: F401
|
|
from govoplan_idm.backend.assignment_lifecycle import SqlIdmAssignmentLifecycle
|
|
from govoplan_idm.backend.db.models import IdmOrganizationFunctionAssignment
|
|
from govoplan_organizations.backend.db import models as organization_models # noqa: F401
|
|
|
|
|
|
class AssignmentExpiryTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.database = configure_database("sqlite:///:memory:")
|
|
Base.metadata.create_all(
|
|
self.database.engine,
|
|
tables=[
|
|
IdmOrganizationFunctionAssignment.__table__,
|
|
ChangeSequenceEntry.__table__,
|
|
],
|
|
)
|
|
self.lifecycle = SqlIdmAssignmentLifecycle()
|
|
|
|
def tearDown(self) -> None:
|
|
reset_database(dispose=True)
|
|
|
|
@staticmethod
|
|
def _assignment(
|
|
assignment_id: str,
|
|
*,
|
|
tenant_id: str = "tenant-1",
|
|
valid_until: datetime,
|
|
active: bool = True,
|
|
expired_event_at: datetime | None = None,
|
|
) -> IdmOrganizationFunctionAssignment:
|
|
return IdmOrganizationFunctionAssignment(
|
|
id=assignment_id,
|
|
tenant_id=tenant_id,
|
|
identity_id=f"identity-{assignment_id}",
|
|
function_id="function-1",
|
|
organization_unit_id="unit-1",
|
|
source="direct",
|
|
valid_until=valid_until,
|
|
expired_event_at=expired_event_at,
|
|
is_active=active,
|
|
settings={},
|
|
)
|
|
|
|
def test_sweep_claims_due_assignments_once_and_preserves_provenance(self) -> None:
|
|
boundary = datetime(2026, 7, 31, 12, tzinfo=timezone.utc)
|
|
with self.database.session() as session:
|
|
session.add_all(
|
|
(
|
|
self._assignment(
|
|
"due",
|
|
valid_until=boundary - timedelta(seconds=1),
|
|
),
|
|
self._assignment(
|
|
"future",
|
|
valid_until=boundary + timedelta(seconds=1),
|
|
),
|
|
self._assignment(
|
|
"revoked",
|
|
valid_until=boundary - timedelta(seconds=1),
|
|
active=False,
|
|
),
|
|
self._assignment(
|
|
"already-emitted",
|
|
valid_until=boundary - timedelta(seconds=1),
|
|
expired_event_at=boundary - timedelta(minutes=1),
|
|
),
|
|
self._assignment(
|
|
"other-tenant",
|
|
tenant_id="tenant-2",
|
|
valid_until=boundary - timedelta(seconds=1),
|
|
),
|
|
)
|
|
)
|
|
session.commit()
|
|
|
|
events: list[PlatformEvent] = []
|
|
bus = EventBus()
|
|
bus.subscribe("idm.function_assignment.expired.v1", events.append)
|
|
with self.database.SessionLocal() as session, event_bus_context(bus):
|
|
result = self.lifecycle.process_expired(
|
|
session,
|
|
tenant_id="tenant-1",
|
|
effective_at=boundary,
|
|
)
|
|
session.commit()
|
|
|
|
self.assertEqual(1, result["selected"])
|
|
self.assertEqual(1, result["expired"])
|
|
self.assertEqual(["due"], result["assignment_ids"])
|
|
self.assertEqual(1, len(events))
|
|
self.assertEqual("system", events[0].actor.type)
|
|
self.assertEqual("tenant-1", events[0].tenant.id)
|
|
self.assertEqual("identity-due", events[0].payload["identity_id"])
|
|
self.assertEqual("function-1", events[0].payload["function_id"])
|
|
|
|
with self.database.session() as session:
|
|
due = session.get(IdmOrganizationFunctionAssignment, "due")
|
|
self.assertEqual(boundary, due.expired_event_at.replace(tzinfo=timezone.utc))
|
|
repeated = self.lifecycle.process_expired(
|
|
session,
|
|
tenant_id="tenant-1",
|
|
effective_at=boundary,
|
|
)
|
|
session.commit()
|
|
self.assertEqual(0, repeated["expired"])
|
|
self.assertEqual(1, len(events))
|
|
|
|
def test_sweep_rollback_releases_marker_and_event(self) -> None:
|
|
boundary = datetime(2026, 7, 31, 12, tzinfo=timezone.utc)
|
|
with self.database.session() as session:
|
|
session.add(
|
|
self._assignment(
|
|
"rolled-back",
|
|
valid_until=boundary - timedelta(seconds=1),
|
|
)
|
|
)
|
|
session.commit()
|
|
|
|
events: list[PlatformEvent] = []
|
|
bus = EventBus()
|
|
bus.subscribe("idm.function_assignment.expired.v1", events.append)
|
|
with self.database.SessionLocal() as session, event_bus_context(bus):
|
|
self.lifecycle.process_expired(session, effective_at=boundary)
|
|
session.rollback()
|
|
self.assertEqual([], events)
|
|
|
|
with self.database.session() as session:
|
|
item = session.get(IdmOrganizationFunctionAssignment, "rolled-back")
|
|
self.assertIsNone(item.expired_event_at)
|
|
|
|
def test_limit_validation_is_bounded(self) -> None:
|
|
with self.database.session() as session:
|
|
for value in (0, 1001):
|
|
with self.subTest(limit=value), self.assertRaises(ValueError):
|
|
self.lifecycle.process_expired(session, limit=value)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|