feat(cases): accept replay-safe ticket escalations

This commit is contained in:
2026-08-22 11:41:45 +02:00
parent f236034e88
commit d3a70f87cd
8 changed files with 351 additions and 7 deletions
+108
View File
@@ -0,0 +1,108 @@
from __future__ import annotations
from datetime import UTC, datetime
from types import SimpleNamespace
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from govoplan_core.core.tickets import TicketCaseEscalationCommand
from govoplan_core.db.base import Base
from govoplan_cases.backend.db import models as _case_models
from govoplan_cases.backend.service import upsert_case_status, upsert_case_type
from govoplan_cases.backend.ticket_escalation import TicketCaseEscalationProvider
NOW = datetime(2026, 8, 22, 10, 0, tzinfo=UTC)
class _Principal:
def __init__(self, tenant_id: str = "tenant-1", *, can_create: bool = True) -> None:
self.tenant_id = tenant_id
self.account_id = "case-manager-1"
self.identity_id = None
self.membership_id = "membership-1"
self.group_ids = frozenset()
self.role_ids = frozenset()
self.function_assignment_ids = frozenset()
self.acting_assignment_id = None
self.scopes = frozenset({"cases:case:create", "cases:case:read"} if can_create else {"cases:case:read"})
self.user = SimpleNamespace(id=self.account_id)
def has(self, scope: str) -> bool:
return scope in self.scopes
class TicketEscalationTests(unittest.TestCase):
def setUp(self) -> None:
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
Base.metadata.create_all(engine)
self.Session = sessionmaker(bind=engine, expire_on_commit=False)
self.principal = _Principal()
def test_escalation_creates_one_linked_case_and_replays(self) -> None:
with self.Session() as session:
upsert_case_status(
session,
self.principal,
status_key="open",
label="Open",
)
upsert_case_type(
session,
self.principal,
type_key="service-request",
label="Service request",
initial_status_key="open",
)
command = TicketCaseEscalationCommand(
tenant_id="tenant-1",
ticket_id="ticket-1",
ticket_number="TKT-2026-0001",
title="Broken streetlight",
case_type_key="service-request",
occurred_at=NOW,
idempotency_key="escalation-1",
handoff_note="Formal procedure required.",
metadata={"ticket_revision": 3},
)
provider = TicketCaseEscalationProvider()
created = provider.escalate_ticket(session, self.principal, command=command)
session.commit()
replay = provider.escalate_ticket(session, self.principal, command=command)
self.assertFalse(created.replayed)
self.assertTrue(replay.replayed)
self.assertEqual(created.case_id, replay.case_id)
self.assertEqual("service-request", created.metadata["case_type_key"])
self.assertTrue(created.case_url.startswith("/cases/"))
self.assertEqual(1, session.query(_case_models.CaseIdentity).count())
current = session.query(_case_models.CaseRecordRevision).filter(
_case_models.CaseRecordRevision.case_id == created.case_id,
_case_models.CaseRecordRevision.superseded_at.is_(None),
).one()
self.assertEqual("tickets", current.snapshot["context"]["work_item_ref"]["owner_module"])
self.assertEqual("ticket-1", current.snapshot["metadata"]["source_resource_id"])
def test_escalation_fails_closed_for_cross_tenant_or_missing_permission(self) -> None:
command = TicketCaseEscalationCommand(
tenant_id="tenant-1",
ticket_id="ticket-1",
ticket_number="TKT-1",
title="Request",
case_type_key="service-request",
occurred_at=NOW,
idempotency_key="escalation-2",
)
provider = TicketCaseEscalationProvider()
with self.Session() as session:
with self.assertRaises(PermissionError):
provider.escalate_ticket(session, _Principal("tenant-2"), command=command)
with self.assertRaises(PermissionError):
provider.escalate_ticket(session, _Principal(can_create=False), command=command)
if __name__ == "__main__":
unittest.main()