"""Ownership resolution, branch by branch. The rule under test is the one that is easy to soften and expensive to soften: **never invent an owner.** Every branch either produces a person with a real, checkable link to the failure, or produces nobody. The last test is the important one. It pins the failure direction: when no link exists, the answer is ``none`` — not "IT", not the last person who edited the integration row. A default would hide the gap and make the whole column meaningless once most rows carried it. """ from __future__ import annotations import uuid import pytest from app.domains.asset.models import AssetSoftwareCatalog from app.domains.automation_exception.ownership import ( OWNER_SOURCE_NONE, OWNER_SOURCE_SOFTWARE_IT, OWNER_SOURCE_SOFTWARE_OWNER, OWNER_SOURCE_SYSTEM_BACKUP, OWNER_SOURCE_SYSTEM_REGISTRY, resolve_owner, ) from app.domains.provisioning.models import SoftwareConnector from app.domains.system_registry.models import RegisteredSystem from app.identity.person_models import IdentityPerson from app.integrations.models import Integration async def _integration(db, slug="acme") -> Integration: row = Integration( kind="software_provisioning", provider="acme", name="Acme", slug=slug, enabled=True, is_primary=False, ) db.add(row) await db.flush() return row async def _departed_person(db) -> IdentityPerson: """A person who is already gone. Inserted as ``left``, never transitioned into it: the leaver guard deliberately ignores inserts (a brand-new row can have no pending blockers) and deliberately traps updates. """ row = IdentityPerson( # ``hris_person_id`` is NOT NULL — every person in this platform # comes from the HR source of truth, so a row without one cannot # exist. Same shape the offboarding tests use. hris_person_id=f"P{uuid.uuid4().hex[:8]}", work_email=f"gone-{uuid.uuid4().hex[:8]}@pingcap.com", display_name="Departed Owner", employee_no=f"P{uuid.uuid4().hex[:5]}", employment_status="left", source_system="successfactors", ) db.add(row) await db.flush() return row async def _catalog(db, *, owner=None, it_contact=None) -> AssetSoftwareCatalog: row = AssetSoftwareCatalog( name=f"pkg-{uuid.uuid4().hex[:8]}", owner_person_id=owner, it_contact_person_id=it_contact, status="active", ) db.add(row) await db.flush() return row @pytest.mark.asyncio class TestOwnershipResolution: async def test_the_package_the_failure_is_about_wins( self, db_session, employee_user, ): person = employee_user[0] catalog = await _catalog(db_session, owner=person.id) resolved = await resolve_owner( db_session, integration=None, software_id=catalog.id, ) assert resolved.person_id == person.id assert resolved.source == OWNER_SOURCE_SOFTWARE_OWNER async def test_the_it_contact_is_the_fallback_within_the_catalog( self, db_session, employee_user, ): person = employee_user[0] catalog = await _catalog(db_session, owner=None, it_contact=person.id) resolved = await resolve_owner( db_session, integration=None, software_id=catalog.id, ) assert resolved.person_id == person.id assert resolved.source == OWNER_SOURCE_SOFTWARE_IT async def test_an_integration_reaches_its_owner_through_the_connector( self, db_session, employee_user, ): # The schema-backed path: software_connectors binds a catalog row # to an integration with real foreign keys. person = employee_user[0] integration = await _integration(db_session) catalog = await _catalog(db_session, owner=person.id) db_session.add( SoftwareConnector( software_id=catalog.id, integration_id=integration.id, is_active=True, ), ) await db_session.flush() resolved = await resolve_owner( db_session, integration=integration, software_id=None, ) assert resolved.person_id == person.id assert resolved.source == OWNER_SOURCE_SOFTWARE_OWNER async def test_an_inactive_connector_is_not_a_link( self, db_session, employee_user, ): person = employee_user[0] integration = await _integration(db_session) catalog = await _catalog(db_session, owner=person.id) db_session.add( SoftwareConnector( software_id=catalog.id, integration_id=integration.id, is_active=False, ), ) await db_session.flush() resolved = await resolve_owner( db_session, integration=integration, software_id=None, ) assert resolved.person_id is None async def test_the_registry_slug_match_is_used_when_it_happens_to_match( self, db_session, employee_user, ): # Opportunistic by design — see the module docstring. It matched # 0 of 5 integrations on the local dataset, so this test # constructs the match rather than assuming one exists. person = employee_user[0] integration = await _integration(db_session, slug="matching-slug") db_session.add( RegisteredSystem( slug="matching-slug", name="Matching", kind="internal", status="active", exposure="internal", owner_person_id=person.id, ), ) await db_session.flush() resolved = await resolve_owner( db_session, integration=integration, software_id=None, ) assert resolved.person_id == person.id assert resolved.source == OWNER_SOURCE_SYSTEM_REGISTRY async def test_a_departed_owner_falls_through_to_the_backup( self, db_session, admin_user, ): # An owner who has left is not an owner. This is why the registry # carries a backup at all. # # The departed person is INSERTED already ``left`` rather than # transitioned. Flipping a live person's ``employment_status`` to # ``left`` is exactly what ``leaver_blocked``'s before_flush guard # exists to stop — an unauthorised write there is trapped and # audited, and rightly so. Every other test in this repo that # needs a departed person inserts one; this follows that. departed = await _departed_person(db_session) backup = admin_user[0] integration = await _integration(db_session, slug="with-backup") db_session.add( RegisteredSystem( slug="with-backup", name="Backed", kind="internal", status="active", exposure="internal", owner_person_id=departed.id, backup_owner_person_id=backup.id, ), ) await db_session.flush() resolved = await resolve_owner( db_session, integration=integration, software_id=None, ) assert resolved.person_id == backup.id assert resolved.source == OWNER_SOURCE_SYSTEM_BACKUP async def test_no_link_means_nobody_not_a_default( self, db_session, ): # The load-bearing assertion of this module. Defaulting unowned # rows to IT would hide the gap AND make the column meaningless # once most rows carried the same fallback. Routing work to a # group is a notification decision made at send time, not an # ownership claim stored on the row. integration = await _integration(db_session, slug="orphan") resolved = await resolve_owner( db_session, integration=integration, software_id=None, ) assert resolved.person_id is None assert resolved.source == OWNER_SOURCE_NONE async def test_a_source_with_no_integration_and_no_package_is_unowned( self, db_session, ): # A whole-cron failure belongs to the platform, not to a system. resolved = await resolve_owner( db_session, integration=None, software_id=None, ) assert resolved.person_id is None assert resolved.source == OWNER_SOURCE_NONE