```
feat: 添加飞书用户模块和订阅功能支持 - 新增feishu_users模块用于处理飞书用户身份验证和权限管理 - 新增subscriptions模块用于处理订阅相关功能 - 新增personalization模块用于个性化服务 - 在alembic迁移配置中注册新的模型模块 - 在API路由器中添加feishu_users和subscriptions路由 - 实现事件调度服务的改进,包括错误处理和状态更新优化 - 添加飞书命令处理的权限检查机制 - 实现飞书应用票据事件处理 - 改进审计日志记录功能 ```
This commit is contained in:
322
tests/test_feishu_subscription_commands.py
Normal file
322
tests/test_feishu_subscription_commands.py
Normal file
@@ -0,0 +1,322 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user