新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
395 lines
13 KiB
Python
395 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from yuxi.channels.adapters.mattermost.security import (
|
|
MattermostSecurity,
|
|
MattermostSecurityConfig,
|
|
DM_POLICY_OPEN,
|
|
DM_POLICY_ALLOWLIST,
|
|
DM_POLICY_DISABLED,
|
|
DM_POLICY_PAIRING,
|
|
GROUP_POLICY_OPEN,
|
|
GROUP_POLICY_ALLOWLIST,
|
|
GROUP_POLICY_DISABLED,
|
|
normalize_allow_entry,
|
|
is_allow_entry_match,
|
|
)
|
|
from yuxi.channels.adapters.mattermost.monitor import MentionGate, MODE_ONMESSAGE, MODE_ONCHAR, MODE_ONCALL
|
|
from yuxi.channels.adapters.mattermost.reply import ReplyManager, REPLY_OFF, REPLY_FIRST, REPLY_ALL
|
|
from yuxi.channels.adapters.mattermost.session import resolve_chat_id, resolve_chat_type, build_thread_id
|
|
from yuxi.channels.models import ChatType
|
|
|
|
|
|
class TestNormalizeAllowEntry:
|
|
def test_plain_user_id(self):
|
|
assert normalize_allow_entry("abc123") == "abc123"
|
|
|
|
def test_mattermost_prefix(self):
|
|
assert normalize_allow_entry("mattermost:abc123") == "abc123"
|
|
|
|
def test_user_prefix(self):
|
|
assert normalize_allow_entry("user:abc123") == "abc123"
|
|
|
|
def test_channel_prefix(self):
|
|
assert normalize_allow_entry("channel:ch1") == "ch1"
|
|
|
|
def test_at_username(self):
|
|
assert normalize_allow_entry("@johndoe") == "@johndoe"
|
|
|
|
def test_empty(self):
|
|
assert normalize_allow_entry("") == ""
|
|
assert normalize_allow_entry(" ") == ""
|
|
|
|
def test_case_insensitive_prefix(self):
|
|
assert normalize_allow_entry("MATTERMOST:abc") == "abc"
|
|
|
|
|
|
class TestIsAllowEntryMatch:
|
|
def test_exact_user_id_match(self):
|
|
assert is_allow_entry_match("abc123", "abc123", "johndoe") is True
|
|
|
|
def test_user_id_mismatch(self):
|
|
assert is_allow_entry_match("abc123", "xyz456", "johndoe") is False
|
|
|
|
def test_username_match(self):
|
|
assert is_allow_entry_match("@johndoe", "abc123", "johndoe") is True
|
|
|
|
def test_username_match_casefold(self):
|
|
assert is_allow_entry_match("@JohnDoe", "abc123", "johndoe") is True
|
|
|
|
def test_username_mismatch(self):
|
|
assert is_allow_entry_match("@johndoe", "abc123", "janedoe") is False
|
|
|
|
def test_empty_entry(self):
|
|
assert is_allow_entry_match("", "abc123") is False
|
|
|
|
|
|
class TestMattermostSecurityConfig:
|
|
def test_defaults(self):
|
|
cfg = MattermostSecurityConfig.from_config({})
|
|
assert cfg.dm_policy == "open"
|
|
assert cfg.group_policy == "open"
|
|
|
|
def test_custom_policies(self):
|
|
cfg = MattermostSecurityConfig.from_config({
|
|
"dm_policy": "allowlist",
|
|
"group_policy": "allowlist",
|
|
"allow_from": ["u1", "@johndoe"],
|
|
"group_allow_from": ["u2"],
|
|
})
|
|
assert cfg.dm_policy == "allowlist"
|
|
assert cfg.group_policy == "allowlist"
|
|
assert "u1" in cfg.allow_from
|
|
assert "@johndoe" in cfg.allow_from
|
|
|
|
def test_invalid_policy_falls_back(self):
|
|
cfg = MattermostSecurityConfig.from_config({"dm_policy": "invalid"})
|
|
assert cfg.dm_policy == "open"
|
|
|
|
|
|
class TestMattermostSecurity:
|
|
def test_open_dm(self):
|
|
sec = MattermostSecurity({})
|
|
result = sec.check_dm("u1")
|
|
assert result.allowed is True
|
|
|
|
def test_disabled_dm(self):
|
|
sec = MattermostSecurity({"dm_policy": "disabled"})
|
|
result = sec.check_dm("u1")
|
|
assert result.allowed is False
|
|
|
|
def test_allowlist_dm(self):
|
|
sec = MattermostSecurity({"dm_policy": "allowlist", "allow_from": ["u1"]})
|
|
assert sec.check_dm("u1").allowed is True
|
|
assert sec.check_dm("u2").allowed is False
|
|
|
|
def test_allowlist_dm_empty(self):
|
|
sec = MattermostSecurity({"dm_policy": "allowlist"})
|
|
result = sec.check_dm("u1")
|
|
assert result.allowed is False
|
|
|
|
def test_open_group(self):
|
|
sec = MattermostSecurity({})
|
|
result = sec.check_group("u1")
|
|
assert result.allowed is True
|
|
|
|
def test_disabled_group(self):
|
|
sec = MattermostSecurity({"group_policy": "disabled"})
|
|
result = sec.check_group("u1")
|
|
assert result.allowed is False
|
|
|
|
def test_policy_properties(self):
|
|
sec = MattermostSecurity({"dm_policy": "pairing", "group_policy": "allowlist"})
|
|
assert sec.dm_policy == "pairing"
|
|
assert sec.group_policy == "allowlist"
|
|
|
|
|
|
class TestMentionGate:
|
|
def test_dm_always_responds_onmessage(self):
|
|
gate = MentionGate({})
|
|
result = gate.check("direct", "hello", bot_mentioned=False)
|
|
assert result.should_respond is True
|
|
|
|
def test_dm_oncall_still_responds(self):
|
|
gate = MentionGate({"chatmode": "oncall"})
|
|
result = gate.check("direct", "hello", bot_mentioned=False)
|
|
assert result.should_respond is True
|
|
|
|
def test_dm_onchar_with_match(self):
|
|
gate = MentionGate({"chatmode": "onchar", "onchar_prefixes": [">"]})
|
|
result = gate.check("direct", "> hello", bot_mentioned=False)
|
|
assert result.should_respond is True
|
|
|
|
def test_dm_onchar_no_match(self):
|
|
gate = MentionGate({"chatmode": "onchar", "onchar_prefixes": [">"]})
|
|
result = gate.check("direct", "hello", bot_mentioned=False)
|
|
assert result.should_respond is False
|
|
|
|
def test_group_requires_mention(self):
|
|
gate = MentionGate({"require_mention": True})
|
|
result = gate.check("group", "hello", bot_mentioned=False)
|
|
assert result.should_respond is False
|
|
|
|
def test_group_with_mention(self):
|
|
gate = MentionGate({"require_mention": True})
|
|
result = gate.check("group", "hello", bot_mentioned=True)
|
|
assert result.should_respond is True
|
|
|
|
def test_group_onchar(self):
|
|
gate = MentionGate({"chatmode": "onchar", "onchar_prefixes": ["!", ">"]})
|
|
result = gate.check("group", "!help", bot_mentioned=False)
|
|
assert result.should_respond is True
|
|
|
|
|
|
class TestReplyManager:
|
|
def test_off_mode(self):
|
|
rm = ReplyManager({})
|
|
assert rm.should_reply("direct") is False
|
|
|
|
def test_first_mode_dm(self):
|
|
rm = ReplyManager({"reply_to_mode": "first"})
|
|
assert rm.should_reply("direct") is True
|
|
|
|
def test_all_mode(self):
|
|
rm = ReplyManager({"reply_to_mode": "all"})
|
|
assert rm.should_reply("group") is True
|
|
|
|
def test_resolve_reply_target_off(self):
|
|
rm = ReplyManager({"reply_to_mode": "off"})
|
|
assert rm.resolve_reply_target("ch1", "root1") is None
|
|
|
|
def test_resolve_reply_target_all(self):
|
|
rm = ReplyManager({"reply_to_mode": "all"})
|
|
assert rm.resolve_reply_target("ch1", "root1") == "root1"
|
|
|
|
|
|
class TestSessionThread:
|
|
def test_build_thread_id(self):
|
|
assert build_thread_id("ch1", "root1") == "channel_ch1:thread_root1"
|
|
|
|
def test_resolve_chat_id_thread(self):
|
|
chat_id = resolve_chat_id(
|
|
{"root_id": "root1", "channel_id": "ch1", "user_id": "u1"},
|
|
{"type": "D"},
|
|
)
|
|
assert chat_id == "channel_ch1:thread_root1"
|
|
|
|
def test_resolve_chat_id_dm(self):
|
|
chat_id = resolve_chat_id(
|
|
{"user_id": "u1", "channel_id": "ch1"},
|
|
{"type": "D"},
|
|
)
|
|
assert chat_id == "dm_u1"
|
|
|
|
def test_resolve_chat_id_group(self):
|
|
chat_id = resolve_chat_id(
|
|
{"user_id": "u1", "channel_id": "ch1"},
|
|
{"type": "O"},
|
|
)
|
|
assert chat_id == "channel_ch1"
|
|
|
|
def test_resolve_chat_type_thread(self):
|
|
ct = resolve_chat_type(
|
|
{"root_id": "root1", "channel_id": "ch1", "user_id": "u1"},
|
|
{},
|
|
)
|
|
assert ct == ChatType.THREAD
|
|
|
|
def test_resolve_chat_type_direct(self):
|
|
ct = resolve_chat_type({"user_id": "u1", "channel_id": "ch1"}, {"type": "D"})
|
|
assert ct == ChatType.DIRECT
|
|
|
|
def test_resolve_chat_type_group(self):
|
|
ct = resolve_chat_type({"user_id": "u1", "channel_id": "ch1"}, {"type": "O"})
|
|
assert ct == ChatType.GROUP
|
|
|
|
|
|
class TestTargetResolution:
|
|
def test_channel_prefix(self):
|
|
from yuxi.channels.adapters.mattermost.target_resolution import parse_target
|
|
|
|
t = parse_target("channel:abc123")
|
|
assert t.target_type == "channel"
|
|
assert t.target_id == "abc123"
|
|
|
|
def test_user_prefix(self):
|
|
from yuxi.channels.adapters.mattermost.target_resolution import parse_target
|
|
|
|
t = parse_target("user:u1")
|
|
assert t.target_type == "user"
|
|
assert t.target_id == "u1"
|
|
|
|
def test_at_username(self):
|
|
from yuxi.channels.adapters.mattermost.target_resolution import parse_target
|
|
|
|
t = parse_target("@johndoe")
|
|
assert t.target_type == "user"
|
|
assert t.target_id == "johndoe"
|
|
|
|
def test_hash_channel(self):
|
|
from yuxi.channels.adapters.mattermost.target_resolution import parse_target
|
|
|
|
t = parse_target("#general")
|
|
assert t.target_type == "channel"
|
|
assert t.target_id == "general"
|
|
|
|
def test_plain_id(self):
|
|
from yuxi.channels.adapters.mattermost.target_resolution import parse_target
|
|
|
|
t = parse_target("abc123")
|
|
assert t.target_type == "unknown"
|
|
|
|
|
|
class TestPolls:
|
|
def test_build_poll_props_min_options(self):
|
|
from yuxi.channels.adapters.mattermost.polls import build_poll_props
|
|
|
|
props = build_poll_props("Q?", ["A", "B"], "poll1")
|
|
assert "attachments" in props
|
|
assert len(props["attachments"]) == 1
|
|
assert props["props"]["poll_id"] == "poll1"
|
|
|
|
def test_build_yes_no_poll(self):
|
|
from yuxi.channels.adapters.mattermost.polls import build_yes_no_poll_props
|
|
|
|
props = build_yes_no_poll_props("Yes or No?", "poll2")
|
|
assert "attachments" in props
|
|
assert props["props"]["type"] == "yes_no_poll"
|
|
|
|
|
|
class TestSlashCommands:
|
|
def test_build_command_payload(self):
|
|
from yuxi.channels.adapters.mattermost.slash import (
|
|
SlashCommand,
|
|
SlashCommandConfig,
|
|
build_command_payload,
|
|
)
|
|
|
|
cmd = SlashCommand(command="/fp", description="ForcePilot")
|
|
payload = build_command_payload(cmd, "team1", "https://example.com/callback")
|
|
assert payload["team_id"] == "team1"
|
|
assert payload["trigger"] == "fp"
|
|
assert payload["method"] == "P"
|
|
|
|
def test_default_commands(self):
|
|
from yuxi.channels.adapters.mattermost.slash import get_supported_commands
|
|
|
|
cmds = get_supported_commands()
|
|
assert len(cmds) >= 5
|
|
|
|
|
|
class TestInteractions:
|
|
def test_build_button_attachment(self):
|
|
from yuxi.channels.adapters.mattermost.interactions import build_button_attachment
|
|
|
|
att = build_button_attachment(
|
|
"Title", "Text",
|
|
[{"text": "OK", "action": "confirm", "id": "btn1"}],
|
|
callback_id="cb1",
|
|
)
|
|
assert att["title"] == "Title"
|
|
assert len(att["actions"]) == 1
|
|
|
|
def test_verify_hmac_signature(self):
|
|
from yuxi.channels.adapters.mattermost.interactions import verify_hmac_signature
|
|
|
|
assert verify_hmac_signature(b"data", "", "") is True
|
|
assert verify_hmac_signature(b"data", "any", "") is True
|
|
|
|
|
|
class TestCache:
|
|
def test_record_and_get(self):
|
|
from yuxi.channels.adapters.mattermost.cache import SentMessageCache
|
|
|
|
cache = SentMessageCache()
|
|
cache.record("m1", "ch1", thread_id="th1")
|
|
entry = cache.get("m1")
|
|
assert entry["msg_id"] == "m1"
|
|
assert entry["thread_id"] == "th1"
|
|
|
|
def test_get_thread_id(self):
|
|
from yuxi.channels.adapters.mattermost.cache import SentMessageCache
|
|
|
|
cache = SentMessageCache()
|
|
cache.record("m1", "ch1", thread_id="th1")
|
|
assert cache.get_thread_id("m1") == "th1"
|
|
assert cache.get_thread_id("m2") is None
|
|
|
|
def test_clear(self):
|
|
from yuxi.channels.adapters.mattermost.cache import SentMessageCache
|
|
|
|
cache = SentMessageCache()
|
|
cache.record("m1", "ch1")
|
|
cache.clear()
|
|
assert cache.size() == 0
|
|
|
|
|
|
class TestSSRF:
|
|
def test_localhost_is_private(self):
|
|
from yuxi.channels.adapters.mattermost.client import is_private_url
|
|
|
|
assert is_private_url("http://localhost:8080") is True
|
|
|
|
def test_private_ip(self):
|
|
from yuxi.channels.adapters.mattermost.client import is_private_url
|
|
|
|
assert is_private_url("http://10.0.0.1") is True
|
|
assert is_private_url("http://192.168.1.1") is True
|
|
|
|
def test_public_url(self):
|
|
from yuxi.channels.adapters.mattermost.client import is_private_url
|
|
|
|
assert is_private_url("https://example.com") is False
|
|
|
|
def test_empty_url(self):
|
|
from yuxi.channels.adapters.mattermost.client import is_private_url
|
|
|
|
assert is_private_url("") is False
|
|
|
|
|
|
class TestConfigSchema:
|
|
def test_missing_required(self):
|
|
from yuxi.channels.adapters.mattermost.config_schema import validate_config_schema
|
|
|
|
errors = validate_config_schema({})
|
|
assert len(errors) >= 2
|
|
|
|
def test_valid_config(self):
|
|
from yuxi.channels.adapters.mattermost.config_schema import validate_config_schema
|
|
|
|
errors = validate_config_schema({
|
|
"server_url": "https://example.com",
|
|
"bot_token": "token123",
|
|
})
|
|
assert len(errors) == 0
|
|
|
|
def test_invalid_dm_policy(self):
|
|
from yuxi.channels.adapters.mattermost.config_schema import validate_config_schema
|
|
|
|
errors = validate_config_schema({
|
|
"server_url": "https://example.com",
|
|
"bot_token": "token123",
|
|
"dm_policy": "bad_policy",
|
|
})
|
|
assert any("dm_policy" in e for e in errors) |