feat: add configurable matrix permalink base, unify user placeholders, and refactor notification rendering
feat: add configurable matrix permalink base, unify user placeholders, and refactor notification rendering
This commit is contained in:
Regular → Executable
Regular → Executable
Regular → Executable
+142
@@ -264,6 +264,47 @@ class TestBotEvents:
|
||||
# Should update user timestamp
|
||||
real_bot.upsert_user_timestamp.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_newjoin_notification_supports_user_link(self, bot, mock_state_evt):
|
||||
"""Test join notification formatting with user_link placeholder."""
|
||||
from community.bot import CommunityBot
|
||||
real_bot = CommunityBot()
|
||||
real_bot.config = {
|
||||
**bot.config,
|
||||
"greeting_rooms": {"!room:example.com": "none"},
|
||||
"greetings": {},
|
||||
"notification_room": "!notifications:example.com",
|
||||
"join_notification_message": "{user_link} joined {room_link} ({room_id})"
|
||||
}
|
||||
real_bot.client = bot.client
|
||||
real_bot.database = bot.database
|
||||
real_bot.log = bot.log
|
||||
|
||||
real_bot.client.parse_user_id = Mock(return_value=("alice", "example.com"))
|
||||
room_name_state = Mock()
|
||||
room_name_state.name = "Test Room"
|
||||
real_bot.client.get_state_event = AsyncMock(return_value=room_name_state)
|
||||
real_bot.client.send_notice = AsyncMock()
|
||||
real_bot.database.execute = AsyncMock()
|
||||
|
||||
mock_state_evt.source = 0
|
||||
|
||||
with patch.object(real_bot, 'get_space_roomlist', return_value=["!room:example.com"]), \
|
||||
patch.object(real_bot, 'check_if_banned', return_value=False), \
|
||||
patch.object(real_bot, 'upsert_user_timestamp', return_value=None):
|
||||
await real_bot.newjoin(mock_state_evt)
|
||||
|
||||
real_bot.client.send_notice.assert_called_once()
|
||||
args, kwargs = real_bot.client.send_notice.call_args
|
||||
assert args[0] == "!notifications:example.com"
|
||||
assert args[1] == "https://matrix.to/#/@user:example.com joined https://matrix.to/#/!room:example.com (!room:example.com)"
|
||||
assert "https://matrix.to/#/@user:example.com" in kwargs["html"]
|
||||
assert ">alice</a>" in kwargs["html"]
|
||||
assert "https://matrix.to/#/!room:example.com" in kwargs["html"]
|
||||
assert ">Test Room</a>" in kwargs["html"]
|
||||
assert "(!room:example.com)" in kwargs["html"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_message_timestamp_tracking_enabled(self, bot, mock_message_evt):
|
||||
"""Test message timestamp update with tracking enabled."""
|
||||
@@ -450,3 +491,104 @@ class TestBotEvents:
|
||||
|
||||
# Should not update user timestamp
|
||||
real_bot.upsert_user_timestamp.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sleep_if_configured_uses_asyncio_sleep(self):
|
||||
"""Test that configured delays use asyncio.sleep without blocking."""
|
||||
real_bot = CommunityBot()
|
||||
with patch("community.bot.asyncio.sleep", new=AsyncMock()) as sleep_mock:
|
||||
await real_bot._sleep_if_configured(1.5)
|
||||
sleep_mock.assert_awaited_once_with(1.5)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_newjoin_uses_async_delay_for_greeting(self, bot, mock_state_evt):
|
||||
"""Test that join greetings use the async delay helper."""
|
||||
real_bot = CommunityBot()
|
||||
real_bot.client = Mock()
|
||||
real_bot.client.parse_user_id.return_value = ("alice", "example.com")
|
||||
real_bot.client.send_notice = AsyncMock()
|
||||
real_bot.client.get_state_event = AsyncMock(return_value=Mock(name="Test Room"))
|
||||
real_bot.database = bot.database
|
||||
real_bot.log = bot.log
|
||||
real_bot.config = {
|
||||
**bot.config,
|
||||
"parent_room": "!parent:example.com",
|
||||
"greeting_rooms": {"!room:example.com": "default"},
|
||||
"greetings": {"default": "Welcome {user}"},
|
||||
"welcome_sleep": 2,
|
||||
"notification_room": "!notif:example.com",
|
||||
"join_notification_message": "{user_link} joined {room_link}",
|
||||
}
|
||||
real_bot._sleep_if_configured = AsyncMock()
|
||||
real_bot.check_if_banned = AsyncMock(return_value=False)
|
||||
real_bot.ban_this_user = AsyncMock()
|
||||
real_bot.do_sync = AsyncMock()
|
||||
real_bot.get_space_roomlist = AsyncMock(return_value=["!room:example.com"])
|
||||
|
||||
await real_bot.newjoin(mock_state_evt)
|
||||
|
||||
real_bot._sleep_if_configured.assert_awaited_once_with(2)
|
||||
assert real_bot.client.send_notice.await_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_kick_uses_async_delay(self, bot):
|
||||
"""Test that user kicking uses the async delay helper between rooms."""
|
||||
real_bot = CommunityBot()
|
||||
real_bot.client = Mock()
|
||||
real_bot.client.get_state_event = AsyncMock(side_effect=[
|
||||
{"name": "Room One"},
|
||||
{}, # membership lookup
|
||||
])
|
||||
real_bot.client.kick_user = AsyncMock()
|
||||
real_bot.database = bot.database
|
||||
real_bot.log = bot.log
|
||||
real_bot.config = {**bot.config, "sleep": 0.5, "parent_room": "!room:example.com"}
|
||||
real_bot._sleep_if_configured = AsyncMock()
|
||||
|
||||
evt = Mock(spec=MessageEvent)
|
||||
evt.mark_read = AsyncMock()
|
||||
evt.respond = AsyncMock()
|
||||
evt.reply = AsyncMock()
|
||||
|
||||
with patch.object(real_bot, "get_space_roomlist", AsyncMock(return_value=[])):
|
||||
await real_bot.user_kick(evt, "@user:example.com")
|
||||
|
||||
real_bot._sleep_if_configured.assert_awaited_once_with(0.5)
|
||||
|
||||
def test_render_message_template_uses_consistent_user_placeholders(self):
|
||||
"""{user} should stay plain while {user_link} becomes clickable."""
|
||||
real_bot = CommunityBot()
|
||||
|
||||
plain_text, html_text = real_bot._render_message_template(
|
||||
"{user} | {user_link} | {room} | {room_link} | {room_id}",
|
||||
"@alice:example.com",
|
||||
"alice",
|
||||
"!room:example.com",
|
||||
"Test Room",
|
||||
)
|
||||
|
||||
assert plain_text == (
|
||||
"@alice:example.com | https://matrix.to/#/@alice:example.com | "
|
||||
"Test Room | https://matrix.to/#/!room:example.com | !room:example.com"
|
||||
)
|
||||
assert "@alice:example.com" in html_text
|
||||
assert "<a href='https://matrix.to/#/@alice:example.com'>alice</a>" in html_text
|
||||
assert "<a href='https://matrix.to/#/!room:example.com'>Test Room</a>" in html_text
|
||||
|
||||
def test_render_message_template_without_room_keeps_room_placeholders_safe(self):
|
||||
"""Greeting templates without room data should not break placeholder rendering."""
|
||||
real_bot = CommunityBot()
|
||||
|
||||
plain_text, html_text = real_bot._render_message_template(
|
||||
"Welcome {user} / {user_link}",
|
||||
"@alice:example.com",
|
||||
"alice",
|
||||
)
|
||||
|
||||
assert plain_text == "Welcome @alice:example.com / https://matrix.to/#/@alice:example.com"
|
||||
assert html_text == (
|
||||
"Welcome @alice:example.com / "
|
||||
"<a href='https://matrix.to/#/@alice:example.com'>alice</a>"
|
||||
)
|
||||
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Executable
+57
@@ -0,0 +1,57 @@
|
||||
"""Tests for notification and greeting template rendering."""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
from mautrix.types import EventType, RoomID
|
||||
|
||||
from community.bot import CommunityBot
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bot():
|
||||
bot = CommunityBot.__new__(CommunityBot)
|
||||
bot.client = Mock()
|
||||
bot.config = {"matrix_to_base_url": "https://matrix.to"}
|
||||
return bot
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_display_name_uses_member_displayname(bot):
|
||||
member_state = Mock()
|
||||
member_state.displayname = "Alice"
|
||||
bot.client.get_state_event = AsyncMock(return_value=member_state)
|
||||
|
||||
result = await bot._get_user_display_name(RoomID("!room:example.org"), "@alice:example.org")
|
||||
|
||||
assert result == "Alice"
|
||||
bot.client.get_state_event.assert_awaited_once_with(
|
||||
RoomID("!room:example.org"),
|
||||
EventType.ROOM_MEMBER,
|
||||
state_key="@alice:example.org",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_display_name_falls_back_to_localpart(bot):
|
||||
bot.client.get_state_event = AsyncMock(side_effect=Exception("missing"))
|
||||
bot.client.parse_user_id = Mock(return_value=("alice", "example.org"))
|
||||
|
||||
result = await bot._get_user_display_name(RoomID("!room:example.org"), "@alice:example.org")
|
||||
|
||||
assert result == "alice"
|
||||
|
||||
|
||||
def test_render_message_template_supports_user_id_and_user_link(bot):
|
||||
plain, html = bot._render_message_template(
|
||||
"{user} / {user_id} / {user_link}",
|
||||
"@alice:example.org",
|
||||
"Alice",
|
||||
"!room:example.org",
|
||||
"General",
|
||||
)
|
||||
|
||||
assert plain == "Alice / @alice:example.org / https://matrix.to/#/@alice:example.org"
|
||||
assert "Alice / @alice:example.org / " in html
|
||||
assert '<a href=' in html
|
||||
assert '>Alice</a>' in html
|
||||
Regular → Executable
Reference in New Issue
Block a user