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

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

323 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import datetime
from uuid import uuid4
import pytest
from sqlalchemy import create_engine, func, select
from sqlalchemy.orm import Session
from app.application.feishu.handlers.subscriptions import handle_subscription_command
from app.core.database import Base
from app.modules.feishu.constants import FeishuCommandName
from app.modules.feishu.service import FeishuService
from app.modules.feishu_users.constants import FeishuUserRole
from app.modules.feishu_users.models import FeishuUser
from app.modules.feishu_users.principal import FeishuPrincipal
from app.modules.subscriptions.constants import (
DAILY_DELIVERY_LIMIT_REACHED,
MAX_ACTIVE_SUBSCRIPTIONS,
PushDeliveryStatus,
PushSubscriptionStatus,
SubscriptionScheduleType,
SubscriptionTargetType,
)
from app.modules.subscriptions.models import PushDelivery, PushSubscription
def _user(
db: Session,
*,
suffix: str,
role: str = FeishuUserRole.USER,
) -> FeishuUser:
record = FeishuUser(
code=f"FSU-{suffix}",
tenant_key=f"tenant-{suffix}",
open_id=f"open-{suffix}",
role=role,
timezone="Asia/Shanghai",
)
db.add(record)
db.commit()
db.refresh(record)
return record
def _handle(
db: Session,
text: str,
principal: FeishuPrincipal,
) -> dict:
result = handle_subscription_command(
db,
FeishuService(db),
text,
principal,
False,
)
assert result is not None
return result
def test_create_private_subscription_replies_with_normalized_plan() -> None:
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
try:
with Session(engine) as db:
user = _user(db, suffix="private-command")
principal = FeishuPrincipal.from_user(
user,
chat_id="private-chat",
chat_type="p2p",
)
result = _handle(
db,
"订阅 每天 09:00请提醒我喝水",
principal,
)
subscription = db.scalar(select(PushSubscription))
assert subscription is not None
assert result["command"] == FeishuCommandName.SUBSCRIPTION_CREATE
assert "订阅已启用" in result["content"]
assert "计划:每天 09:00" in result["content"]
assert "下次执行:" in result["content"]
assert f"暂停订阅 {subscription.code}" in result["content"]
assert subscription.owner_id == user.id
assert subscription.target_type == SubscriptionTargetType.USER
assert subscription.target_id == user.open_id
assert subscription.prompt == "请提醒我:喝水"
finally:
engine.dispose()
def test_group_subscription_uses_current_verified_chat_and_requires_admin() -> None:
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
try:
with Session(engine) as db:
ordinary = _user(db, suffix="ordinary-group")
denied = _handle(
db,
"订阅 每周一 09:00群提醒",
FeishuPrincipal.from_user(
ordinary,
chat_id="verified-group",
chat_type="group",
),
)
assert "无权" in denied["content"]
assert db.scalar(select(func.count()).select_from(PushSubscription)) == 0
admin = _user(db, suffix="admin-group", role=FeishuUserRole.ADMIN)
accepted = _handle(
db,
"订阅 每周一 09:00群提醒",
FeishuPrincipal.from_user(
admin,
chat_id="verified-group",
chat_type="group",
),
)
subscription = db.scalar(select(PushSubscription))
assert accepted["command"] == FeishuCommandName.SUBSCRIPTION_CREATE
assert subscription is not None
assert subscription.target_type == SubscriptionTargetType.CHAT
assert subscription.target_id == "verified-group"
finally:
engine.dispose()
def test_list_pause_resume_and_cancel_subscription_commands() -> None:
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
try:
with Session(engine) as db:
user = _user(db, suffix="lifecycle-command")
principal = FeishuPrincipal.from_user(user, chat_type="p2p")
_handle(db, "订阅 每天 10:00生命周期测试", principal)
subscription = db.scalar(select(PushSubscription))
assert subscription is not None
listed = _handle(db, "我的订阅", principal)
assert listed["command"] == FeishuCommandName.SUBSCRIPTION_LIST
assert subscription.code in listed["content"]
assert "每天 10:00" in listed["content"]
paused = _handle(db, f"暂停订阅 {subscription.code}", principal)
db.refresh(subscription)
assert paused["command"] == FeishuCommandName.SUBSCRIPTION_PAUSE
assert subscription.status == PushSubscriptionStatus.PAUSED
resumed = _handle(db, f"恢复订阅 {subscription.code}", principal)
db.refresh(subscription)
assert resumed["command"] == FeishuCommandName.SUBSCRIPTION_RESUME
assert subscription.status == PushSubscriptionStatus.ACTIVE
assert "下次执行:" in resumed["content"]
cancelled = _handle(db, f"退订 {subscription.code}", principal)
db.refresh(subscription)
assert cancelled["command"] == FeishuCommandName.SUBSCRIPTION_CANCEL
assert subscription.status == PushSubscriptionStatus.CANCELLED
assert subscription.next_run_at is None
finally:
engine.dispose()
def test_timezone_and_quiet_hour_commands_update_current_user() -> None:
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
try:
with Session(engine) as db:
user = _user(db, suffix="settings-command")
principal = FeishuPrincipal.from_user(user, chat_type="p2p")
timezone_result = _handle(db, "设置时区 Asia/Tokyo", principal)
db.refresh(user)
assert timezone_result["command"] == FeishuCommandName.SUBSCRIPTION_TIMEZONE
assert user.timezone == "Asia/Tokyo"
quiet_result = _handle(db, "设置安静时段 22:00-07:00", principal)
db.refresh(user)
assert quiet_result["command"] == FeishuCommandName.SUBSCRIPTION_QUIET_HOURS
assert user.quiet_hours_start.strftime("%H:%M") == "22:00"
assert user.quiet_hours_end.strftime("%H:%M") == "07:00"
closed = _handle(db, "关闭安静时段", principal)
db.refresh(user)
assert closed["command"] == FeishuCommandName.SUBSCRIPTION_QUIET_HOURS
assert user.quiet_hours_start is None
assert user.quiet_hours_end is None
finally:
engine.dispose()
def test_invalid_schedule_and_capacity_error_create_no_partial_record() -> None:
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
try:
with Session(engine) as db:
user = _user(db, suffix="invalid-command")
principal = FeishuPrincipal.from_user(user, chat_type="p2p")
invalid = _handle(
db,
"订阅 每隔10分钟过于频繁",
principal,
)
assert "示例" not in invalid["content"]
assert "订阅 每天 09:00" in invalid["content"]
assert db.scalar(select(func.count()).select_from(PushSubscription)) == 0
for index in range(MAX_ACTIVE_SUBSCRIPTIONS):
db.add(
PushSubscription(
code=f"SUB-CAPACITY-{index}",
owner_id=user.id,
target_type=SubscriptionTargetType.USER,
target_id=user.open_id,
prompt=f"已有订阅 {index}",
schedule_type=SubscriptionScheduleType.DAILY,
schedule_config={"hour": 9, "minute": 0},
timezone=user.timezone,
next_run_at=datetime(2026, 7, 27, 1, 0),
status=PushSubscriptionStatus.ACTIVE,
consented_at=datetime(2026, 7, 26, 0, 0),
)
)
db.commit()
limited = _handle(
db,
"订阅 每天 11:00第 51 条",
principal,
)
assert "最多 50 个" in limited["content"]
assert (
db.scalar(select(func.count()).select_from(PushSubscription))
== MAX_ACTIVE_SUBSCRIPTIONS
)
finally:
engine.dispose()
def test_unrelated_text_is_not_claimed_by_subscription_handler() -> None:
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
try:
with Session(engine) as db:
user = _user(db, suffix="unrelated-command")
assert (
handle_subscription_command(
db,
FeishuService(db),
"今天怎么样",
FeishuPrincipal.from_user(user),
False,
)
is None
)
finally:
engine.dispose()
def test_list_explains_daily_delivery_limit_skip() -> None:
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
try:
with Session(engine) as db:
user = _user(db, suffix="delivery-note")
principal = FeishuPrincipal.from_user(user, chat_type="p2p")
_handle(db, "订阅 每天 10:00投递说明", principal)
subscription = db.scalar(select(PushSubscription))
assert subscription is not None
now = datetime(2026, 7, 26, 1, 0)
db.add(
PushDelivery(
code="DEL-DAILY-LIMIT",
subscription_id=subscription.id,
scheduled_for=now,
idempotency_key=uuid4().hex + uuid4().hex,
message_uuid=str(uuid4()),
status=PushDeliveryStatus.SKIPPED,
next_attempt_at=None,
last_error=DAILY_DELIVERY_LIMIT_REACHED,
created_at=now,
updated_at=now,
)
)
db.commit()
listed = _handle(db, "我的订阅", principal)
assert "每日最多 96 条限制" in listed["content"]
finally:
engine.dispose()
@pytest.mark.parametrize(
"command",
[
"设置时区 Invalid/Timezone",
"设置安静时段 25:00-07:00",
"订阅 每隔999999999999999999小时不会创建",
],
)
def test_invalid_settings_and_oversized_interval_return_command_error(
command: str,
) -> None:
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
try:
with Session(engine) as db:
user = _user(db, suffix=f"invalid-{abs(hash(command))}")
principal = FeishuPrincipal.from_user(user, chat_type="p2p")
result = _handle(db, command, principal)
assert "未执行" in result["content"] or "无法识别" in result["content"]
assert db.scalar(select(func.count()).select_from(PushSubscription)) == 0
finally:
engine.dispose()