from __future__ import annotations import asyncio import pytest from yuxi.channels.adapters.mattermost.adapter import ( STREAM_BUFFER_MAX_CHARS, MattermostAdapter, _parse_driver_scheme_port, ) from yuxi.channels.models import ChannelStatus class TestParseDriverSchemePort: def test_https_default(self): scheme, port = _parse_driver_scheme_port("https://mattermost.example.com") assert scheme == "https" assert port == 443 def test_http_default(self): scheme, port = _parse_driver_scheme_port("http://mattermost.example.com") assert scheme == "http" assert port == 80 def test_https_with_explicit_port(self): scheme, port = _parse_driver_scheme_port("https://mattermost.example.com:8443") assert scheme == "https" assert port == 8443 def test_http_with_explicit_port(self): scheme, port = _parse_driver_scheme_port("http://localhost:8065") assert scheme == "http" assert port == 8065 def test_no_scheme_defaults_to_https(self): scheme, port = _parse_driver_scheme_port("mattermost.example.com") assert scheme == "https" assert port == 443 def test_url_with_path(self): scheme, port = _parse_driver_scheme_port("https://mattermost.example.com/api/v4") assert scheme == "https" assert port == 443 @pytest.fixture def adapter(): config = { "bot_token": "test-token", "server_url": "https://mattermost.example.com", } return MattermostAdapter(config=config) class TestShouldIgnorePost: def test_ignore_bot_own_message(self, adapter): adapter._bot_user_id = "bot_001" post = {"user_id": "bot_001", "message": "hello"} assert adapter._should_ignore_post(post) is True def test_ignore_system_add_remove(self, adapter): adapter._bot_user_id = "bot_001" post = {"user_id": "u1", "type": "system_add_remove"} assert adapter._should_ignore_post(post) is True def test_ignore_system_join_leave(self, adapter): adapter._bot_user_id = "bot_001" post = {"user_id": "u1", "type": "system_join_leave"} assert adapter._should_ignore_post(post) is True def test_allow_other_user_message(self, adapter): adapter._bot_user_id = "bot_001" post = {"user_id": "u2", "message": "hello"} assert adapter._should_ignore_post(post) is False class TestParseWsPost: def test_parse_json_string(self): post = MattermostAdapter._parse_ws_post({"post": '{"id":"m1","message":"hello"}'}) assert post == {"id": "m1", "message": "hello"} def test_parse_already_dict(self): post_dict = {"id": "m1", "message": "hello"} post = MattermostAdapter._parse_ws_post({"post": post_dict}) assert post == post_dict def test_parse_invalid_json_returns_none(self): post = MattermostAdapter._parse_ws_post({"post": "not-json"}) assert post is None def test_parse_empty_string_returns_none(self): post = MattermostAdapter._parse_ws_post({"post": ""}) assert post is None def test_parse_missing_key_returns_none(self): post = MattermostAdapter._parse_ws_post({}) assert post is None def test_parse_none_value_returns_none(self): post = MattermostAdapter._parse_ws_post({"post": None}) assert post is None class TestSendStreamChunk: def _make_adapter_for_stream(self): config = { "bot_token": "test-token", "server_url": "https://mattermost.example.com", } adapter = MattermostAdapter(config=config) adapter._bot_user_id = "bot_001" adapter._bot_username = "testbot" return adapter def test_send_stream_chunk_not_connected(self): adapter = self._make_adapter_for_stream() result = asyncio.run(adapter.send_stream_chunk("ch1", "msg1", "hello", finished=False)) assert result.success is False assert result.error == "Not connected" def test_send_stream_chunk_buffer_accumulation(self): adapter = self._make_adapter_for_stream() adapter._status = ChannelStatus.CONNECTED adapter._driver = _make_mock_driver() result = asyncio.run(adapter.send_stream_chunk("ch1", "msg1", "chunk1", finished=False)) assert result.success is True assert result.message_id == "msg1" result = asyncio.run(adapter.send_stream_chunk("ch1", "msg1", "chunk2", finished=False)) assert result.success is True assert result.message_id == "msg1" entry = adapter._streaming_messages.get("ch1", {}) pending = entry.get("_pending", "") assert "chunk2" in pending def test_send_stream_chunk_finished_flushes(self): adapter = self._make_adapter_for_stream() adapter._status = ChannelStatus.CONNECTED adapter._driver = _make_mock_driver() asyncio.run(adapter.send_stream_chunk("ch1", "", "chunk1", finished=False)) result = asyncio.run(adapter.send_stream_chunk("ch1", "", "chunk2", finished=True)) assert result.success is True def test_send_stream_chunk_first_post_creates_msg(self): adapter = self._make_adapter_for_stream() adapter._status = ChannelStatus.CONNECTED adapter._driver = _make_mock_driver() result = asyncio.run(adapter.send_stream_chunk("ch1", "", "hello world", finished=False)) assert result.success is True assert result.message_id == "mock_post_id" def test_send_stream_chunk_with_existing_msg_id(self): adapter = self._make_adapter_for_stream() adapter._status = ChannelStatus.CONNECTED adapter._driver = _make_mock_driver() result = asyncio.run(adapter.send_stream_chunk("ch1", "existing_msg", "update text", finished=False)) assert result.success is True assert result.message_id == "existing_msg" def test_send_stream_chunk_buffer_overflow(self): adapter = self._make_adapter_for_stream() adapter._status = ChannelStatus.CONNECTED adapter._driver = _make_mock_driver() huge_chunk = "x" * (STREAM_BUFFER_MAX_CHARS + 100) result = asyncio.run(adapter.send_stream_chunk("ch1", "", huge_chunk, finished=False)) assert result.success is True def test_send_stream_chunk_concurrent_lock(self): adapter = self._make_adapter_for_stream() adapter._status = ChannelStatus.CONNECTED adapter._driver = _make_mock_driver() async def send_chunks(): tasks = [] for i in range(10): tasks.append(adapter.send_stream_chunk("ch1", "", f"chunk_{i}", finished=False)) results = await asyncio.gather(*tasks) return results results = asyncio.run(send_chunks()) assert all(r.success for r in results) def test_send_stream_chunk_cleanup_on_finished(self): adapter = self._make_adapter_for_stream() adapter._status = ChannelStatus.CONNECTED adapter._driver = _make_mock_driver() asyncio.run(adapter.send_stream_chunk("ch1", "msg_xxx", "final", finished=True)) assert "ch1" not in adapter._streaming_messages def test_send_stream_chunk_rate_limiting(self): adapter = self._make_adapter_for_stream() adapter._status = ChannelStatus.CONNECTED adapter._driver = _make_mock_driver() result = asyncio.run(adapter.send_stream_chunk("ch1", "", "first", finished=False)) assert result.success is True entry = adapter._streaming_messages.get("ch1", {}) assert "_last_update" in entry class TestSendMedia: def _make_adapter(self): config = { "bot_token": "test-token", "server_url": "https://mattermost.example.com", } adapter = MattermostAdapter(config=config) adapter._bot_user_id = "bot_001" return adapter def test_send_media_not_connected(self): adapter = self._make_adapter() result = asyncio.run(adapter.send_media("ch1", "image", b"fake_data")) assert result.success is False assert result.error == "Not connected" class TestDownloadMedia: def _make_adapter(self): config = { "bot_token": "test-token", "server_url": "https://mattermost.example.com", } adapter = MattermostAdapter(config=config) adapter._bot_user_id = "bot_001" return adapter def test_download_media_not_connected(self): adapter = self._make_adapter() with pytest.raises(Exception): asyncio.run(adapter.download_media("file_001")) class TestRefreshToken: def test_refresh_token_returns_true(self, adapter): result = asyncio.run(adapter._refresh_token_if_needed()) assert result is True class TestReconnectTaskTracking: def test_ws_reconnect_task_initialized_none(self, adapter): assert adapter._ws_reconnect_task is None def test_streaming_lock_initialized(self, adapter): assert adapter._streaming_lock is not None class TestDedupCheck: def test_first_post_not_duplicate(self, adapter): assert adapter._dedup_check("post_001") is False def test_second_same_post_is_duplicate(self, adapter): assert adapter._dedup_check("post_001") is False assert adapter._dedup_check("post_001") is True def test_different_posts_not_duplicates(self, adapter): assert adapter._dedup_check("post_001") is False assert adapter._dedup_check("post_002") is False def test_empty_post_id_not_duplicate(self, adapter): assert adapter._dedup_check("") is False assert adapter._dedup_check("") is False def test_cache_eviction_on_overflow(self, adapter): adapter._seen_posts = {f"post_{i}": 0.0 for i in range(2050)} result = adapter._dedup_check("post_new") assert result is False assert len(adapter._seen_posts) <= 2050 class TestNormalizeReaction: def _make_adapter(self): from yuxi.channels.adapters.mattermost.adapter import MattermostAdapter config = { "bot_token": "test-token", "server_url": "https://mattermost.example.com", } adapter = MattermostAdapter(config=config) adapter._bot_user_id = "bot_001" adapter._bot_username = "testbot" return adapter def test_reaction_added_normalization(self): adapter = self._make_adapter() raw = { "event": "reaction_added", "data": {"reaction": '{"user_id":"u1","post_id":"p1","emoji_name":"+1","create_at":123}'}, "broadcast": {"channel_id": "ch1", "team_id": "t1", "user_id": "u1"}, } msg = adapter.normalize_inbound(raw) assert msg.event_type == "system.event" assert msg.content == ":+1:" assert msg.metadata["reaction_event"] == "reaction_added" assert msg.metadata["emoji_name"] == "+1" assert msg.metadata["post_id"] == "p1" assert msg.metadata["channel_id"] == "ch1" assert msg.identity.channel_user_id == "u1" def test_reaction_removed_normalization(self): adapter = self._make_adapter() raw = { "event": "reaction_removed", "data": {"reaction": '{"user_id":"u2","post_id":"p2","emoji_name":"thumbsup"}'}, "broadcast": {"channel_id": "ch2", "team_id": "t2", "user_id": "u2"}, } msg = adapter.normalize_inbound(raw) assert msg.event_type == "system.event" assert msg.content == ":thumbsup:" assert msg.metadata["reaction_event"] == "reaction_removed" def test_reaction_data_already_dict(self): adapter = self._make_adapter() raw = { "event": "reaction_added", "data": {"reaction": {"user_id": "u3", "post_id": "p3", "emoji_name": "heart"}}, "broadcast": {"channel_id": "ch3", "team_id": "t3", "user_id": "u3"}, } msg = adapter.normalize_inbound(raw) assert msg.content == ":heart:" class TestErrorLogging: def _make_adapter(self): config = { "bot_token": "test-token", "server_url": "https://mattermost.example.com", } adapter = MattermostAdapter(config=config) adapter._bot_user_id = "bot_001" adapter._bot_username = "testbot" adapter._status = ChannelStatus.CONNECTED class FailingPosts: def update_post(self, **kwargs): raise RuntimeError("connection refused") def delete_post(self, **kwargs): raise RuntimeError("connection refused") def create_post(self, **kwargs): raise RuntimeError("connection refused") class FailingReactions: def create_reaction(self, **kwargs): raise RuntimeError("connection refused") class FailingFiles: def upload_file(self, **kwargs): raise RuntimeError("connection refused") class FailingDriver: def __init__(self): self.posts = FailingPosts() self.reactions = FailingReactions() self.files = FailingFiles() adapter._driver = FailingDriver() adapter._ws_task = object() return adapter def test_edit_message_returns_failure_on_error(self): adapter = self._make_adapter() result = asyncio.run(adapter.edit_message("ch1", "msg1", "test")) assert result.success is False assert result.error is not None def test_delete_message_returns_failure_on_error(self): adapter = self._make_adapter() result = asyncio.run(adapter.delete_message("ch1", "msg1")) assert result.success is False assert result.error is not None def test_send_reaction_returns_failure_on_error(self): adapter = self._make_adapter() result = asyncio.run(adapter.send_reaction("ch1", "msg1", "+1")) assert result.success is False assert result.error is not None def test_send_media_raises_on_error(self): from yuxi.channels.exceptions import DeliveryFailedError adapter = self._make_adapter() with pytest.raises(DeliveryFailedError): asyncio.run(adapter.send_media("ch1", "image", b"data")) def _make_mock_driver(): class MockPosts: def create_post(self, options=None): return {"id": "mock_post_id"} def patch_post(self, post_id=None, options=None): return {"id": post_id} class MockDriver: def __init__(self): self.posts = MockPosts() return MockDriver()