Files
company-ai-platform/tests/test_feishu_app_ticket.py
JiuContinent d7db84571d ```
feat: 添加飞书用户模块和订阅功能支持

- 新增feishu_users模块用于处理飞书用户身份验证和权限管理
- 新增subscriptions模块用于处理订阅相关功能
- 新增personalization模块用于个性化服务
- 在alembic迁移配置中注册新的模型模块
- 在API路由器中添加feishu_users和subscriptions路由
- 实现事件调度服务的改进,包括错误处理和状态更新优化
- 添加飞书命令处理的权限检查机制
- 实现飞书应用票据事件处理
- 改进审计日志记录功能
```
2026-07-27 08:02:17 +08:00

270 lines
9.1 KiB
Python

import json
import sys
from collections.abc import Iterator
from types import SimpleNamespace
from typing import Any
import pytest
from fastapi import HTTPException
from sqlalchemy import create_engine, func, select
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from app.application.feishu.events import FeishuEventService
from app.core.config import get_settings
from app.core.database import Base
from app.modules.audit.models import AuditLog
from app.modules.feishu import long_connection
from app.modules.feishu.app_tickets import FeishuAppTicketService
from app.modules.feishu.constants import FeishuEventSource
from app.modules.feishu.models import FeishuAppTicket, FeishuEventReceipt
@pytest.fixture
def session_factory(
monkeypatch: pytest.MonkeyPatch,
) -> Iterator[sessionmaker[Session]]:
monkeypatch.setenv("FEISHU_APP_ID", "cli-ticket-app")
monkeypatch.setenv("FEISHU_APP_SECRET", "ticket-app-secret")
monkeypatch.setenv("FEISHU_VERIFICATION_TOKEN", "ticket-token")
monkeypatch.setenv("FEISHU_USER_FEATURES_ENABLED", "false")
get_settings.cache_clear()
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(
engine,
tables=[
AuditLog.__table__,
FeishuEventReceipt.__table__,
FeishuAppTicket.__table__,
],
)
factory = sessionmaker(bind=engine, expire_on_commit=False)
try:
yield factory
finally:
Base.metadata.drop_all(
engine,
tables=[
FeishuAppTicket.__table__,
FeishuEventReceipt.__table__,
AuditLog.__table__,
],
)
engine.dispose()
get_settings.cache_clear()
def _v2_ticket_event(
event_id: str,
ticket: str,
*,
app_id: str = "cli-ticket-app",
) -> dict[str, Any]:
return {
"schema": "2.0",
"header": {
"event_id": event_id,
"event_type": "app_ticket",
"token": "ticket-token",
"app_id": app_id,
},
"event": {"app_ticket": ticket},
}
def _v1_ticket_event(
event_id: str,
ticket: str,
*,
app_id: str = "cli-ticket-app",
) -> dict[str, Any]:
return {
"ts": "1785081600.000",
"uuid": event_id,
"token": "ticket-token",
"type": "app_ticket",
"event": {
"app_id": app_id,
"app_ticket": ticket,
},
}
def test_verified_ticket_is_deduplicated_rotated_and_never_leaked(
session_factory: sessionmaker[Session],
) -> None:
first_ticket = "ticket-secret-first"
rotated_ticket = "ticket-secret-rotated"
with session_factory() as db:
service = FeishuEventService(db)
first = service._handle_verified_event(
_v2_ticket_event("ticket-event-1", first_ticket),
source=FeishuEventSource.WEBHOOK,
)
stored = db.scalar(select(FeishuAppTicket))
assert stored is not None
stored_id = stored.id
first_received_at = stored.received_at
assert FeishuAppTicketService(db).get_ticket("cli-ticket-app") == first_ticket
assert first_ticket not in json.dumps(first, ensure_ascii=False)
duplicate_payload = _v2_ticket_event(
"ticket-event-1",
"ticket-secret-duplicate-must-not-win",
)
duplicate = service._handle_verified_event(
duplicate_payload,
source=FeishuEventSource.LONG_CONNECTION,
)
assert duplicate["duplicate"] is True
assert FeishuAppTicketService(db).get_ticket("cli-ticket-app") == first_ticket
rotated = service._handle_verified_event(
_v2_ticket_event("ticket-event-2", rotated_ticket),
source=FeishuEventSource.LONG_CONNECTION,
)
db.expire_all()
current = db.scalar(select(FeishuAppTicket))
assert current is not None
assert current.id == stored_id
assert current.app_ticket == rotated_ticket
assert current.received_at >= first_received_at
assert db.scalar(select(func.count()).select_from(FeishuAppTicket)) == 1
assert db.scalar(select(func.count()).select_from(FeishuEventReceipt)) == 2
assert rotated_ticket not in json.dumps(rotated, ensure_ascii=False)
audits = list(db.execute(select(AuditLog)).scalars())
assert len(audits) == 2
serialized_audits = json.dumps(
[
{
"request": item.request_payload,
"response": item.response_payload,
"target": item.target_id,
}
for item in audits
],
ensure_ascii=False,
)
assert first_ticket not in serialized_audits
assert rotated_ticket not in serialized_audits
assert "ticket-secret-duplicate-must-not-win" not in serialized_audits
def test_only_verified_matching_app_ticket_events_can_write(
session_factory: sessionmaker[Session],
) -> None:
with session_factory() as db:
service = FeishuEventService(db)
unverified = _v2_ticket_event("unverified-ticket", "unverified-secret")
unverified["header"]["token"] = "invalid-token"
with pytest.raises(HTTPException) as unverified_error:
service.handle_event(unverified, source=FeishuEventSource.WEBHOOK)
assert unverified_error.value.status_code == 401
with pytest.raises(HTTPException) as mismatch_error:
service._handle_verified_event(
_v2_ticket_event(
"wrong-app-ticket",
"wrong-app-secret",
app_id="cli-other-app",
),
source=FeishuEventSource.WEBHOOK,
)
assert mismatch_error.value.status_code == 401
missing_ticket = _v2_ticket_event("missing-ticket", "")
with pytest.raises(HTTPException) as missing_error:
service._handle_verified_event(
missing_ticket,
source=FeishuEventSource.WEBHOOK,
)
assert missing_error.value.status_code == 400
not_ticket_event = _v2_ticket_event("ordinary-event", "must-not-store")
not_ticket_event["header"]["event_type"] = "im.message.receive_v1"
result = service._handle_verified_event(
not_ticket_event,
source=FeishuEventSource.WEBHOOK,
auto_reply=False,
)
assert result["handled"] is False
assert db.scalar(select(FeishuAppTicket)) is None
def test_v1_app_ticket_payload_uses_uuid_receipt(
session_factory: sessionmaker[Session],
) -> None:
ticket = "v1-ticket-secret"
with session_factory() as db:
result = FeishuEventService(db).handle_event(
_v1_ticket_event("v1-ticket-uuid", ticket),
source=FeishuEventSource.WEBHOOK,
)
receipt = db.scalar(select(FeishuEventReceipt))
assert result == {"ok": True, "handled": True}
assert receipt is not None
assert receipt.event_id == "v1-ticket-uuid"
assert receipt.event_key == "cli-ticket-app:app_ticket:v1-ticket-uuid"
assert FeishuAppTicketService(db).get_ticket("cli-ticket-app") == ticket
assert ticket not in json.dumps(result, ensure_ascii=False)
def test_long_connection_registers_custom_app_ticket_handler(
monkeypatch: pytest.MonkeyPatch,
) -> None:
registrations: dict[str, Any] = {}
class FakeBuilder:
def register_p2_im_message_receive_v1(self, handler: Any) -> "FakeBuilder":
registrations["message"] = handler
return self
def register_p1_customized_event(
self,
event_type: str,
handler: Any,
) -> "FakeBuilder":
registrations[event_type] = handler
return self
def build(self) -> "FakeBuilder":
return self
class FakeDispatcherHandler:
@staticmethod
def builder(encrypt_key: str, verification_token: str) -> FakeBuilder:
registrations["builder_args"] = (encrypt_key, verification_token)
return FakeBuilder()
class FakeClient:
def __init__(self, **kwargs: Any):
registrations["client_kwargs"] = kwargs
def start(self) -> None:
registrations["started"] = True
fake_lark = SimpleNamespace(
EventDispatcherHandler=FakeDispatcherHandler,
LogLevel=SimpleNamespace(WARNING="warning"),
ws=SimpleNamespace(Client=FakeClient),
)
monkeypatch.setitem(sys.modules, "lark_oapi", fake_lark)
monkeypatch.setenv("FEISHU_APP_ID", "cli-ticket-app")
monkeypatch.setenv("FEISHU_APP_SECRET", "ticket-app-secret")
monkeypatch.setenv("FEISHU_VERIFICATION_TOKEN", "ticket-token")
get_settings.cache_clear()
try:
long_connection.run_long_connection()
finally:
get_settings.cache_clear()
assert registrations["app_ticket"] is long_connection._handle_app_ticket_event
assert registrations["message"] is long_connection._handle_message_event
assert registrations["started"] is True