"""3-dim onboarding rule matching (department + job_title + location). Rule matches if every rule column is either NULL (wildcard) or equals the person's corresponding attribute. Multiple rules may match; the union (dedup on target) is what the onboarding flow uses. """ from __future__ import annotations from uuid import uuid4 import pytest from app.core.provision_match import policy_matches_values from app.domains.provisioning.models import ProvisionPolicy from app.domains.provisioning.service import ProvisioningService from app.identity.person_models import IdentityPerson def _mk_person(**kw) -> IdentityPerson: person = IdentityPerson( id=uuid4(), source_system="successfactors", hris_person_id=f"sf-{uuid4().hex[:6]}", work_email=f"{uuid4().hex[:6]}@example.com", display_name="Person", employment_status="active", ) for k, v in kw.items(): setattr(person, k, v) return person def _pin_primary_department(monkeypatch, value: str) -> None: """Pin ``IdentityPerson.primary_department`` for the duration of one test. The real property reads ``org_memberships``, which these unit-style tests deliberately never load, so the value has to be supplied. It MUST go through ``monkeypatch``. The property lives on the CLASS, not on the instance, so the hand-rolled ``try/finally`` this file used to carry — ``type(person).primary_department = IdentityPerson.primary_department`` — restored nothing: by the time the ``finally`` ran, reading ``IdentityPerson.primary_department`` returned the patch itself, and the line reassigned it to itself. Every subsequent test in the process then saw ``person.department == "Sales"`` for everyone. ``monkeypatch`` reads the original BEFORE it writes, which is the whole difference. """ monkeypatch.setattr( IdentityPerson, "primary_department", property(lambda _: value), ) def _mk_policy( *, department: str | None = None, job_title: str | None = None, location: str | None = None, ) -> ProvisionPolicy: return ProvisionPolicy( id=uuid4(), department_name=department, match_job_title=job_title, match_location=location, target_type="seat", target_id=uuid4(), is_active=True, ) @pytest.mark.parametrize("strict", [False, True]) def test_loaded_value_matcher_has_the_same_wildcard_and_mismatch_semantics(strict): assert policy_matches_values( rule_department=None, rule_job_title="Engineer", rule_location=None, department="Platform", job_title="Engineer", location="CN-SH", strict_null_on_missing=strict, ) assert not policy_matches_values( rule_department=None, rule_job_title="Sales", rule_location=None, department="Platform", job_title="Engineer", location="CN-SH", strict_null_on_missing=strict, ) def test_loaded_value_matcher_preserves_lenient_missing_value_semantics(): kwargs = { "rule_department": "Platform", "rule_job_title": None, "rule_location": None, "department": None, "job_title": "Engineer", "location": "CN-SH", } assert policy_matches_values(**kwargs, strict_null_on_missing=False) assert not policy_matches_values(**kwargs, strict_null_on_missing=True) @pytest.mark.asyncio async def test_wildcard_rule_matches_everyone(db_session, monkeypatch): """All-NULL rule matches every employee regardless of attrs.""" rule = _mk_policy() db_session.add(rule) await db_session.flush() svc = ProvisioningService(db_session) person = _mk_person(job_title="Eng", work_location_code="CN-SH") # primary_department is derived from org_memberships; with none loaded # it's None — which tests the "strict_null_on_missing=False" path. matches = await svc._match_policies_for_person(person) assert rule.id in [m.id for m in matches] @pytest.mark.asyncio async def test_sales_north_america_matches_only_relevant_rules(db_session, monkeypatch): """Salesforce for sales + Slack for North America — a salesperson in North America picks up both.""" sf_rule = _mk_policy(job_title="Sales") slack_rule = _mk_policy(location="US-SF") china_slack_rule = _mk_policy(location="CN-SH") engineering_sf_rule = _mk_policy(department="Engineering") db_session.add_all([sf_rule, slack_rule, china_slack_rule, engineering_sf_rule]) await db_session.flush() svc = ProvisioningService(db_session) person = _mk_person(job_title="Sales", work_location_code="US-SF") _pin_primary_department(monkeypatch, "Sales") matches = {m.id for m in await svc._match_policies_for_person(person)} assert sf_rule.id in matches assert slack_rule.id in matches assert china_slack_rule.id not in matches assert engineering_sf_rule.id not in matches @pytest.mark.asyncio async def test_union_dedup_by_target(db_session, monkeypatch): """Two rules matching the same person with the same target produce one entry.""" shared_target = uuid4() rule_a = ProvisionPolicy( id=uuid4(), department_name="Sales", match_job_title=None, match_location=None, target_type="seat", target_id=shared_target, is_active=True, ) rule_b = ProvisionPolicy( id=uuid4(), department_name=None, match_job_title=None, match_location="US-SF", target_type="seat", target_id=shared_target, is_active=True, ) db_session.add_all([rule_a, rule_b]) await db_session.flush() svc = ProvisioningService(db_session) person = _mk_person(work_location_code="US-SF") _pin_primary_department(monkeypatch, "Sales") matches = await svc._match_policies_for_person(person) target_ids = [m.target_id for m in matches] assert target_ids.count(shared_target) == 1 @pytest.mark.asyncio async def test_the_department_patch_does_not_outlive_the_test_that_set_it(): """Guard for the leak this file used to ship. ``primary_department`` is patched on the CLASS by the two tests above. Defined LAST on purpose: pytest runs a module in definition order, so a restore that silently fails to restore is caught right here instead of hundreds of tests later, in a different suite, as an unrelated-looking assertion (the MCP directory roster reporting ``{'Sales'}`` for three people it had just filed under 「Treasury and Tax」). """ person = _mk_person() person.org_memberships = [] assert person.primary_department is None assert person.department is None