Files
MoviePilot/tests/test_agent_channel_admin_identity.py
2026-08-13 14:53:59 +08:00

426 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.
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