feat: 添加飞书用户模块和订阅功能支持 - 新增feishu_users模块用于处理飞书用户身份验证和权限管理 - 新增subscriptions模块用于处理订阅相关功能 - 新增personalization模块用于个性化服务 - 在alembic迁移配置中注册新的模型模块 - 在API路由器中添加feishu_users和subscriptions路由 - 实现事件调度服务的改进,包括错误处理和状态更新优化 - 添加飞书命令处理的权限检查机制 - 实现飞书应用票据事件处理 - 改进审计日志记录功能 ```
168 lines
5.1 KiB
Python
168 lines
5.1 KiB
Python
from collections.abc import Iterator
|
|
from datetime import timedelta
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from sqlalchemy import create_engine, func, select
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
from app.application.scheduling import create_scheduler
|
|
from app.core.config import get_settings
|
|
from app.core.database import Base
|
|
from app.core.utils.time import utc_now
|
|
from app.modules.feishu_users.models import FeishuUser
|
|
from app.modules.personalization.models import AIConversation, AIConversationMessage
|
|
from app.modules.personalization.services import ConversationService
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_settings() -> Iterator[None]:
|
|
get_settings.cache_clear()
|
|
yield
|
|
get_settings.cache_clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def session_factory() -> Iterator[sessionmaker[Session]]:
|
|
engine = create_engine(
|
|
"sqlite://",
|
|
connect_args={"check_same_thread": False},
|
|
poolclass=StaticPool,
|
|
)
|
|
Base.metadata.create_all(engine)
|
|
factory = sessionmaker(bind=engine, expire_on_commit=False)
|
|
try:
|
|
yield factory
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
def test_global_cleanup_removes_inactive_user_history(
|
|
session_factory: sessionmaker[Session],
|
|
) -> None:
|
|
with session_factory() as db:
|
|
expired = _conversation(db, "expired")
|
|
mixed = _conversation(db, "mixed")
|
|
fresh = _conversation(db, "fresh")
|
|
old_time = utc_now() - timedelta(days=31)
|
|
fresh_time = utc_now() - timedelta(days=29)
|
|
db.add_all(
|
|
[
|
|
AIConversationMessage(
|
|
conversation_id=expired.id,
|
|
role="user",
|
|
content="expired question",
|
|
created_at=old_time,
|
|
),
|
|
AIConversationMessage(
|
|
conversation_id=expired.id,
|
|
role="assistant",
|
|
content="expired answer",
|
|
created_at=old_time,
|
|
),
|
|
AIConversationMessage(
|
|
conversation_id=mixed.id,
|
|
role="user",
|
|
content="old mixed question",
|
|
created_at=old_time,
|
|
),
|
|
AIConversationMessage(
|
|
conversation_id=mixed.id,
|
|
role="assistant",
|
|
content="fresh mixed answer",
|
|
created_at=fresh_time,
|
|
),
|
|
AIConversationMessage(
|
|
conversation_id=fresh.id,
|
|
role="user",
|
|
content="fresh question",
|
|
created_at=fresh_time,
|
|
),
|
|
]
|
|
)
|
|
db.commit()
|
|
|
|
deleted = ConversationService(db).cleanup_expired_globally()
|
|
|
|
assert deleted == {
|
|
"conversation_messages": 3,
|
|
"conversations": 1,
|
|
}
|
|
assert db.get(AIConversation, expired.id) is None
|
|
assert db.get(AIConversation, mixed.id) is not None
|
|
assert db.get(AIConversation, fresh.id) is not None
|
|
assert db.scalar(
|
|
select(func.count()).select_from(AIConversationMessage)
|
|
) == 2
|
|
|
|
with session_factory() as verification_db:
|
|
assert verification_db.get(AIConversation, expired.id) is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("features_enabled", "job_expected"),
|
|
[(False, False), (True, True)],
|
|
)
|
|
def test_scheduler_wires_one_global_retention_job(
|
|
session_factory: sessionmaker[Session],
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
features_enabled: bool,
|
|
job_expected: bool,
|
|
) -> None:
|
|
monkeypatch.setenv(
|
|
"FEISHU_USER_FEATURES_ENABLED",
|
|
str(features_enabled).lower(),
|
|
)
|
|
monkeypatch.setattr("app.core.database.SessionLocal", session_factory)
|
|
get_settings.cache_clear()
|
|
app = FastAPI()
|
|
scheduler = create_scheduler(app)
|
|
|
|
job = scheduler.get_job("personalization_retention_cleanup")
|
|
|
|
assert (job is not None) is job_expected
|
|
if job is None:
|
|
return
|
|
assert job.trigger.interval == timedelta(minutes=1)
|
|
|
|
with session_factory() as db:
|
|
expired = _conversation(db, "scheduled")
|
|
db.add(
|
|
AIConversationMessage(
|
|
conversation_id=expired.id,
|
|
role="user",
|
|
content="expired scheduled message",
|
|
created_at=utc_now() - timedelta(days=31),
|
|
)
|
|
)
|
|
db.commit()
|
|
expired_id = expired.id
|
|
|
|
job.func()
|
|
|
|
assert app.state.last_personalization_retention_cleanup == {
|
|
"conversation_messages": 1,
|
|
"conversations": 1,
|
|
}
|
|
with session_factory() as db:
|
|
assert db.get(AIConversation, expired_id) is None
|
|
|
|
|
|
def _conversation(db: Session, suffix: str) -> AIConversation:
|
|
owner = FeishuUser(
|
|
code=f"FSU-{suffix}",
|
|
tenant_key=f"tenant-{suffix}",
|
|
open_id=f"open-{suffix}",
|
|
)
|
|
db.add(owner)
|
|
db.flush()
|
|
conversation = AIConversation(
|
|
owner_id=owner.id,
|
|
chat_type="private",
|
|
chat_key=f"chat-{suffix}",
|
|
)
|
|
db.add(conversation)
|
|
db.flush()
|
|
return conversation
|