"""Durable Feishu-create boundary for API-key applications.""" from __future__ import annotations import asyncio import uuid from datetime import date, timedelta from decimal import Decimal import httpx import pytest from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from sqlalchemy.orm import selectinload from app.approval.models import ApprovalRequest, ApprovalSubmissionIntent from app.core.exceptions import AppException from app.domains.apikey.models import ApiKeyAssignment, ApiKeyProvider from app.domains.apikey.schemas import ApiKeyRequestSubmit from app.domains.apikey.service import STATUS_REJECTED, ApiKeyService from app.domains.asset.models import EmployeeRequest from app.identity.person_models import IdentityPerson from tests.support.approval_intents import make_submission_intent_retries_due class _FeishuClient: def __init__(self, mode: str = "success") -> None: self.mode = mode self.created = 0 self.instance_uuids: list[str] = [] self.instance_code = f"api-instance-{uuid.uuid4().hex[:10]}" async def get_approval_definition(self, approval_code: str) -> dict: return {"form": "[]", "approval_name": "API key approval"} async def resolve_user_id_by_email(self, email: str) -> str: return "feishu-api-key-applicant" async def create_instance( self, *, approval_code, user_id, form, instance_uuid=None, **kwargs, ) -> dict: assert instance_uuid self.created += 1 self.instance_uuids.append(instance_uuid) if self.mode == "reject": raise AppException( code="FEISHU_REQUEST_REJECTED", message="provider rejected the approval form", status_code=400, ) if self.mode == "response_loss": 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, ) if self.mode == "unknown_then_reject": if self.created == 1: raise httpx.ReadTimeout("provider result unknown") raise AppException( code="FEISHU_REQUEST_REJECTED", message="provider definitely rejected the retry", status_code=400, ) return { "instance_code": self.instance_code, "instance_status": "PENDING", } async def find_instance_by_uuid( self, *, approval_code, instance_uuid, start_time, end_time, **kwargs, ) -> dict: assert instance_uuid == self.instance_uuids[0] return { "instance_code": self.instance_code, "instance_status": "PENDING", "uuid": instance_uuid, } class _SlowFeishuClient(_FeishuClient): async def create_instance(self, **kwargs) -> dict: await asyncio.sleep(0.05) return await super().create_instance(**kwargs) async def _entities( db: AsyncSession, ) -> tuple[IdentityPerson, ApiKeyProvider, ApiKeyRequestSubmit]: person = IdentityPerson( id=uuid.uuid4(), work_email=f"api-intent-{uuid.uuid4().hex[:8]}@test.local", display_name="API intent applicant", employment_status="active", source_system="test", hris_person_id=f"sf-{uuid.uuid4().hex[:8]}", ) provider = ApiKeyProvider( id=uuid.uuid4(), name=f"Test API provider {uuid.uuid4().hex[:8]}", description="", is_active=True, sort_order=0, ) db.add_all([person, provider]) await db.commit() body = ApiKeyRequestSubmit( provider_id=provider.id, project_name="durable-agent-project", reason="Test lossless Feishu submission", monthly_budget_usd=Decimal("20.00"), valid_from=date.today(), valid_until=date.today() + timedelta(days=30), answers={}, ) return person, provider, body async def _submit_inner( db: AsyncSession, *, person: IdentityPerson, provider: ApiKeyProvider, body: ApiKeyRequestSubmit, client: _FeishuClient, ) -> None: service = ApiKeyService(db) service._client = client await service._submit_api_key_request_inner( body=body, provider=provider, current_user=person, approval_code="API-KEY-CODE", trace_id="trace-api-key-intent", ip_address="127.0.0.1", user_agent="pytest", ) @pytest.mark.asyncio async def test_response_loss_60012_replay_reuses_uuid_and_domain_rows( db_session: AsyncSession, ) -> None: person, provider, body = await _entities(db_session) client = _FeishuClient("response_loss") with pytest.raises(AppException) as caught: await _submit_inner( db_session, person=person, provider=provider, body=body, client=client, ) assert caught.value.code == "APPROVAL_SUBMISSION_RESULT_UNKNOWN" assignment = (await db_session.scalars(select(ApiKeyAssignment))).one() await ApiKeyService(db_session)._resume_api_key_submission( assignment, current_user=person, provider=provider, client=client, ) await db_session.refresh(assignment) assert assignment.detail["feishu"]["instance_code"] == client.instance_code assert client.instance_uuids == [client.instance_uuids[0]] * 2 assert await db_session.scalar(select(func.count()).select_from(ApiKeyAssignment)) == 1 assert await db_session.scalar(select(func.count()).select_from(EmployeeRequest)) == 1 assert await db_session.scalar(select(func.count()).select_from(ApprovalRequest)) == 1 intent = (await db_session.scalars(select(ApprovalSubmissionIntent))).one() assert intent.state == "completed" assert intent.external_id == client.instance_code @pytest.mark.asyncio async def test_process_exit_after_provider_acceptance_finalizes_without_repost( db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: person, provider, body = await _entities(db_session) client = _FeishuClient() original = ApiKeyService._finalize_api_key_submission async def _exit(*args, **kwargs): raise SystemExit("worker terminated after provider acceptance") monkeypatch.setattr(ApiKeyService, "_finalize_api_key_submission", _exit) with pytest.raises(SystemExit): await _submit_inner( db_session, person=person, provider=provider, body=body, client=client, ) intent = (await db_session.scalars(select(ApprovalSubmissionIntent))).one() assert intent.state == "provider_created" assignment = (await db_session.scalars(select(ApiKeyAssignment))).one() monkeypatch.setattr(ApiKeyService, "_finalize_api_key_submission", original) await ApiKeyService(db_session)._resume_api_key_submission( assignment, current_user=person, provider=provider, client=client, ) assert client.created == 1 await db_session.refresh(intent) assert intent.state == "completed" @pytest.mark.asyncio async def test_scheduled_reconciler_recovers_unknown_create( db_session: AsyncSession, ) -> None: person, provider, body = await _entities(db_session) client = _FeishuClient("response_loss") with pytest.raises(AppException): await _submit_inner( db_session, person=person, provider=provider, body=body, client=client, ) await make_submission_intent_retries_due(db_session) summary = await ApiKeyService(db_session).reconcile_submission_intents( min_age_minutes=0, client=client, ) assert summary == { "checked": 1, "reconciled": 1, "still_unknown": 0, "failed": 0, } intent = (await db_session.scalars(select(ApprovalSubmissionIntent))).one() assert intent.state == "completed" assert client.instance_uuids == [client.instance_uuids[0]] * 2 @pytest.mark.asyncio async def test_definite_provider_rejection_is_terminal_and_not_pending( db_session: AsyncSession, ) -> None: person, provider, body = await _entities(db_session) client = _FeishuClient("reject") with pytest.raises(AppException) as caught: await _submit_inner( db_session, person=person, provider=provider, body=body, client=client, ) assert caught.value.code == "FEISHU_REQUEST_REJECTED" assignment = (await db_session.scalars(select(ApiKeyAssignment))).one() employee_request = (await db_session.scalars(select(EmployeeRequest))).one() intent = (await db_session.scalars(select(ApprovalSubmissionIntent))).one() assert assignment.status == STATUS_REJECTED assert employee_request.status == "rejected" assert intent.state == "failed" assert await db_session.scalar(select(func.count()).select_from(ApprovalRequest)) == 0 @pytest.mark.asyncio async def test_definite_rejection_during_unknown_replay_closes_local_pending( db_session: AsyncSession, ) -> None: person, provider, body = await _entities(db_session) client = _FeishuClient("unknown_then_reject") with pytest.raises(AppException): await _submit_inner( db_session, person=person, provider=provider, body=body, client=client, ) assignment = (await db_session.scalars(select(ApiKeyAssignment))).one() with pytest.raises(AppException) as caught: await ApiKeyService(db_session)._resume_api_key_submission( assignment, current_user=person, provider=provider, client=client, ) assert caught.value.code == "FEISHU_REQUEST_REJECTED" await db_session.refresh(assignment) employee_request = (await db_session.scalars(select(EmployeeRequest))).one() assert assignment.status == STATUS_REJECTED assert employee_request.status == "rejected" @pytest.mark.asyncio async def test_dual_session_submit_serializes_to_one_provider_instance( db_engine, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: person, provider, body = await _entities(db_session) client = _SlowFeishuClient() async def _flows(*, session=None): return {"api_key_request": "API-KEY-CODE"} monkeypatch.setattr("app.integrations.service.get_approval_flows_map", _flows) factory = async_sessionmaker( db_engine, class_=AsyncSession, expire_on_commit=False, ) async def _run() -> str: async with factory() as session: service = ApiKeyService(session) service._client = client session_person = await session.scalar( select(IdentityPerson) .where(IdentityPerson.id == person.id) .options(selectinload(IdentityPerson.user_roles)) ) assert session_person is not None try: await service.submit_api_key_request( body=body, current_user=session_person, trace_id=f"trace-{uuid.uuid4()}", ip_address=None, user_agent=None, ) return "created" except AppException as exc: return exc.code outcomes = await asyncio.gather(_run(), _run()) assert outcomes.count("created") == 1 assert next(outcome for outcome in outcomes if outcome != "created") in { "API_KEY_REQUEST_DUPLICATE", "API_KEY_REQUEST_IN_FLIGHT", } assert client.created == 1 assert await db_session.scalar(select(func.count()).select_from(ApiKeyAssignment)) == 1 assert await db_session.scalar(select(func.count()).select_from(ApprovalRequest)) == 1