mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
fix(agent): bind channel admin identity (#6294)
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.helper.agent import matches_channel_admin
|
||||
from app.modules.discord import DiscordModule
|
||||
from app.modules.feishu.feishu import Feishu
|
||||
from app.modules.qqbot import QQBotModule
|
||||
from app.modules.slack import SlackModule
|
||||
from app.modules.synologychat import SynologyChatModule
|
||||
from app.modules.telegram import TelegramModule
|
||||
from app.modules.vocechat import VoceChatModule
|
||||
from app.modules.wechat import WechatModule
|
||||
from app.modules.wechatclawbot import WechatClawBotModule
|
||||
|
||||
|
||||
def _parse_module_message(module, *, config: dict, body, client=None, form=None):
|
||||
"""使用隔离的渠道配置调用消息解析器。"""
|
||||
client = client or SimpleNamespace()
|
||||
with patch.object(
|
||||
module,
|
||||
"get_config",
|
||||
return_value=SimpleNamespace(name="channel-test", config=config),
|
||||
), patch.object(module, "get_instance", return_value=client):
|
||||
return module.message_parser(
|
||||
source="channel-test",
|
||||
body=body,
|
||||
form=form or {},
|
||||
args={},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "principal_ids", "expected"),
|
||||
[
|
||||
({"ADMINS": " user-1, 42 "}, ("user-1",), True),
|
||||
({"ADMINS": "user-1,42"}, ("user-2", 7), False),
|
||||
({"ADMINS": ""}, ("user-1",), False),
|
||||
({}, ("user-1",), False),
|
||||
(None, ("user-1",), False),
|
||||
],
|
||||
)
|
||||
def test_matches_channel_admin_uses_nonempty_stable_principal_set(
|
||||
config, principal_ids, expected
|
||||
):
|
||||
assert matches_channel_admin(config, "ADMINS", *principal_ids) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message_kind", ["message", "callback"])
|
||||
def test_telegram_uses_user_id_not_same_named_username(message_kind):
|
||||
module = TelegramModule()
|
||||
client = SimpleNamespace(bot_username=None, answer_callback_query=Mock())
|
||||
if message_kind == "message":
|
||||
payload = {
|
||||
"message_id": 10,
|
||||
"from": {"id": 10002, "username": "admin"},
|
||||
"chat": {"id": 10002},
|
||||
"text": "hello",
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"callback_query": {
|
||||
"id": "callback-1",
|
||||
"from": {"id": 10002, "username": "admin"},
|
||||
"data": "choice:1",
|
||||
"message": {"message_id": 10, "chat": {"id": 10002}},
|
||||
}
|
||||
}
|
||||
|
||||
message = _parse_module_message(
|
||||
module,
|
||||
config={"TELEGRAM_ADMINS": "admin,10001"},
|
||||
body=json.dumps(payload),
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message.userid == 10002
|
||||
assert message.username == "admin"
|
||||
assert message.is_channel_admin is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message_kind", ["message", "callback"])
|
||||
def test_telegram_uses_stable_user_id_for_admin(message_kind):
|
||||
module = TelegramModule()
|
||||
client = SimpleNamespace(bot_username=None, answer_callback_query=Mock())
|
||||
if message_kind == "message":
|
||||
payload = {
|
||||
"message_id": 10,
|
||||
"from": {"id": 10001, "username": "renamed-user"},
|
||||
"chat": {"id": 10001},
|
||||
"text": "hello",
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"callback_query": {
|
||||
"id": "callback-1",
|
||||
"from": {"id": 10001, "username": "renamed-user"},
|
||||
"data": "choice:1",
|
||||
"message": {"message_id": 10, "chat": {"id": 10001}},
|
||||
}
|
||||
}
|
||||
|
||||
message = _parse_module_message(
|
||||
module,
|
||||
config={"TELEGRAM_ADMINS": "10001"},
|
||||
body=json.dumps(payload),
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message.userid == 10001
|
||||
assert message.is_channel_admin is True
|
||||
|
||||
|
||||
def test_telegram_slash_does_not_accept_admin_display_username():
|
||||
"""Telegram 斜杠命令不得把可修改的 username 当作管理员 ID。"""
|
||||
module = TelegramModule()
|
||||
client = SimpleNamespace(bot_username=None, send_msg=Mock())
|
||||
message = _parse_module_message(
|
||||
module,
|
||||
config={"TELEGRAM_ADMINS": "admin"},
|
||||
body=json.dumps(
|
||||
{
|
||||
"message_id": 10,
|
||||
"from": {"id": 10002, "username": "admin"},
|
||||
"chat": {"id": 10002},
|
||||
"text": "/sites",
|
||||
}
|
||||
),
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message is None
|
||||
client.send_msg.assert_called_once()
|
||||
|
||||
|
||||
def test_telegram_empty_admin_list_keeps_legacy_slash_without_agent_admin():
|
||||
"""空名单保持传统命令可用,但不能生成 Agent 管理员身份。"""
|
||||
module = TelegramModule()
|
||||
client = SimpleNamespace(bot_username=None, send_msg=Mock())
|
||||
message = _parse_module_message(
|
||||
module,
|
||||
config={"TELEGRAM_ADMINS": ""},
|
||||
body=json.dumps(
|
||||
{
|
||||
"message_id": 10,
|
||||
"from": {"id": 10002, "username": "admin"},
|
||||
"chat": {"id": 10002},
|
||||
"text": "/sites",
|
||||
}
|
||||
),
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message.is_channel_admin is False
|
||||
client.send_msg.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
{"type": "message", "user": "UADMIN", "text": "hello"},
|
||||
{
|
||||
"type": "block_actions",
|
||||
"user": {"id": "UADMIN", "name": "renamed-user"},
|
||||
"actions": [{"value": "choice:1"}],
|
||||
"message": {"ts": "1710000000.000100"},
|
||||
"container": {"channel_id": "C01"},
|
||||
},
|
||||
],
|
||||
)
|
||||
def test_slack_message_and_callback_use_stable_user_id(payload):
|
||||
message = _parse_module_message(
|
||||
SlackModule(),
|
||||
config={"SLACK_ADMINS": "UADMIN"},
|
||||
body=json.dumps(payload),
|
||||
)
|
||||
|
||||
assert message.userid == "UADMIN"
|
||||
assert message.is_channel_admin is True
|
||||
|
||||
|
||||
def test_slack_slash_does_not_accept_admin_display_username():
|
||||
"""Slack 原生斜杠命令只接受稳定 user_id,不接受 user_name。"""
|
||||
client = SimpleNamespace(send_msg=Mock())
|
||||
message = _parse_module_message(
|
||||
SlackModule(),
|
||||
config={"SLACK_ADMINS": "admin"},
|
||||
body=json.dumps(
|
||||
{
|
||||
"command": "/sites",
|
||||
"user_id": "UUSER",
|
||||
"user_name": "admin",
|
||||
"channel_id": "C01",
|
||||
}
|
||||
),
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message is None
|
||||
client.send_msg.assert_called_once()
|
||||
|
||||
|
||||
def test_slack_empty_admin_list_keeps_legacy_slash_without_agent_admin():
|
||||
"""Slack 空名单不预拦截命令,但 Agent 管理员事实仍为否。"""
|
||||
client = SimpleNamespace(send_msg=Mock())
|
||||
message = _parse_module_message(
|
||||
SlackModule(),
|
||||
config={"SLACK_ADMINS": ""},
|
||||
body=json.dumps(
|
||||
{
|
||||
"command": "/sites",
|
||||
"user_id": "UUSER",
|
||||
"user_name": "admin",
|
||||
"channel_id": "C01",
|
||||
}
|
||||
),
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message.is_channel_admin is False
|
||||
client.send_msg.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
{
|
||||
"type": "message",
|
||||
"userid": "discord-admin-id",
|
||||
"username": "renamed-user",
|
||||
"text": "hello",
|
||||
},
|
||||
{
|
||||
"type": "interaction",
|
||||
"userid": "discord-admin-id",
|
||||
"username": "renamed-user",
|
||||
"callback_data": "choice:1",
|
||||
},
|
||||
],
|
||||
)
|
||||
def test_discord_message_and_callback_use_stable_user_id(payload):
|
||||
message = _parse_module_message(
|
||||
DiscordModule(),
|
||||
config={"DISCORD_ADMINS": "discord-admin-id"},
|
||||
body=json.dumps(payload),
|
||||
)
|
||||
|
||||
assert message.userid == "discord-admin-id"
|
||||
assert message.is_channel_admin is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("payload", "admins"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"text": "hello",
|
||||
"sender": {
|
||||
"open_id": "ou_admin",
|
||||
"user_id": "u_other",
|
||||
"name": "renamed-user",
|
||||
},
|
||||
},
|
||||
"ou_admin",
|
||||
),
|
||||
(
|
||||
{
|
||||
"type": "cardAction",
|
||||
"callback_data": "choice:1",
|
||||
"sender": {
|
||||
"open_id": "ou_other",
|
||||
"user_id": "u_admin",
|
||||
"name": "renamed-user",
|
||||
},
|
||||
},
|
||||
"u_admin",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_feishu_message_and_card_callback_accept_open_id_or_user_id(payload, admins):
|
||||
with patch.object(Feishu, "_build_api_client", return_value=Mock()), patch.object(
|
||||
Feishu, "_start_ws_client"
|
||||
), patch("app.modules.feishu.feishu.UserOper") as user_oper:
|
||||
user_oper.return_value.get_name.return_value = None
|
||||
client = Feishu(
|
||||
FEISHU_APP_ID="app-id",
|
||||
FEISHU_APP_SECRET="app-secret",
|
||||
FEISHU_ADMINS=admins,
|
||||
name="feishu-test",
|
||||
)
|
||||
message = client.parse_message(payload)
|
||||
|
||||
assert message.userid == payload["sender"]["open_id"]
|
||||
assert message.is_channel_admin is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("user_id", "username", "expected"),
|
||||
[("wxid_admin", "renamed-user", True), ("wxid_user", "admin-name", False)],
|
||||
)
|
||||
def test_wechatclawbot_uses_channel_user_id_not_username(user_id, username, expected):
|
||||
message = _parse_module_message(
|
||||
WechatClawBotModule(),
|
||||
config={"WECHATCLAWBOT_ADMINS": "admin-name,wxid_admin"},
|
||||
body={
|
||||
"__channel__": "wechatclawbot",
|
||||
"userid": user_id,
|
||||
"username": username,
|
||||
"text": "hello",
|
||||
},
|
||||
)
|
||||
|
||||
assert message.userid == user_id
|
||||
assert message.is_channel_admin is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("sender", "admins", "expected"),
|
||||
[("wechat-admin", "wechat-admin", True), ("wechat-user", "display-admin", False)],
|
||||
)
|
||||
def test_wechat_bot_uses_sender_userid(sender, admins, expected):
|
||||
message = _parse_module_message(
|
||||
WechatModule(),
|
||||
config={"WECHAT_MODE": "bot", "WECHAT_ADMINS": admins},
|
||||
body=json.dumps(
|
||||
{
|
||||
"body": {
|
||||
"from": {"userid": sender},
|
||||
"msgtype": "text",
|
||||
"text": {"content": "hello"},
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assert message.userid == sender
|
||||
assert message.is_channel_admin is expected
|
||||
|
||||
|
||||
def test_qq_c2c_uses_user_openid_for_admin():
|
||||
message = _parse_module_message(
|
||||
QQBotModule(),
|
||||
config={"QQBOT_ADMINS": "qq-admin"},
|
||||
body={
|
||||
"type": "C2C_MESSAGE_CREATE",
|
||||
"content": "hello",
|
||||
"author": {"user_openid": "qq-admin"},
|
||||
},
|
||||
)
|
||||
|
||||
assert message.userid == "qq-admin"
|
||||
assert message.is_channel_admin is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("admins", "expected"),
|
||||
[("member-admin", True), ("group:group-admin", False), ("group-admin", False)],
|
||||
)
|
||||
def test_qq_group_uses_only_member_openid_for_admin(admins, expected):
|
||||
message = _parse_module_message(
|
||||
QQBotModule(),
|
||||
config={"QQBOT_ADMINS": admins},
|
||||
body={
|
||||
"type": "GROUP_AT_MESSAGE_CREATE",
|
||||
"content": "hello",
|
||||
"author": {"member_openid": "member-admin"},
|
||||
"group_openid": "group-admin",
|
||||
},
|
||||
)
|
||||
|
||||
assert message.userid == "group:group-admin"
|
||||
assert message.username == "member-admin"
|
||||
assert message.is_channel_admin is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("admins", "expected"),
|
||||
[("7", True), ("UID#7", True), ("GID#2", False)],
|
||||
)
|
||||
def test_vocechat_group_uses_only_sender_uid_for_admin(admins, expected):
|
||||
message = _parse_module_message(
|
||||
VoceChatModule(),
|
||||
config={"VOCECHAT_ADMINS": admins, "channel_id": "2"},
|
||||
body=json.dumps(
|
||||
{
|
||||
"detail": {
|
||||
"type": "normal",
|
||||
"content_type": "text/plain",
|
||||
"content": "hello",
|
||||
},
|
||||
"from_uid": 7,
|
||||
"target": {"gid": 2},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assert message.userid == "GID#2"
|
||||
assert message.username == "GID#2"
|
||||
assert message.is_channel_admin is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("admins", "expected"),
|
||||
[("42", True), ("display-admin", False)],
|
||||
)
|
||||
def test_synology_chat_uses_numeric_user_id_not_username(admins, expected):
|
||||
client = SimpleNamespace(check_token=Mock(return_value=True))
|
||||
message = _parse_module_message(
|
||||
SynologyChatModule(),
|
||||
config={"SYNOLOGYCHAT_ADMINS": admins},
|
||||
body={},
|
||||
form={
|
||||
"token": "token",
|
||||
"text": "hello",
|
||||
"user_id": "42",
|
||||
"username": "display-admin",
|
||||
},
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message.userid == 42
|
||||
assert message.username == "display-admin"
|
||||
assert message.is_channel_admin is expected
|
||||
Reference in New Issue
Block a user