feat(feishu): 添加飞书入站事件inbox和混合数据库协调功能 - 实现飞书入站事件持久化inbox机制,支持状态管理、租约锁定和重试退避 - 添加混合数据库基线协调工具,确保平台PostgreSQL结构安全对齐 - 增加运行组件心跳检测和readiness就绪检查机制 - 实现app_ticket事件的安全轮换和验证处理 - 添加生产环境运行编排和fail-closed安全机制 - 支持webhook快速确认和长连接独立进程处理 - 完善个人数据擦除时的待处理事件清理功能 ```
227 lines
7.1 KiB
Python
227 lines
7.1 KiB
Python
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import create_engine, text
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
from app.core.config import get_settings
|
|
from app.core.database import Base, get_db
|
|
from app.core.database.migrations import expected_alembic_heads
|
|
from app.modules.feishu import long_connection
|
|
from app.modules.observability.constants import (
|
|
HeartbeatComponent,
|
|
HeartbeatStatus,
|
|
ObservabilityKey,
|
|
ObservabilityStatus,
|
|
)
|
|
from app.modules.observability.routes import router as observability_router
|
|
from app.modules.observability.service import ObservabilityService
|
|
|
|
|
|
def test_connection_heartbeat_tracks_disconnect_and_recovery(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
client = SimpleNamespace(_conn=None)
|
|
statuses: list[str] = []
|
|
|
|
class FakeStopEvent:
|
|
def __init__(self) -> None:
|
|
self.wait_count = 0
|
|
|
|
def is_set(self) -> bool:
|
|
return self.wait_count >= 3
|
|
|
|
def wait(self, timeout: float) -> None:
|
|
assert timeout == 1
|
|
self.wait_count += 1
|
|
if self.wait_count == 1:
|
|
client._conn = SimpleNamespace(
|
|
state=SimpleNamespace(name="OPEN"),
|
|
)
|
|
elif self.wait_count == 2:
|
|
client._conn = SimpleNamespace(
|
|
state=SimpleNamespace(name="CLOSED"),
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
long_connection,
|
|
"_record_runtime_heartbeat",
|
|
lambda _instance_id, status_value: statuses.append(status_value),
|
|
)
|
|
|
|
long_connection._heartbeat_loop(
|
|
FakeStopEvent(), # type: ignore[arg-type]
|
|
"feishu-events-test",
|
|
30,
|
|
client,
|
|
)
|
|
|
|
assert statuses == [
|
|
HeartbeatStatus.DEGRADED,
|
|
HeartbeatStatus.OK,
|
|
HeartbeatStatus.DEGRADED,
|
|
]
|
|
|
|
|
|
def test_connection_adapter_supports_sdk_state_variants() -> None:
|
|
assert long_connection._sdk_connection_is_open(SimpleNamespace(_conn=None)) is False
|
|
unknown_client = SimpleNamespace(_conn=SimpleNamespace())
|
|
assert long_connection._sdk_connection_is_open(unknown_client) is False
|
|
assert (
|
|
long_connection._connection_heartbeat_status(unknown_client)
|
|
== HeartbeatStatus.DEGRADED
|
|
)
|
|
assert (
|
|
long_connection._sdk_connection_is_open(
|
|
SimpleNamespace(_conn=SimpleNamespace(closed=False))
|
|
)
|
|
is True
|
|
)
|
|
assert (
|
|
long_connection._sdk_connection_is_open(
|
|
SimpleNamespace(_conn=SimpleNamespace(open=False))
|
|
)
|
|
is False
|
|
)
|
|
assert (
|
|
long_connection._sdk_connection_is_open(
|
|
SimpleNamespace(
|
|
_conn=SimpleNamespace(state=SimpleNamespace(name="OPEN"))
|
|
)
|
|
)
|
|
is True
|
|
)
|
|
assert (
|
|
long_connection._sdk_connection_is_open(
|
|
SimpleNamespace(
|
|
_conn=SimpleNamespace(state=SimpleNamespace(name="CLOSING"))
|
|
)
|
|
)
|
|
is False
|
|
)
|
|
|
|
|
|
def test_readiness_returns_503_while_disconnected_and_recovers(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setenv("APP_ENV", "test")
|
|
monkeypatch.setenv("FEISHU_EVENT_TRANSPORT", "long_connection")
|
|
monkeypatch.setenv("FEISHU_APP_ID", "app-id")
|
|
monkeypatch.setenv("FEISHU_APP_SECRET", "app-secret")
|
|
monkeypatch.setenv("FEISHU_USER_FEATURES_ENABLED", "false")
|
|
monkeypatch.setenv("SCHEDULER_ENABLED", "false")
|
|
monkeypatch.setenv("TASK_QUEUE_ENABLED", "false")
|
|
get_settings.cache_clear()
|
|
|
|
engine = create_engine(
|
|
"sqlite://",
|
|
connect_args={"check_same_thread": False},
|
|
poolclass=StaticPool,
|
|
)
|
|
Base.metadata.create_all(engine)
|
|
with engine.begin() as connection:
|
|
connection.execute(
|
|
text("create table alembic_version (version_num varchar(32))")
|
|
)
|
|
connection.execute(
|
|
text("insert into alembic_version (version_num) values (:revision)"),
|
|
{"revision": expected_alembic_heads()[0]},
|
|
)
|
|
session_factory = sessionmaker(bind=engine, expire_on_commit=False)
|
|
app = FastAPI()
|
|
app.include_router(observability_router, prefix="/api/v1")
|
|
|
|
def override_get_db() -> Any:
|
|
db = session_factory()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
app.dependency_overrides[get_db] = override_get_db
|
|
client = TestClient(app)
|
|
try:
|
|
with Session(engine) as db:
|
|
ObservabilityService(db).record_heartbeat(
|
|
HeartbeatComponent.FEISHU_EVENTS,
|
|
"feishu-events-test",
|
|
status_value=HeartbeatStatus.DEGRADED,
|
|
)
|
|
|
|
disconnected = client.get("/api/v1/health/ready")
|
|
|
|
assert disconnected.status_code == 503
|
|
disconnected_check = disconnected.json()[ObservabilityKey.CHECKS][
|
|
ObservabilityKey.FEISHU_EVENTS
|
|
]
|
|
assert (
|
|
disconnected_check[ObservabilityKey.STATUS]
|
|
== ObservabilityStatus.DEGRADED
|
|
)
|
|
assert disconnected_check["reason"] == "heartbeat_degraded"
|
|
|
|
with Session(engine) as db:
|
|
ObservabilityService(db).record_heartbeat(
|
|
HeartbeatComponent.FEISHU_EVENTS,
|
|
"feishu-events-test",
|
|
status_value=HeartbeatStatus.OK,
|
|
)
|
|
|
|
recovered = client.get("/api/v1/health/ready")
|
|
|
|
assert recovered.status_code == 200
|
|
recovered_check = recovered.json()[ObservabilityKey.CHECKS][
|
|
ObservabilityKey.FEISHU_EVENTS
|
|
]
|
|
assert recovered_check[ObservabilityKey.STATUS] == ObservabilityStatus.OK
|
|
assert recovered_check["reason"] is None
|
|
finally:
|
|
client.close()
|
|
engine.dispose()
|
|
get_settings.cache_clear()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("configured_identities", "expected_reason"),
|
|
[
|
|
([""], "admin_identities_missing"),
|
|
(["not-an-identity"], "admin_identities_invalid"),
|
|
],
|
|
)
|
|
def test_production_readiness_parses_admin_identities_without_exposing_them(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
configured_identities: list[str],
|
|
expected_reason: str,
|
|
) -> None:
|
|
sensitive_value = configured_identities[0]
|
|
settings = SimpleNamespace(
|
|
app_env="production",
|
|
feishu_user_features_enabled=True,
|
|
feishu_admin_identities=configured_identities,
|
|
feishu_event_transport="long_connection",
|
|
feishu_app_id="app-id",
|
|
feishu_app_secret="app-secret",
|
|
feishu_app_type="self",
|
|
feishu_default_tenant_key=None,
|
|
)
|
|
monkeypatch.setattr(
|
|
"app.modules.observability.service.get_settings",
|
|
lambda: settings,
|
|
)
|
|
engine = create_engine("sqlite://")
|
|
Base.metadata.create_all(engine)
|
|
try:
|
|
with Session(engine) as db:
|
|
result = ObservabilityService(db)._feishu_subscriptions_check()
|
|
|
|
assert result[ObservabilityKey.STATUS] == ObservabilityStatus.DEGRADED
|
|
assert expected_reason in result["reasons"]
|
|
if sensitive_value:
|
|
assert sensitive_value not in str(result)
|
|
finally:
|
|
engine.dispose()
|