"""Tests for the ``assignment_scope`` cycle scope filter. The cycle creation form lets the admin choose which holder population the cycle reaches: * ``all`` → no holder-presence filter (legacy default). * ``assigned`` → only ``AssetItem.assigned_to IS NOT NULL``. * ``idle`` → only ``AssetItem.assigned_to IS NULL``. These tests exercise ``AssetInventoryService._query_eligible_assets`` directly (the SQL the snapshot builds against) and assert that each mode produces exactly the rows the UI claims. """ from __future__ import annotations import uuid import pytest from sqlalchemy.ext.asyncio import AsyncSession from app.domains.asset.inventory_service import AssetInventoryService from app.domains.asset.models import AssetItem from app.identity.person_models import IdentityPerson async def _make_person(db: AsyncSession, email: str) -> IdentityPerson: p = IdentityPerson( id=uuid.uuid4(), work_email=email, display_name=email.split("@")[0], employment_status="active", source_system="successfactors", hris_person_id=f"sf-{uuid.uuid4().hex[:8]}", ) db.add(p) await db.commit() return p async def _make_asset( db: AsyncSession, *, tag: str, assigned_to: uuid.UUID | None, location: str = "北京", category: str = "laptop", status: str = "idle", ) -> AssetItem: a = AssetItem( asset_tag=tag, category=category, status=status, location=location, assigned_to=assigned_to, ) db.add(a) await db.commit() return a @pytest.mark.asyncio async def test_assignment_scope_all_returns_assigned_and_idle( db_session: AsyncSession, ): person = await _make_person(db_session, "alice@test.com") assigned = await _make_asset(db_session, tag="AS-1", assigned_to=person.id) idle = await _make_asset(db_session, tag="IDLE-1", assigned_to=None) svc = AssetInventoryService(db_session) rows = await svc._query_eligible_assets({"assignment_scope": "all"}) tags = {r.asset_tag for r in rows} assert assigned.asset_tag in tags assert idle.asset_tag in tags @pytest.mark.asyncio async def test_assignment_scope_assigned_excludes_idle( db_session: AsyncSession, ): person = await _make_person(db_session, "bob@test.com") assigned = await _make_asset(db_session, tag="AS-2", assigned_to=person.id) idle = await _make_asset(db_session, tag="IDLE-2", assigned_to=None) svc = AssetInventoryService(db_session) rows = await svc._query_eligible_assets( {"assignment_scope": "assigned"}, ) tags = {r.asset_tag for r in rows} assert assigned.asset_tag in tags assert idle.asset_tag not in tags @pytest.mark.asyncio async def test_assignment_scope_idle_excludes_assigned( db_session: AsyncSession, ): person = await _make_person(db_session, "carol@test.com") assigned = await _make_asset(db_session, tag="AS-3", assigned_to=person.id) idle = await _make_asset(db_session, tag="IDLE-3", assigned_to=None) svc = AssetInventoryService(db_session) rows = await svc._query_eligible_assets({"assignment_scope": "idle"}) tags = {r.asset_tag for r in rows} assert idle.asset_tag in tags assert assigned.asset_tag not in tags @pytest.mark.asyncio async def test_assignment_scope_combines_with_other_filters( db_session: AsyncSession, ): """``assignment_scope=idle`` AND ``locations=['北京']`` must both apply — narrow to idle laptops in 北京 only.""" person = await _make_person(db_session, "dora@test.com") # Idle in 北京 — should match. target = await _make_asset( db_session, tag="IDLE-BJ", assigned_to=None, location="北京", ) # Idle elsewhere — assignment_scope match, but location filtered out. elsewhere = await _make_asset( db_session, tag="IDLE-SH", assigned_to=None, location="上海", ) # Assigned in 北京 — location match, but assignment_scope filtered out. assigned_bj = await _make_asset( db_session, tag="AS-BJ", assigned_to=person.id, location="北京", ) svc = AssetInventoryService(db_session) rows = await svc._query_eligible_assets({ "assignment_scope": "idle", "locations": ["北京"], }) tags = {r.asset_tag for r in rows} assert target.asset_tag in tags assert elsewhere.asset_tag not in tags assert assigned_bj.asset_tag not in tags @pytest.mark.asyncio async def test_default_no_assignment_scope_means_all( db_session: AsyncSession, ): """A scope dict without ``assignment_scope`` (legacy cycles created before this PR) must behave the same as ``all`` — no surprise narrowing of historical cycles after the upgrade.""" person = await _make_person(db_session, "ed@test.com") a = await _make_asset(db_session, tag="LEG-AS", assigned_to=person.id) i = await _make_asset(db_session, tag="LEG-IDLE", assigned_to=None) svc = AssetInventoryService(db_session) rows = await svc._query_eligible_assets({}) # no key tags = {r.asset_tag for r in rows} assert a.asset_tag in tags assert i.asset_tag in tags