265 lines
8.8 KiB
Python
265 lines
8.8 KiB
Python
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
|