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)