"""GET /api/v1/role-changes?q= — server-side person search. Covers: * ``q`` matches person.display_name (case-insensitive substring) * ``q`` matches person.work_email * ``q`` matches nothing → row absent * ``q`` composes with ``only_pending=true`` """ from __future__ import annotations from datetime import UTC, date, datetime, timedelta from uuid import uuid4 import pytest from app.core.coordinators.role_change_coordinator import ( REVIEW_STATUS_CONFIRMED_KEEP, REVIEW_STATUS_PENDING, ROLE_CHANGE_EVENT_TYPE, ) from app.domains.cmdb.models import CmdbCloudAccessGrant, CmdbCloudScope from app.domains.hr.models import HrLifecycleEvent from app.identity.person_models import IdentityPerson from app.integrations.models import Integration async def _mkperson(db_session, *, name: str, email: str) -> IdentityPerson: p = IdentityPerson( id=uuid4(), source_system="successfactors", hris_person_id=f"sf-rcq-{uuid4().hex[:8]}", employee_no=f"EMP-{uuid4().hex[:6]}", work_email=email, display_name=name, employment_status="active", hire_date=date.today() - timedelta(days=200), ) db_session.add(p) await db_session.commit() await db_session.refresh(p) return p async def _mkevent(db_session, person: IdentityPerson) -> HrLifecycleEvent: ev = HrLifecycleEvent( id=uuid4(), person_id=person.id, event_type=ROLE_CHANGE_EVENT_TYPE, detail={ "before": {"dept_code": "OLD"}, "after": {"dept_code": "NEW"}, }, effective_date=date.today(), ) db_session.add(ev) await db_session.commit() return ev async def _mkscope(db_session) -> CmdbCloudScope: integ = Integration( id=uuid4(), kind="cloud", provider="aws", name=f"rcq-test-aws-{uuid4().hex[:6]}", slug=f"rcq-aws-{uuid4().hex[:6]}", enabled=True, is_primary=False, ) db_session.add(integ) await db_session.flush() scope = CmdbCloudScope( id=uuid4(), integration_id=str(integ.id), scope_type="aws_account", external_id=f"acct-{uuid4().hex[:8]}", status="active", ) db_session.add(scope) await db_session.flush() return scope async def _mkgrant( db_session, *, person: IdentityPerson, scope: CmdbCloudScope, review_status: str = REVIEW_STATUS_PENDING, ) -> CmdbCloudAccessGrant: grant = CmdbCloudAccessGrant( scope_id=str(scope.id), person_id=str(person.id), role_key=f"aws:Reader-{uuid4().hex[:4]}", role_display_name="AWS Reader", status="active", granted_at=datetime.now(UTC) - timedelta(days=60), review_status=review_status, review_status_changed_at=datetime.now(UTC), ) db_session.add(grant) await db_session.commit() await db_session.refresh(grant) return grant async def _fetch_person_ids(client, token: str, **params) -> set[str]: resp = await client.get( "/api/v1/role-changes", params=params, headers={"Authorization": f"Bearer {token}"}, ) assert resp.status_code == 200, resp.text return {r["person"]["id"] for r in resp.json()["data"]["rows"]} @pytest.mark.asyncio async def test_q_matches_display_name(client, admin_user, db_session): _, token = admin_user hit = await _mkperson( db_session, name="Alice Wonderland", email="alice.w@company.com", ) miss = await _mkperson( db_session, name="Bob Builder", email="bob.b@company.com", ) await _mkevent(db_session, hit) await _mkevent(db_session, miss) ids = await _fetch_person_ids(client, token, q="wonder") assert str(hit.id) in ids assert str(miss.id) not in ids @pytest.mark.asyncio async def test_q_matches_work_email(client, admin_user, db_session): _, token = admin_user hit = await _mkperson( db_session, name="Carol Smith", email="carol.smith@company.com", ) miss = await _mkperson( db_session, name="Dave Jones", email="dave.jones@company.com", ) await _mkevent(db_session, hit) await _mkevent(db_session, miss) ids = await _fetch_person_ids(client, token, q="CAROL.SMITH@") assert str(hit.id) in ids assert str(miss.id) not in ids @pytest.mark.asyncio async def test_q_no_match_returns_empty(client, admin_user, db_session): _, token = admin_user person = await _mkperson( db_session, name="Eve Adams", email="eve.adams@company.com", ) await _mkevent(db_session, person) ids = await _fetch_person_ids(client, token, q="zzz-no-such-needle") assert ids == set() @pytest.mark.asyncio async def test_q_composes_with_only_pending(client, admin_user, db_session): _, token = admin_user scope = await _mkscope(db_session) pending_person = await _mkperson( db_session, name="Frank Pending", email="frank.pending@company.com", ) await _mkevent(db_session, pending_person) await _mkgrant( db_session, person=pending_person, scope=scope, review_status=REVIEW_STATUS_PENDING, ) resolved_person = await _mkperson( db_session, name="Grace Resolved", email="grace.resolved@company.com", ) await _mkevent(db_session, resolved_person) await _mkgrant( db_session, person=resolved_person, scope=scope, review_status=REVIEW_STATUS_CONFIRMED_KEEP, ) # q hits the pending person → kept by both filters. ids = await _fetch_person_ids( client, token, q="frank", only_pending="true", ) assert str(pending_person.id) in ids assert str(resolved_person.id) not in ids # q hits the resolved person, but only_pending drops them → empty. ids = await _fetch_person_ids( client, token, q="grace", only_pending="true", ) assert ids == set()