import asyncio import os import certifi import pytest from app.services.integrations.feishu_integration_service.mixin_config import FeishuConfigMixin from app.services.integrations.feishu_long_connection_core.mixin_lifecycle_connection import ( LongConnectionLifecycleConnectionMixin, ) from app.services.integrations.feishu_long_connection_core.mixin_lifecycle_control import ( LongConnectionLifecycleControlMixin, ) from app.services.integrations.feishu_long_connection_core.mixin_runtime import ( LongConnectionRuntimeMixin, ) from app.services.integrations.feishu_long_connection_core.shared import ( ensure_feishu_tls_trust_bundle, ) @pytest.fixture(autouse=True) def _clear_tls_env(monkeypatch: pytest.MonkeyPatch) -> None: for key in ( "FEISHU_LONG_CONNECTION_SSL_CERT_FILE", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", ): monkeypatch.delenv(key, raising=False) def test_ensure_feishu_tls_trust_bundle_defaults_to_certifi(monkeypatch: pytest.MonkeyPatch) -> None: bundle = ensure_feishu_tls_trust_bundle() assert bundle["bundle_path"] == certifi.where() assert bundle["source"] == "certifi" assert os.environ["SSL_CERT_FILE"] == certifi.where() assert os.environ["REQUESTS_CA_BUNDLE"] == certifi.where() assert os.environ["CURL_CA_BUNDLE"] == certifi.where() def test_ensure_feishu_tls_trust_bundle_honors_explicit_override(monkeypatch: pytest.MonkeyPatch) -> None: override_path = "/tmp/custom-feishu-ca.pem" monkeypatch.setenv("FEISHU_LONG_CONNECTION_SSL_CERT_FILE", override_path) bundle = ensure_feishu_tls_trust_bundle() assert bundle["bundle_path"] == override_path assert bundle["source"] == "FEISHU_LONG_CONNECTION_SSL_CERT_FILE" assert os.environ["SSL_CERT_FILE"] == override_path assert os.environ["REQUESTS_CA_BUNDLE"] == override_path assert os.environ["CURL_CA_BUNDLE"] == override_path class _DummyLongConnectionManager(LongConnectionLifecycleConnectionMixin): def __init__(self) -> None: self._boot_lock = asyncio.Lock() self._queue = asyncio.Queue() self._running = True self._thread = None self._client = None self._sdk_available = True self._configured = False self._event_mode = "long_connection" self._tenant_id = 1 self._last_error = None self._last_error_code = None self._last_error_retryable = False self._last_received_at = None self._last_received_message_id = None self._last_event_at = None self._last_auth_at = None self._last_connect_at = None self._last_disconnect_at = None self._restart_attempts = 0 self._next_retry_at = 0.0 self._stopping = False self._max_retry_attempts = 3 self._retry_base_seconds = 1.0 async def _load_active_config(self) -> dict[str, object] | None: return { "tenant_id": 1, "app_id": "app-id", "app_secret": "app-secret", "encrypt_key": "encrypt-key", "verification_token": "verification-token", } async def _preflight_auth(self, *, tenant_id: int) -> None: # noqa: ARG002 return None def _start_ws_thread( self, app_id: str, app_secret: str, *, encrypt_key: str = "", verification_token: str = "", ) -> None: self.started_with = (app_id, app_secret, encrypt_key, verification_token) class _FakeResult: def __init__(self, value) -> None: self._value = value def scalar_one_or_none(self): return self._value class _FakeDB: def __init__(self, integration) -> None: self._integration = integration self.committed = False async def execute(self, query): # noqa: ARG002 return _FakeResult(self._integration) async def commit(self) -> None: self.committed = True @pytest.mark.asyncio async def test_feishu_config_load_preserves_event_credentials() -> None: integration = type( "IntegrationRow", (), { "config": { "app_id": "app-id", "app_secret": "app-secret", "event_mode": "long_connection", "encrypt_key": "encrypt-key", "verification_token": "verification-token", }, "tenant_id": 1, }, )() service = FeishuConfigMixin(_FakeDB(integration)) await service.load_config(tenant_id=1) assert service.app_id == "app-id" assert service.app_secret == "app-secret" assert service.encrypt_key == "encrypt-key" assert service.verification_token == "verification-token" assert service.event_mode == "long_connection" @pytest.mark.asyncio async def test_feishu_config_save_preserves_existing_event_credentials() -> None: integration = type( "IntegrationRow", (), { "config": { "app_id": "old-app", "app_secret": "old-secret", "event_mode": "long_connection", "encrypt_key": "existing-encrypt-key", "verification_token": "existing-verification-token", }, "tenant_id": 1, "is_active": True, }, )() service = FeishuConfigMixin(_FakeDB(integration)) await service.save_config( app_id="new-app", app_secret="new-secret", encrypt_key="", verification_token="", event_mode="long_connection", tenant_id=1, ) assert integration.config["app_id"] == "new-app" assert integration.config["app_secret"] == "new-secret" assert integration.config["encrypt_key"] == "existing-encrypt-key" assert integration.config["verification_token"] == "existing-verification-token" assert service.encrypt_key == "existing-encrypt-key" assert service.verification_token == "existing-verification-token" assert service.app_id == "new-app" assert service.app_secret == "new-secret" assert service.event_mode == "long_connection" assert service.db.committed is True @pytest.mark.asyncio async def test_ensure_connection_bootstraps_tls_bundle_before_ws_start() -> None: manager = _DummyLongConnectionManager() started = await manager._ensure_connection(reason="startup") assert started is True assert manager.started_with == ( "app-id", "app-secret", "encrypt-key", "verification-token", ) assert os.environ["SSL_CERT_FILE"] == certifi.where() assert os.environ["REQUESTS_CA_BUNDLE"] == certifi.where() assert os.environ["CURL_CA_BUNDLE"] == certifi.where() @pytest.mark.asyncio async def test_long_connection_status_tracks_received_and_processed_times() -> None: class _RuntimeDummy(LongConnectionRuntimeMixin): def __init__(self) -> None: self._running = True self._loop = asyncio.get_running_loop() self._queue = asyncio.Queue() self._tenant_id = 1 self._last_received_at = None self._last_received_message_id = None self._last_event_at = None manager = _RuntimeDummy() manager._loop = asyncio.get_running_loop() manager._on_message_event( { "header": {"event_type": "im.message.receive_v1"}, "event": {"message": {"message_id": "msg-123"}}, } ) await asyncio.sleep(0) assert manager._last_received_message_id == "msg-123" assert manager._last_received_at is not None assert manager._queue.qsize() == 1 lifecycle_manager = type("StatusOnly", (), {})() lifecycle_manager._running = True lifecycle_manager._tenant_id = 1 lifecycle_manager._event_mode = "long_connection" lifecycle_manager._sdk_available = True lifecycle_manager._configured = True lifecycle_manager._worker_task = None lifecycle_manager._supervisor_task = None lifecycle_manager._thread = None lifecycle_manager._last_error = None lifecycle_manager._last_error_code = None lifecycle_manager._last_error_retryable = False lifecycle_manager._last_received_at = manager._last_received_at lifecycle_manager._last_received_message_id = manager._last_received_message_id lifecycle_manager._last_event_at = None lifecycle_manager._last_auth_at = None lifecycle_manager._last_connect_at = None lifecycle_manager._last_disconnect_at = None lifecycle_manager._restart_attempts = 0 lifecycle_manager._next_retry_at = 0.0 lifecycle_manager._max_retry_attempts = 3 lifecycle_manager._retry_base_seconds = 1.0 status = LongConnectionLifecycleControlMixin.status(lifecycle_manager) assert status["last_received_message_id"] == "msg-123" assert status["last_received_at"] == manager._last_received_at assert status["last_event_at"] is None