"""Durable Feishu-create regressions for VPN and AI-tool applications.""" from __future__ import annotations import asyncio import uuid from contextlib import asynccontextmanager 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.ai_tool_application import service as ai_service from app.domains.ai_tool_application.models import AiToolApplication from app.domains.vpn_application import service as vpn_service from app.domains.vpn_application.models import VPN_NODES, VpnApplication from app.identity.person_models import IdentityPerson from tests.support.approval_intents import make_submission_intent_retries_due class _Flow: external_code = "approval-code" class _Provider: def __init__(self, *, lose_first_response: bool = False) -> None: self.lose_first_response = lose_first_response self.created = 0 self.uuids: list[str] = [] self.instance_code = f"instance-{uuid.uuid4()}" 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, *, instance_uuid=None, **kwargs) -> dict: assert instance_uuid self.created += 1 self.uuids.append(instance_uuid) if self.lose_first_response and self.created == 1: raise httpx.ReadTimeout("accepted; app_secret=must-not-persist") if self.lose_first_response: raise AppException( code="FEISHU_API_ERROR", message="Feishu API error 60012: duplicate uuid", status_code=502, ) return {"instance_code": self.instance_code} async def find_instance_by_uuid(self, *, instance_uuid, **kwargs) -> dict: assert instance_uuid == self.uuids[0] return {"instance_code": self.instance_code, "uuid": instance_uuid} async def _flow(*args, **kwargs): return _Flow() def _wire(kind: str, monkeypatch, provider: _Provider) -> None: if kind == "vpn": monkeypatch.setattr(vpn_service, "get_approval_flow", _flow) monkeypatch.setattr(vpn_service, "_build_vpn_form_payload", lambda *args: "[]") else: monkeypatch.setattr(ai_service, "get_approval_flow", _flow) monkeypatch.setattr(ai_service, "_build_ai_form_payload", lambda *args: "[]") def _service(kind: str, db: AsyncSession, provider: _Provider): if kind == "vpn": service = vpn_service.VpnApplicationService(db) else: service = ai_service.AiToolApplicationService(db) service._feishu_client = provider return service async def _submit(kind: str, service, requester: IdentityPerson, trace: str): if kind == "vpn": return await service.submit( requester=requester, vpn_node=VPN_NODES[0], usage_description="Overseas development service", resource_checklist="github.com", trace_id=trace, ) return await service.submit( requester=requester, ai_name="Security review tool", how_to_use=["API"], data_level=["L1"], purpose="Developer productivity", data_type="Public source code", privacy_policy="https://example.test/privacy", trace_id=trace, ) async def _requester(db: AsyncSession) -> IdentityPerson: person = IdentityPerson( id=uuid.uuid4(), source_system="successfactors", hris_person_id=f"sf-{uuid.uuid4().hex[:8]}", display_name="Durable intent requester", work_email=f"intent-{uuid.uuid4().hex[:8]}@test.com", employment_status="active", ) db.add(person) await db.commit() return person @pytest.mark.parametrize("kind", ["vpn", "ai"]) @pytest.mark.asyncio async def test_response_loss_retry_reuses_uuid_and_finalizes_once( kind: str, db_session: AsyncSession, monkeypatch, ) -> None: requester = await _requester(db_session) provider = _Provider(lose_first_response=True) _wire(kind, monkeypatch, provider) service = _service(kind, db_session, provider) with pytest.raises(AppException) as exc_info: await _submit(kind, service, requester, "trace-loss-1") assert exc_info.value.code == "APPROVAL_SUBMISSION_RESULT_UNKNOWN" intent = await db_session.scalar( select(ApprovalSubmissionIntent).where( ApprovalSubmissionIntent.applicant_id == requester.id, ) ) assert intent is not None assert intent.state == "unknown" assert "must-not-persist" not in (intent.last_error or "") row = await _submit(kind, service, requester, "trace-loss-2") assert row.feishu_instance_code == provider.instance_code assert provider.uuids == [intent.provider_request_uuid] * 2 await db_session.refresh(intent) assert intent.state == "completed" @pytest.mark.parametrize("kind", ["vpn", "ai"]) @pytest.mark.asyncio async def test_process_exit_after_provider_commit_finalizes_without_resubmit( kind: str, db_session: AsyncSession, monkeypatch, ) -> None: requester = await _requester(db_session) provider = _Provider() _wire(kind, monkeypatch, provider) service_class = ( vpn_service.VpnApplicationService if kind == "vpn" else ai_service.AiToolApplicationService ) original = service_class._finalize_submission_intent crashed = False async def _crash_once(self, intent_id, *, recovered): nonlocal crashed if not crashed: crashed = True raise SystemExit("process exited") return await original(self, intent_id, recovered=recovered) monkeypatch.setattr(service_class, "_finalize_submission_intent", _crash_once) service = _service(kind, db_session, provider) with pytest.raises(SystemExit): await _submit(kind, service, requester, "trace-crash-1") row = await _submit(kind, service, requester, "trace-crash-2") assert row.feishu_instance_code == provider.instance_code assert provider.created == 1 @pytest.mark.parametrize("kind", ["vpn", "ai"]) @pytest.mark.asyncio async def test_cron_recovers_unknown_without_user_retry( kind: str, db_session: AsyncSession, monkeypatch, ) -> None: requester = await _requester(db_session) provider = _Provider(lose_first_response=True) _wire(kind, monkeypatch, provider) service = _service(kind, db_session, provider) with pytest.raises(AppException): await _submit(kind, service, requester, "trace-cron") await make_submission_intent_retries_due(db_session) summary = await service.reconcile_submission_intents(min_age_minutes=0) assert summary == { "checked": 1, "reconciled": 1, "still_unknown": 0, "failed": 0, } assert provider.uuids[0] == provider.uuids[1] @pytest.mark.parametrize("kind", ["vpn", "ai"]) @pytest.mark.asyncio async def test_dual_session_submit_creates_one_provider_instance( kind: str, db_session: AsyncSession, monkeypatch, ) -> None: requester = await _requester(db_session) provider = _Provider() _wire(kind, monkeypatch, provider) process_lock = asyncio.Lock() factory = async_sessionmaker( db_session.bind, expire_on_commit=False, autoflush=False, ) @asynccontextmanager async def _lock(*args, **kwargs): async with process_lock: yield "test-lock" monkeypatch.setattr("app.core.distributed_lock.distributed_lock", _lock) async def _one(trace: str): async with factory() as session: person = await session.get(IdentityPerson, requester.id) assert person is not None return await _submit(kind, _service(kind, session, provider), person, trace) results = await asyncio.gather( _one("trace-a"), _one("trace-b"), return_exceptions=True, ) assert sum(isinstance(item, AppException) for item in results) == 1 assert provider.created == 1 model = VpnApplication if kind == "vpn" else AiToolApplication assert await db_session.scalar(select(func.count()).select_from(model)) == 1