from __future__ import annotations import asyncio import uuid from contextlib import asynccontextmanager from decimal import Decimal import httpx import pytest from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from app.approval.models import ApprovalSubmissionIntent from app.core.exceptions import AppException from app.domains.cmdb import dns_request_service as dns_service from app.domains.cmdb import scope_request_service as scope_service from app.domains.cmdb.models import ( DNS_REQ_ACTION_CREATE, CmdbCloudScopeRequest, CmdbDnsRecord, CmdbDnsRecordRequest, CmdbDomain, ) from app.domains.cmdb.schemas import DnsRecordRequestCreate from app.identity.person_models import IdentityPerson from app.integrations.models import Integration class _Flow: external_code = "scope-approval-code" class _FakeClient: def __init__(self) -> None: self.created = 0 self.uuids: list[str] = [] async def get_approval_definition(self, approval_code: str) -> dict: return {"form": "[]"} async def resolve_user_id_by_email(self, email: str) -> str: return "feishu-user" async def create_instance( self, *, approval_code, user_id, form, instance_uuid=None, **kwargs, ) -> dict: assert instance_uuid self.created += 1 self.uuids.append(instance_uuid) return {"instance_code": "scope-instance"} async def find_instance_by_uuid(self, **kwargs) -> dict | None: return None class _ResponseLossClient(_FakeClient): async def create_instance( self, *, approval_code, user_id, form, instance_uuid=None, **kwargs, ) -> dict: assert instance_uuid self.created += 1 self.uuids.append(instance_uuid) if self.created == 1: raise httpx.ReadTimeout("provider accepted; response lost") raise AppException( code="FEISHU_API_ERROR", message="Feishu API error 60012: duplicate uuid", status_code=502, ) async def find_instance_by_uuid(self, **kwargs) -> dict: assert kwargs["instance_uuid"] == self.uuids[0] assert kwargs["end_time"] >= kwargs["start_time"] return { "instance_code": "scope-recovered-instance", "uuid": kwargs["instance_uuid"], } async def _seed_scope_subjects( db: AsyncSession, ) -> tuple[IdentityPerson, Integration]: person = IdentityPerson( id=uuid.uuid4(), source_system="test", hris_person_id=f"scope-{uuid.uuid4().hex[:8]}", display_name="Scope Requester", work_email=f"scope-{uuid.uuid4().hex[:8]}@test.com", employment_status="active", ) integration = Integration( id=uuid.uuid4(), kind="cloud", provider="aws", name=f"scope-{uuid.uuid4().hex[:8]}", slug=f"scope-{uuid.uuid4().hex[:8]}", enabled=True, is_primary=False, capabilities=[], ) db.add_all([person, integration]) await db.commit() await db.refresh(person, attribute_names=["org_memberships"]) await db.refresh(integration) return person, integration async def _submit_inner( service: scope_service.CloudScopeRequestService, *, person: IdentityPerson, integration: Integration, trace_id: str, ) -> CmdbCloudScopeRequest: return await service._submit_inner( requester=person, integration=integration, clean_name="durable-scope", clean_budget=Decimal("100.00"), clean_type="development", clean_description="durable intent regression", trace_id=trace_id, ) @pytest.mark.asyncio async def test_scope_response_loss_reuses_uuid_and_recovers_60012( db_session: AsyncSession, monkeypatch, ) -> None: person, integration = await _seed_scope_subjects(db_session) client = _ResponseLossClient() async def _flow(*args, **kwargs): return _Flow() monkeypatch.setattr(scope_service, "get_approval_flow", _flow) service = scope_service.CloudScopeRequestService(db_session) service._feishu_client = client with pytest.raises(AppException) as exc_info: await _submit_inner( service, person=person, integration=integration, trace_id="loss-1", ) assert exc_info.value.code == "APPROVAL_SUBMISSION_RESULT_UNKNOWN" row = await _submit_inner( service, person=person, integration=integration, trace_id="loss-2", ) assert row.feishu_instance_code == "scope-recovered-instance" assert client.created == 2 assert len(set(client.uuids)) == 1 intent = await db_session.scalar(select(ApprovalSubmissionIntent).where( ApprovalSubmissionIntent.scope.like("cmdb_scope:%"), )) assert intent is not None and intent.state == "completed" @pytest.mark.asyncio async def test_scope_process_exit_is_recovered_by_cron_without_second_create( db_session: AsyncSession, monkeypatch, ) -> None: person, integration = await _seed_scope_subjects(db_session) client = _FakeClient() async def _flow(*args, **kwargs): return _Flow() monkeypatch.setattr(scope_service, "get_approval_flow", _flow) original = scope_service.CloudScopeRequestService._finalize_submission_intent crashed = False async def _crash_once(self, intent_id, *, recovered): nonlocal crashed if not crashed: crashed = True raise SystemExit("simulated process exit") return await original(self, intent_id, recovered=recovered) monkeypatch.setattr( scope_service.CloudScopeRequestService, "_finalize_submission_intent", _crash_once, ) service = scope_service.CloudScopeRequestService(db_session) service._feishu_client = client with pytest.raises(SystemExit): await _submit_inner( service, person=person, integration=integration, trace_id="crash", ) summary = await service.reconcile_submission_intents(min_age_minutes=0) assert summary["reconciled"] == 1 assert client.created == 1 assert await db_session.scalar( select(func.count()).select_from(CmdbCloudScopeRequest) ) == 1 @pytest.mark.asyncio async def test_scope_concurrent_submit_creates_one_provider_instance( db_session: AsyncSession, monkeypatch, ) -> None: person, integration = await _seed_scope_subjects(db_session) client = _FakeClient() process_lock = asyncio.Lock() session_factory = async_sessionmaker( db_session.bind, expire_on_commit=False, autoflush=False, ) @asynccontextmanager async def _serialized_lock(*args, **kwargs): async with process_lock: yield "test-lock" async def _flow(*args, **kwargs): return _Flow() monkeypatch.setattr( "app.core.distributed_lock.distributed_lock", _serialized_lock, ) monkeypatch.setattr(scope_service, "get_approval_flow", _flow) async def _submit(trace_id: str): async with session_factory() as session: current_person = await session.get(IdentityPerson, person.id) current_integration = await session.get(Integration, integration.id) assert current_person is not None and current_integration is not None service = scope_service.CloudScopeRequestService(session) service._feishu_client = client return await service.submit( requester=current_person, integration_id=str(current_integration.id), name="durable-scope", description="durable intent regression", monthly_budget_usd=100.0, cloud_account_type="development", trace_id=trace_id, ) results = await asyncio.gather( _submit("concurrent-a"), _submit("concurrent-b"), return_exceptions=True, ) assert sum(isinstance(result, CmdbCloudScopeRequest) for result in results) == 1 errors = [result for result in results if isinstance(result, AppException)] assert len(errors) == 1 and errors[0].code == "CLOUD_SCOPE_REQUEST_DUPLICATE" assert client.created == 1 @pytest.mark.asyncio async def test_dns_concurrent_submit_returns_one_row_and_one_provider_instance( db_session: AsyncSession, monkeypatch, ) -> None: person = IdentityPerson( id=uuid.uuid4(), source_system="test", hris_person_id=f"dns-{uuid.uuid4().hex[:8]}", display_name="DNS Requester", work_email=f"dns-{uuid.uuid4().hex[:8]}@test.com", employment_status="active", ) integration = Integration( id=uuid.uuid4(), kind="cloud", provider="tencent", name=f"dns-{uuid.uuid4().hex[:8]}", slug=f"dns-{uuid.uuid4().hex[:8]}", enabled=True, is_primary=False, capabilities=["dns_records"], ) domain = CmdbDomain( id=uuid.uuid4(), integration_id=str(integration.id), domain_name="example.com", status="active", allow_dns_request=True, ) anchor = CmdbDnsRecord( integration_id=str(integration.id), zone_name="example.com", record_id="anchor", name="@", record_type="NS", value="ns1.example.com", ttl=3600, status="enable", ) db_session.add_all([person, integration]) await db_session.flush() db_session.add_all([domain, anchor]) await db_session.commit() person_id = person.id integration_id = integration.id client = _FakeClient() process_lock = asyncio.Lock() session_factory = async_sessionmaker( db_session.bind, expire_on_commit=False, autoflush=False, ) @asynccontextmanager async def _serialized_lock(*args, **kwargs): async with process_lock: yield "test-lock" async def _flow(*args, **kwargs): return _Flow() monkeypatch.setattr( "app.core.distributed_lock.distributed_lock", _serialized_lock, ) monkeypatch.setattr(dns_service, "get_approval_flow", _flow) body = DnsRecordRequestCreate( action=DNS_REQ_ACTION_CREATE, integration_id=integration_id, zone_name="example.com", name="api", record_type="A", value="10.0.0.8", ttl=600, purpose="concurrent durable intent regression", ) async def _submit(trace_id: str): async with session_factory() as session: current_person = await session.get(IdentityPerson, person_id) assert current_person is not None service = dns_service.DnsRequestService(session) service._feishu_client = client return await service.submit( body=body, requester=current_person, trace_id=trace_id, ip_address=None, user_agent=None, ) first, second = await asyncio.gather( _submit("dns-concurrent-a"), _submit("dns-concurrent-b"), ) assert first.id == second.id assert client.created == 1 async with session_factory() as verify: assert await verify.scalar( select(func.count()).select_from(CmdbDnsRecordRequest) ) == 1