Jack1808/Claude_Code
0
1"""Tests for Discord platform adapter."""2 3import asyncio4from unittest.mock import AsyncMock, MagicMock, patch5 6import pytest7 8from messaging.platforms.discord import (9 DISCORD_AVAILABLE,10 DiscordPlatform,11 _get_discord,12 _parse_allowed_channels,13)14 15 16class TestGetDiscord:17 """Tests for _get_discord helper."""18 19 def test_raises_when_discord_not_available(self):20 import messaging.platforms.discord as discord_mod21 22 with (23 patch.object(discord_mod, "DISCORD_AVAILABLE", False),24 patch.object(discord_mod, "_discord_module", None),25 pytest.raises(ImportError, match=r"discord\.py is required"),26 ):27 _get_discord()28 29 30class TestParseAllowedChannels:31 """Tests for _parse_allowed_channels helper."""32 33 def test_empty_string_returns_empty_set(self):34 assert _parse_allowed_channels("") == set()35 assert _parse_allowed_channels(None) == set()36 37 def test_whitespace_only_returns_empty_set(self):38 assert _parse_allowed_channels(" ") == set()39 40 def test_single_channel(self):41 assert _parse_allowed_channels("123456789") == {"123456789"}42 43 def test_comma_separated(self):44 assert _parse_allowed_channels("111,222,333") == {"111", "222", "333"}45 46 def test_strips_whitespace(self):47 assert _parse_allowed_channels(" 111 , 222 ") == {"111", "222"}48 49 def test_empty_parts_ignored(self):50 assert _parse_allowed_channels("111,,222,") == {"111", "222"}51 52 53@pytest.mark.skipif(not DISCORD_AVAILABLE, reason="discord.py not installed")54class TestDiscordPlatform:55 """Tests for DiscordPlatform (requires discord.py)."""56 57 def test_init_with_token(self):58 platform = DiscordPlatform(59 bot_token="test_token",60 allowed_channel_ids="123,456",61 )62 assert platform.bot_token == "test_token"63 assert platform.allowed_channel_ids == {"123", "456"}64 65 def test_init_without_allowed_channels(self):66 with patch.dict("os.environ", {"ALLOWED_DISCORD_CHANNELS": ""}, clear=False):67 platform = DiscordPlatform(bot_token="token", allowed_channel_ids="")68 assert platform.allowed_channel_ids == set()69 70 def test_empty_allowed_channels_rejects_all_messages(self):71 """When allowed_channel_ids is empty, no channels are allowed (secure default)."""72 with patch.dict("os.environ", {"ALLOWED_DISCORD_CHANNELS": ""}, clear=False):73 platform = DiscordPlatform(bot_token="token", allowed_channel_ids="")74 assert platform.allowed_channel_ids == set()75 # Empty set means: not self.allowed_channel_ids is True -> reject76 77 def test_truncate_long_message(self):78 platform = DiscordPlatform(bot_token="token")79 long_text = "x" * 250080 truncated = platform._truncate(long_text)81 assert len(truncated) == 200082 assert truncated.endswith("...")83 84 def test_truncate_short_message_unchanged(self):85 platform = DiscordPlatform(bot_token="token")86 short = "hello"87 assert platform._truncate(short) == short88 89 def test_truncate_exactly_at_limit_unchanged(self):90 platform = DiscordPlatform(bot_token="token")91 exact = "x" * 200092 assert platform._truncate(exact) == exact93 94 def test_truncate_one_over_limit_truncates(self):95 platform = DiscordPlatform(bot_token="token")96 over = "x" * 200197 result = platform._truncate(over)98 assert len(result) == 200099 assert result.endswith("...")100 101 def test_truncate_empty_string(self):102 platform = DiscordPlatform(bot_token="token")103 assert platform._truncate("") == ""104 105 @pytest.mark.asyncio106 async def test_send_message_returns_message_id(self):107 platform = DiscordPlatform(bot_token="token")108 mock_msg = MagicMock()109 mock_msg.id = 999110 mock_channel = AsyncMock()111 mock_channel.send = AsyncMock(return_value=mock_msg)112 platform._connected = True113 with patch.object(114 platform._client, "get_channel", MagicMock(return_value=mock_channel)115 ):116 msg_id = await platform.send_message("123", "Hello")117 assert msg_id == "999"118 119 @pytest.mark.asyncio120 async def test_edit_message(self):121 platform = DiscordPlatform(bot_token="token")122 mock_msg = AsyncMock()123 mock_channel = AsyncMock()124 mock_channel.fetch_message = AsyncMock(return_value=mock_msg)125 platform._connected = True126 with patch.object(127 platform._client, "get_channel", MagicMock(return_value=mock_channel)128 ):129 await platform.edit_message("123", "456", "Updated text")130 mock_msg.edit.assert_called_once_with(content="Updated text")131 132 @pytest.mark.asyncio133 async def test_send_message_channel_not_found_raises(self):134 platform = DiscordPlatform(bot_token="token")135 platform._connected = True136 with (137 patch.object(platform._client, "get_channel", MagicMock(return_value=None)),138 pytest.raises(RuntimeError, match="Channel"),139 ):140 await platform.send_message("123", "Hello")141 142 @pytest.mark.asyncio143 async def test_send_message_channel_no_send_raises(self):144 platform = DiscordPlatform(bot_token="token")145 platform._connected = True146 mock_channel = MagicMock(spec=[]) # No send attr147 with (148 patch.object(149 platform._client, "get_channel", MagicMock(return_value=mock_channel)150 ),151 pytest.raises(RuntimeError, match="Channel"),152 ):153 await platform.send_message("123", "Hello")154 155 @pytest.mark.asyncio156 async def test_queue_send_message_without_limiter_calls_send_message(self):157 platform = DiscordPlatform(bot_token="token")158 platform._limiter = None159 platform._connected = True160 mock_channel = AsyncMock()161 mock_msg = MagicMock()162 mock_msg.id = 42163 mock_channel.send = AsyncMock(return_value=mock_msg)164 with patch.object(165 platform._client, "get_channel", MagicMock(return_value=mock_channel)166 ):167 result = await platform.queue_send_message("123", "hi")168 assert result == "42"169 mock_channel.send.assert_awaited_once()170 171 @pytest.mark.asyncio172 async def test_queue_edit_message_without_limiter_calls_edit_message(self):173 platform = DiscordPlatform(bot_token="token")174 platform._limiter = None175 platform._connected = True176 mock_msg = AsyncMock()177 mock_channel = AsyncMock()178 mock_channel.fetch_message = AsyncMock(return_value=mock_msg)179 with patch.object(180 platform._client, "get_channel", MagicMock(return_value=mock_channel)181 ):182 await platform.queue_edit_message("123", "456", "Updated")183 mock_msg.edit.assert_called_once_with(content="Updated")184 185 @pytest.mark.asyncio186 async def test_on_discord_message_bot_ignored(self):187 platform = DiscordPlatform(bot_token="token", allowed_channel_ids="123")188 handler = AsyncMock()189 platform.on_message(handler)190 msg = MagicMock()191 msg.author.bot = True192 msg.content = "hello"193 msg.channel.id = 123194 await platform._on_discord_message(msg)195 handler.assert_not_called()196 197 @pytest.mark.asyncio198 async def test_on_discord_message_empty_content_ignored(self):199 platform = DiscordPlatform(bot_token="token", allowed_channel_ids="123")200 handler = AsyncMock()201 platform.on_message(handler)202 msg = MagicMock()203 msg.author.bot = False204 msg.content = ""205 msg.channel.id = 123206 await platform._on_discord_message(msg)207 handler.assert_not_called()208 209 @pytest.mark.asyncio210 async def test_on_discord_message_channel_not_allowed_ignored(self):211 platform = DiscordPlatform(bot_token="token", allowed_channel_ids="123")212 handler = AsyncMock()213 platform.on_message(handler)214 msg = MagicMock()215 msg.author.bot = False216 msg.content = "hello"217 msg.channel.id = 999218 await platform._on_discord_message(msg)219 handler.assert_not_called()220 221 @pytest.mark.asyncio222 async def test_on_discord_message_valid_calls_handler(self):223 platform = DiscordPlatform(bot_token="token", allowed_channel_ids="123")224 handler = AsyncMock()225 platform.on_message(handler)226 msg = MagicMock()227 msg.author.bot = False228 msg.author.id = 456229 msg.author.display_name = "User"230 msg.content = "hello"231 msg.channel.id = 123232 msg.id = 789233 msg.reference = None234 await platform._on_discord_message(msg)235 handler.assert_awaited_once()236 call = handler.call_args[0][0]237 assert call.text == "hello"238 assert call.chat_id == "123"239 assert call.user_id == "456"240 assert call.message_id == "789"241 assert call.platform == "discord"242 243 @pytest.mark.asyncio244 async def test_send_message_with_reply_to(self):245 platform = DiscordPlatform(bot_token="token")246 mock_msg = MagicMock()247 mock_msg.id = 999248 mock_channel = AsyncMock()249 mock_channel.send = AsyncMock(return_value=mock_msg)250 platform._connected = True251 with (252 patch.object(253 platform._client, "get_channel", MagicMock(return_value=mock_channel)254 ),255 patch("messaging.platforms.discord._get_discord") as mock_get,256 ):257 mock_discord = MagicMock()258 mock_get.return_value = mock_discord259 msg_id = await platform.send_message("123", "Hello", reply_to="456")260 assert msg_id == "999"261 mock_channel.send.assert_awaited_once()262 call_kw = mock_channel.send.call_args[1]263 assert call_kw.get("reference") is not None264 265 @pytest.mark.asyncio266 async def test_edit_message_not_found_returns_gracefully(self):267 import discord as discord_pkg268 269 platform = DiscordPlatform(bot_token="token")270 mock_channel = AsyncMock()271 mock_resp = MagicMock()272 mock_resp.status = 404273 mock_channel.fetch_message = AsyncMock(274 side_effect=discord_pkg.NotFound(mock_resp, "Not found")275 )276 platform._connected = True277 with patch.object(278 platform._client, "get_channel", MagicMock(return_value=mock_channel)279 ):280 await platform.edit_message("123", "456", "Updated")281 # Should not raise - NotFound is caught and we return282 283 @pytest.mark.asyncio284 async def test_delete_message(self):285 platform = DiscordPlatform(bot_token="token")286 mock_msg = AsyncMock()287 mock_channel = AsyncMock()288 mock_channel.fetch_message = AsyncMock(return_value=mock_msg)289 platform._connected = True290 with (291 patch.object(292 platform._client, "get_channel", MagicMock(return_value=mock_channel)293 ),294 patch("messaging.platforms.discord._get_discord") as mock_get,295 ):296 mock_get.return_value = MagicMock()297 await platform.delete_message("123", "456")298 mock_msg.delete.assert_awaited_once()299 300 @pytest.mark.asyncio301 async def test_fire_and_forget_with_coroutine(self):302 platform = DiscordPlatform(bot_token="token")303 304 async def _task():305 pass306 307 coro = _task()308 with patch("asyncio.create_task") as mock_create:309 310 def _run(c):311 return asyncio.ensure_future(c)312 313 mock_create.side_effect = _run314 platform.fire_and_forget(coro)315 mock_create.assert_called_once()316 await asyncio.sleep(0)317 318 def test_on_message_registers_handler(self):319 platform = DiscordPlatform(bot_token="token")320 handler = AsyncMock()321 platform.on_message(handler)322 assert platform._message_handler is handler323 324 @pytest.mark.asyncio325 async def test_start_requires_token(self):326 with patch.dict("os.environ", {"DISCORD_BOT_TOKEN": ""}, clear=False):327 platform = DiscordPlatform(bot_token="")328 with pytest.raises(ValueError, match="DISCORD_BOT_TOKEN"):329 await platform.start()330 331 @pytest.mark.asyncio332 async def test_start_connects(self):333 platform = DiscordPlatform(bot_token="token")334 335 async def _fake_start(_token):336 platform._connected = True337 338 with (339 patch.object(340 platform._client,341 "start",342 new_callable=AsyncMock,343 side_effect=_fake_start,344 ),345 patch(346 "messaging.limiter.MessagingRateLimiter.get_instance",347 new_callable=AsyncMock,348 ),349 ):350 await platform.start()351 assert platform.is_connected is True352 353 @pytest.mark.asyncio354 async def test_stop_when_already_closed(self):355 platform = DiscordPlatform(bot_token="token")356 platform._connected = True357 with patch.object(358 platform._client, "is_closed", new_callable=MagicMock, return_value=True359 ):360 await platform.stop()361 assert platform.is_connected is False362 363 @pytest.mark.asyncio364 async def test_stop_closes_client(self):365 platform = DiscordPlatform(bot_token="token")366 platform._connected = True367 mock_close = AsyncMock()368 with (369 patch.object(370 platform._client,371 "is_closed",372 new_callable=MagicMock,373 return_value=False,374 ),375 patch.object(platform._client, "close", mock_close),376 ):377 platform._start_task = None378 await platform.stop()379 mock_close.assert_awaited_once()380 assert platform.is_connected is False381 