"""Server 负责人 defaults to its cloud subscription owner (live-resolved). A server with no explicit ``owner_person_id`` inherits the owner of its subscription (scope). Resolution is live/display-only — nothing is written to ``cmdb_servers.owner_person_id`` (so the semiannual owner-confirmation cron, which keys on a stored owner, never fires for inherited owners). Covers the two attribution links (GCP raw._project_id, cost-facts) + the explicit-owner-wins and API-surfacing behaviour. """ from __future__ import annotations import uuid from datetime import UTC, date, datetime, timedelta import pytest from httpx import AsyncClient from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.domains.cmdb.models import ( CmdbCloudScope, CmdbCostFact, CmdbServer, ) from app.domains.cmdb.service import resolve_inherited_server_owners from app.identity.person_models import IdentityPerson from app.integrations.models import Integration async def _integration(db, provider="aws"): integ = Integration( id=uuid.uuid4(), kind="cloud", provider=provider, name=f"{provider}-x", slug=f"{provider}-{uuid.uuid4().hex[:6]}", enabled=True, is_primary=False, capabilities=["servers"], ) db.add(integ) await db.flush() return integ async def _person(db, email): 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.flush() return p async def _scope(db, integ, owner, **kw): s = CmdbCloudScope( id=uuid.uuid4(), integration_id=str(integ.id), scope_type=kw.pop("scope_type", "aws_account"), external_id=kw.pop("external_id", f"acct-{uuid.uuid4().hex[:8]}"), display_name=kw.pop("display_name", "sub"), owner_person_id=str(owner.id) if owner else None, ) db.add(s) await db.flush() return s async def _server(db, integ, **kw): now = datetime.now(UTC) s = CmdbServer( id=uuid.uuid4(), instance_id=kw.pop("instance_id", f"i-{uuid.uuid4().hex[:10]}"), name=kw.pop("name", "srv"), status="running", region="ap-southeast-1", integration_id=str(integ.id), first_seen_at=now - timedelta(days=100), last_synced_at=now, **kw, ) db.add(s) await db.flush() return s @pytest.mark.asyncio async def test_inherit_via_cost_facts(db_session: AsyncSession): integ = await _integration(db_session) owner = await _person(db_session, "scopeowner@pingcap.com") scope = await _scope(db_session, integ, owner) srv = await _server(db_session, integ) # no owner db_session.add(CmdbCostFact( fact_key=uuid.uuid4().hex, date=date.today(), integration_id=str(integ.id), scope_id=str(scope.id), resource_id=srv.instance_id, resource_type="Instance", service="AmazonEC2", amount_usd=5, currency="USD", )) await db_session.flush() out = await resolve_inherited_server_owners(db_session, [srv]) assert out[str(srv.id)]["person_id"] == str(owner.id) assert out[str(srv.id)]["scope_id"] == str(scope.id) @pytest.mark.asyncio async def test_inherit_via_gcp_project(db_session: AsyncSession): integ = await _integration(db_session, provider="gcp") owner = await _person(db_session, "gcpowner@pingcap.com") scope = await _scope( db_session, integ, owner, scope_type="gcp_project", external_id="proj-42", ) srv = await _server(db_session, integ, raw_data={"_project_id": "proj-42"}) out = await resolve_inherited_server_owners(db_session, [srv]) assert out[str(srv.id)]["person_id"] == str(owner.id) assert out[str(srv.id)]["scope_id"] == str(scope.id) @pytest.mark.asyncio async def test_explicit_owner_not_inherited(db_session: AsyncSession): integ = await _integration(db_session) owner = await _person(db_session, "scopeowner2@pingcap.com") explicit = await _person(db_session, "explicit@pingcap.com") scope = await _scope(db_session, integ, owner) srv = await _server(db_session, integ, owner_person_id=str(explicit.id)) db_session.add(CmdbCostFact( fact_key=uuid.uuid4().hex, date=date.today(), integration_id=str(integ.id), scope_id=str(scope.id), resource_id=srv.instance_id, resource_type="Instance", service="AmazonEC2", amount_usd=5, currency="USD", )) await db_session.flush() # Explicit owner → not a candidate for inheritance at all. out = await resolve_inherited_server_owners(db_session, [srv]) assert str(srv.id) not in out @pytest.mark.asyncio async def test_no_scope_owner_no_inherit(db_session: AsyncSession): integ = await _integration(db_session) scope = await _scope(db_session, integ, None) # scope has no owner srv = await _server(db_session, integ) db_session.add(CmdbCostFact( fact_key=uuid.uuid4().hex, date=date.today(), integration_id=str(integ.id), scope_id=str(scope.id), resource_id=srv.instance_id, resource_type="Instance", service="AmazonEC2", amount_usd=5, currency="USD", )) await db_session.flush() out = await resolve_inherited_server_owners(db_session, [srv]) assert str(srv.id) not in out @pytest.mark.asyncio async def test_detail_endpoint_surfaces_inherited_owner( client: AsyncClient, db_session: AsyncSession, cloud_admin_headers, ): integ = await _integration(db_session) owner = await _person(db_session, "detailowner@pingcap.com") scope = await _scope(db_session, integ, owner, display_name="prod-sub") srv = await _server(db_session, integ) db_session.add(CmdbCostFact( fact_key=uuid.uuid4().hex, date=date.today(), integration_id=str(integ.id), scope_id=str(scope.id), resource_id=srv.instance_id, resource_type="Instance", service="AmazonEC2", amount_usd=5, currency="USD", )) await db_session.commit() r = await client.get( f"/api/v1/cmdb/servers/{srv.id}", headers=cloud_admin_headers, ) assert r.status_code == 200 d = r.json()["data"] # Stored owner stays null; inherited owner is surfaced separately. assert d["owner_person_id"] is None assert d["owner_inherited"] is True assert d["effective_owner_person_id"] == str(owner.id) assert d["inherited_from_scope_name"] == "prod-sub" # And the DB was NOT mutated — no owner written (no cron trigger). fresh = ( await db_session.execute( select(CmdbServer).where(CmdbServer.id == srv.id), ) ).scalar_one() assert fresh.owner_person_id is None