- Use channel.send() instead of followup.send() for custom command output (webhook-based followup messages don't trigger mention notifications) - Add ephemeral "Sending..." confirmation to satisfy interaction response - Add utils/mentions.py for converting text @mentions to Discord format - Add tests for mention conversion utility Co-Authored-By: Claude <noreply@anthropic.com>
215 lines
6.6 KiB
Python
215 lines
6.6 KiB
Python
"""
|
|
Tests for the mention conversion utilities.
|
|
|
|
These tests verify that human-readable @mentions are properly converted
|
|
to Discord's ID-based format for triggering notifications.
|
|
"""
|
|
import pytest
|
|
from unittest.mock import MagicMock, AsyncMock
|
|
from utils.mentions import convert_mentions, convert_mentions_async
|
|
|
|
|
|
class TestConvertMentions:
|
|
"""Tests for the synchronous convert_mentions function."""
|
|
|
|
def test_converts_role_mention(self):
|
|
"""Test that @RoleName is converted to <@&role_id>."""
|
|
# Create mock guild with a role
|
|
mock_role = MagicMock()
|
|
mock_role.name = "Waikiki Whale Sharks"
|
|
mock_role.id = 123456789
|
|
|
|
mock_guild = MagicMock()
|
|
mock_guild.roles = [mock_role]
|
|
mock_guild.members = []
|
|
|
|
content = "@Waikiki Whale Sharks"
|
|
result = convert_mentions(content, mock_guild)
|
|
|
|
assert result == "<@&123456789>"
|
|
|
|
def test_converts_role_mention_case_insensitive(self):
|
|
"""Test that role matching is case-insensitive."""
|
|
mock_role = MagicMock()
|
|
mock_role.name = "Waikiki Whale Sharks"
|
|
mock_role.id = 123456789
|
|
|
|
mock_guild = MagicMock()
|
|
mock_guild.roles = [mock_role]
|
|
mock_guild.members = []
|
|
|
|
content = "@waikiki whale sharks"
|
|
result = convert_mentions(content, mock_guild)
|
|
|
|
assert result == "<@&123456789>"
|
|
|
|
def test_converts_user_mention_by_display_name(self):
|
|
"""Test that @DisplayName is converted to <@user_id>."""
|
|
mock_member = MagicMock()
|
|
mock_member.display_name = "CalCorum"
|
|
mock_member.name = "cal"
|
|
mock_member.id = 987654321
|
|
|
|
mock_guild = MagicMock()
|
|
mock_guild.roles = []
|
|
mock_guild.members = [mock_member]
|
|
|
|
content = "@CalCorum"
|
|
result = convert_mentions(content, mock_guild)
|
|
|
|
assert result == "<@987654321>"
|
|
|
|
def test_converts_user_mention_by_username(self):
|
|
"""Test that @username is converted to <@user_id>."""
|
|
mock_member = MagicMock()
|
|
mock_member.display_name = "Cal Corum"
|
|
mock_member.name = "calcorum"
|
|
mock_member.id = 987654321
|
|
|
|
mock_guild = MagicMock()
|
|
mock_guild.roles = []
|
|
mock_guild.members = [mock_member]
|
|
|
|
content = "@calcorum"
|
|
result = convert_mentions(content, mock_guild)
|
|
|
|
assert result == "<@987654321>"
|
|
|
|
def test_converts_multiple_mentions(self):
|
|
"""Test that multiple mentions in same content are all converted."""
|
|
mock_role1 = MagicMock()
|
|
mock_role1.name = "Team A"
|
|
mock_role1.id = 111
|
|
|
|
mock_role2 = MagicMock()
|
|
mock_role2.name = "Team B"
|
|
mock_role2.id = 222
|
|
|
|
mock_guild = MagicMock()
|
|
mock_guild.roles = [mock_role1, mock_role2]
|
|
mock_guild.members = []
|
|
|
|
content = "@Team A vs @Team B"
|
|
result = convert_mentions(content, mock_guild)
|
|
|
|
assert result == "<@&111> vs <@&222>"
|
|
|
|
def test_preserves_already_formatted_mentions(self):
|
|
"""Test that <@123> and <@&456> are not modified."""
|
|
mock_guild = MagicMock()
|
|
mock_guild.roles = []
|
|
mock_guild.members = []
|
|
|
|
content = "Already formatted: <@123456789> and <@&987654321>"
|
|
result = convert_mentions(content, mock_guild)
|
|
|
|
assert result == content
|
|
|
|
def test_preserves_unrecognized_mentions(self):
|
|
"""Test that @mentions with no match are left unchanged."""
|
|
mock_guild = MagicMock()
|
|
mock_guild.roles = []
|
|
mock_guild.members = []
|
|
|
|
content = "@UnknownRole says hello"
|
|
result = convert_mentions(content, mock_guild)
|
|
|
|
assert result == "@UnknownRole says hello"
|
|
|
|
def test_handles_none_guild(self):
|
|
"""Test that None guild returns content unchanged."""
|
|
content = "@SomeRole"
|
|
result = convert_mentions(content, None)
|
|
|
|
assert result == content
|
|
|
|
def test_handles_empty_content(self):
|
|
"""Test that empty content returns empty."""
|
|
mock_guild = MagicMock()
|
|
result = convert_mentions("", mock_guild)
|
|
|
|
assert result == ""
|
|
|
|
def test_handles_mentions_with_punctuation(self):
|
|
"""Test that mentions followed by punctuation are handled."""
|
|
mock_role = MagicMock()
|
|
mock_role.name = "TestRole"
|
|
mock_role.id = 123
|
|
|
|
mock_guild = MagicMock()
|
|
mock_guild.roles = [mock_role]
|
|
mock_guild.members = []
|
|
|
|
content = "Hello @TestRole!"
|
|
result = convert_mentions(content, mock_guild)
|
|
|
|
assert result == "Hello <@&123>!"
|
|
|
|
def test_role_takes_priority_over_user(self):
|
|
"""Test that when a name matches both role and user, role wins."""
|
|
mock_role = MagicMock()
|
|
mock_role.name = "SameName"
|
|
mock_role.id = 111
|
|
|
|
mock_member = MagicMock()
|
|
mock_member.display_name = "SameName"
|
|
mock_member.name = "samename"
|
|
mock_member.id = 222
|
|
|
|
mock_guild = MagicMock()
|
|
mock_guild.roles = [mock_role]
|
|
mock_guild.members = [mock_member]
|
|
|
|
content = "@SameName"
|
|
result = convert_mentions(content, mock_guild)
|
|
|
|
# Role should be matched first
|
|
assert result == "<@&111>"
|
|
|
|
|
|
class TestConvertMentionsAsync:
|
|
"""Tests for the async convert_mentions_async function."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_converts_role_mention(self):
|
|
"""Test async version converts mentions."""
|
|
mock_role = MagicMock()
|
|
mock_role.name = "TestRole"
|
|
mock_role.id = 123
|
|
|
|
mock_guild = MagicMock()
|
|
mock_guild.roles = [mock_role]
|
|
mock_guild.members = []
|
|
mock_guild.chunked = True
|
|
|
|
content = "@TestRole"
|
|
result = await convert_mentions_async(content, mock_guild)
|
|
|
|
assert result == "<@&123>"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_chunks_guild_when_requested(self):
|
|
"""Test that guild.chunk() is called when fetch_members=True and not chunked."""
|
|
mock_guild = MagicMock()
|
|
mock_guild.roles = []
|
|
mock_guild.members = []
|
|
mock_guild.chunked = False
|
|
mock_guild.chunk = AsyncMock()
|
|
|
|
await convert_mentions_async("@Test", mock_guild, fetch_members=True)
|
|
|
|
mock_guild.chunk.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_skips_chunk_when_already_chunked(self):
|
|
"""Test that guild.chunk() is not called when already chunked."""
|
|
mock_guild = MagicMock()
|
|
mock_guild.roles = []
|
|
mock_guild.members = []
|
|
mock_guild.chunked = True
|
|
mock_guild.chunk = AsyncMock()
|
|
|
|
await convert_mentions_async("@Test", mock_guild, fetch_members=True)
|
|
|
|
mock_guild.chunk.assert_not_called()
|