"""Integration test for feishu Phase-A dept scoping. ``_compute_inscope_dept_ids`` must project only departments that hold ≥1 in-scope (post-filter) member, expanded up the ancestor chain — so hollow shells and depts of filtered-out people are never created on Feishu (the 2026-06-08 prod audit found 10 empty English dept nodes that collide semantically with existing Chinese Feishu depts). """ from __future__ import annotations import uuid import pytest from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.domains.identity_sync.models import IdentitySyncTarget from app.domains.identity_sync.presets import ( _FEISHU_USER_FILTER, FEISHU_CONTACT_USER_MAPPING_RULES, ) from app.domains.identity_sync.service import IdentitySyncService from app.identity.person_models import ( IdentityOrgMembership, IdentityOrgNode, IdentityPerson, ) async def _node(db, *, src_id, name, parent=None, level=1): db.add(IdentityOrgNode( id=uuid.uuid4(), source_system="successfactors", org_node_source_id=src_id, org_node_name=name, parent_org_node_source_id=parent, org_level=level, node_status="active", )) await db.flush() async def _person_in(db, *, email, dept_src, employee_no): pid = uuid.uuid4() db.add(IdentityPerson( id=pid, work_email=email, display_name=email.split("@")[0], employment_status="active", source_system="successfactors", hris_person_id=f"sf-{email}", employee_no=employee_no, is_china_employee=True, country="CHN", )) await db.flush() db.add(IdentityOrgMembership( id=uuid.uuid4(), person_id=pid, source_system="successfactors", org_node_source_id=dept_src, org_node_name=dept_src, is_primary=True, membership_status="active", )) await db.flush() return pid async def _feishu_target(db) -> IdentitySyncTarget: tgt = IdentitySyncTarget( id=uuid.uuid4(), name="Test Feishu", slug="test-feishu-scope", kind="feishu_contact", enabled=False, on_offboard="deactivate", mapping_config={"rules": list(FEISHU_CONTACT_USER_MAPPING_RULES)}, filter_config=dict(_FEISHU_USER_FILTER), schedule_config={"trigger": "manual", "cron_expr": None, "timezone": "UTC"}, runner_options={}, ) db.add(tgt) await db.flush() return tgt async def _scoped(svc: IdentitySyncService, target) -> set[str]: nodes = (await svc.db.execute( select(IdentityOrgNode).where( IdentityOrgNode.deleted_at.is_(None), IdentityOrgNode.node_status == "active", ) )).scalars().all() return await svc._compute_inscope_dept_ids(target=target, nodes=list(nodes)) @pytest.mark.asyncio async def test_empty_shell_skipped_ancestors_kept(db_session: AsyncSession): # Tree: ROOT → {ALPHA (has member), BETA (empty shell)} await _node(db_session, src_id="ROOT", name="Root BU", parent=None, level=1) await _node(db_session, src_id="ALPHA", name="Alpha", parent="ROOT", level=2) await _node(db_session, src_id="BETA", name="Beta Empty", parent="ROOT", level=2) await _person_in(db_session, email="a@pingcap.com", dept_src="ALPHA", employee_no="P00001") target = await _feishu_target(db_session) await db_session.commit() scoped = await _scoped(IdentitySyncService(db_session), target) # ALPHA has a member; ROOT is its ancestor → both in scope. assert "ALPHA" in scoped assert "ROOT" in scoped # BETA is a hollow shell → skipped (this is the 10-empty-dept fix). assert "BETA" not in scoped @pytest.mark.asyncio async def test_dept_of_nonemployee_only_member_skipped(db_session: AsyncSession): """A dept whose ONLY member is excluded by the employee_no gate (service account) must itself be skipped — no empty dept created.""" await _node(db_session, src_id="ROOT", name="Root BU", parent=None, level=1) await _node(db_session, src_id="GAMMA", name="Gamma", parent="ROOT", level=2) # Only member is a non-P service account → filtered out by the gate. await _person_in(db_session, email="svc@pingcap.cn", dept_src="GAMMA", employee_no="TimeAdmin") target = await _feishu_target(db_session) await db_session.commit() scoped = await _scoped(IdentitySyncService(db_session), target) assert "GAMMA" not in scoped assert "ROOT" not in scoped # no in-scope member anywhere