"""Integration tests for ``nexuscrm.seat_sync``. Real DB (catalog / plan / connector / persons), driver stubbed via the coordinator loader. Covers: matched active CRM user → seat import; system/integration + leaver accounts → external (no seat); idempotent re-run; drift cleanup (Helix-side, DB-only) for a seat whose holder left the CRM roster. """ from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta from decimal import Decimal import pytest from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import lazyload from app.domains.asset.models import ( AssetSoftwareCatalog, SoftwareLicensePlan, SoftwareSeatAllocation, ) from app.domains.provisioning.models import SoftwareConnector from app.identity.person_models import IdentityPerson from app.integrations.models import Integration from app.scheduled_jobs import nexuscrm_handlers class _StubDriver: def __init__(self, integration_id: str, members: list[dict]) -> None: self.integration_id = integration_id self._members = members async def list_full_members(self) -> list[dict]: return self._members async def _setup(db: AsyncSession) -> tuple[Integration, SoftwareLicensePlan]: integ = Integration( id=uuid.uuid4(), kind="software_provisioning", provider="nexuscrm", name="NexusCRM test", slug=f"nexuscrm-{uuid.uuid4().hex[:6]}", enabled=True, is_primary=True, config_public={"instance_url": "https://crm.example.com", "consumer_key": "ck", "api_version": "v63.0"}, ) db.add(integ) await db.flush() sw = AssetSoftwareCatalog( id=uuid.uuid4(), name="NexusCRM", asset_type="SaaS", status="active", offboarding_policy="auto_revoke", ) db.add(sw) await db.flush() plan = SoftwareLicensePlan( id=uuid.uuid4(), software_id=sw.id, name="NexusCRM Seat", price=Decimal("0.00"), billing_cycle="monthly", currency="USD", status="active", max_seats=None, ) db.add(plan) await db.flush() db.add(SoftwareConnector( software_id=sw.id, integration_id=integ.id, is_active=True, binding_config={"execution_mode": "manual_notify", "action_on_offboard": "deactivate"}, )) await db.flush() return integ, plan async def _person(db: AsyncSession, email: str, status: str = "active") -> IdentityPerson: p = IdentityPerson( id=uuid.uuid4(), work_email=email, display_name=email.split("@")[0], employment_status=status, source_system="successfactors", hris_person_id=f"sf-{uuid.uuid4().hex[:8]}", country="cn", ) db.add(p) await db.flush() return p def _member(email: str, name: str, is_system: bool = False) -> dict: return {"id": f"005{uuid.uuid4().hex[:12]}", "email": email, "display_name": name, "profile": None, "is_system": is_system} async def _active_seat_user_ids(db: AsyncSession, plan_id: uuid.UUID) -> set: db.expunge_all() rows = (await db.execute( select(SoftwareSeatAllocation).where( SoftwareSeatAllocation.plan_id == plan_id, SoftwareSeatAllocation.status == "active", ).options(lazyload("*")) )).scalars().all() return {s.user_id for s in rows} @pytest.mark.asyncio async def test_import_match_and_external(db_session: AsyncSession, monkeypatch): integ, plan = await _setup(db_session) emp = await _person(db_session, "vince@pingcap.com") await _person(db_session, "leaver@pingcap.com", status="left") members = [ _member("vince@pingcap.com", "Vince"), # matched → seat _member("leaver@pingcap.com", "Lea"), # left → external _member("cloud@00d5g000002g54ueac", "Platform Integration User", True), ] stub = _StubDriver(str(integ.id), members) async def _fake_load(db): return stub monkeypatch.setattr( "app.core.coordinators.nexuscrm_seat_coordinator.load_nexuscrm_driver", _fake_load, ) summary = await nexuscrm_handlers.run_nexuscrm_seat_sync(db_session, {}) assert summary["seats_imported"] == 1 assert summary["external_unmatched"] == 2 # leaver + integration account assert emp.id in await _active_seat_user_ids(db_session, plan.id) # Idempotent: second run imports nothing new. summary2 = await nexuscrm_handlers.run_nexuscrm_seat_sync(db_session, {}) assert summary2["seats_imported"] == 0 class _RaisingDriver: """list_full_members blows up with an EMPTY-str exception, exactly as a raw httpx timeout (ConnectTimeout / ReadTimeout) does in prod.""" def __init__(self, integration_id: str, exc: Exception) -> None: self.integration_id = integration_id self._exc = exc async def list_full_members(self) -> list[dict]: raise self._exc @pytest.mark.asyncio async def test_primary_list_timeout_raises_with_type_name( db_session: AsyncSession, monkeypatch ): """A primary-list failure must (a) raise so the failure_notifier fires, and (b) carry the exception TYPE in the message even when str(exc) is empty — otherwise the on-call gets a signal-free 'list_full_members failed:' with nothing after the colon (the alert that prompted this).""" import httpx integ, _plan = await _setup(db_session) # httpx timeout exceptions render to an empty string. timeout = httpx.ConnectTimeout("") assert str(timeout) == "" stub = _RaisingDriver(str(integ.id), timeout) async def _fake_load(db): return stub monkeypatch.setattr( "app.core.coordinators.nexuscrm_seat_coordinator.load_nexuscrm_driver", _fake_load, ) with pytest.raises(RuntimeError) as ei: await nexuscrm_handlers.run_nexuscrm_seat_sync(db_session, {}) msg = str(ei.value) assert msg.startswith("nexuscrm.list_full_members failed:") # The regression: the type name must survive an empty str(exc). assert "ConnectTimeout" in msg assert msg.rstrip() != "nexuscrm.list_full_members failed:" @pytest.mark.asyncio async def test_drift_revokes_seat_for_absent_holder(db_session: AsyncSession, monkeypatch): integ, plan = await _setup(db_session) # Enough current members that the drift safety ratio passes. keep = [await _person(db_session, f"keep{i}@pingcap.com") for i in range(4)] gone = await _person(db_session, "gone@pingcap.com") # Pre-existing seats (older than the 2h fresh-grant grace). old = datetime.now(UTC) - timedelta(hours=5) for p in [*keep, gone]: db_session.add(SoftwareSeatAllocation( plan_id=plan.id, user_id=p.id, status="active", allocated_at=old, )) await db_session.flush() members = [_member(f"keep{i}@pingcap.com", f"K{i}") for i in range(4)] stub = _StubDriver(str(integ.id), members) async def _fake_load(db): return stub monkeypatch.setattr( "app.core.coordinators.nexuscrm_seat_coordinator.load_nexuscrm_driver", _fake_load, ) summary = await nexuscrm_handlers.run_nexuscrm_seat_sync(db_session, {}) assert summary["seats_revoked"] == 1 active = await _active_seat_user_ids(db_session, plan.id) assert gone.id not in active assert all(p.id in active for p in keep)