Files
matrix-hermes/tests/test_messaging.py
shawn 3ddf8170f8 feat: Matrix connector core — rooms, messaging, sync
- rooms: create, join, leave, forget, list, resolve alias, get info/members,
  invite/kick/ban/unban, set name/topic. RoomInfo/MemberInfo/PowerLevels dataclasses.
- messaging: send text (with HTML, reply, thread), notice, emote, image, file,
  sticker, edit, redact, react, typing, read receipts, fully-read markers.
- sync: SyncEngine with event-type/room/global handler dispatch,
  incremental sync with backoff retry, SyncFilter, paginated message history.
- CLI: rooms (list/create/join/leave/info/members/invite), send (text/image/file),
  sync (listen with filters).
- 182 tests pass (60 existing + 122 new).
2026-05-23 12:07:10 -04:00

361 lines
13 KiB
Python

"""Test Matrix messaging: send text, media, edits, reactions, typing, receipts."""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from matrix_hermes.messaging import (
SendResult,
edit_message,
redact_message,
send_emote,
send_file,
send_image,
send_notice,
send_reaction,
send_read_receipt,
send_sticker,
send_text,
send_typing,
set_fully_read,
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def mock_client():
"""Return a mock mautrix Client with all messaging methods as AsyncMock."""
client = MagicMock()
client.send_text = AsyncMock(return_value="$event123:example.org")
client.send_message = AsyncMock(return_value="$event456:example.org")
client.send_message_event = AsyncMock(return_value="$edit789:example.org")
client.send_emote = AsyncMock(return_value="$emote000:example.org")
client.send_image = AsyncMock(return_value="$img111:example.org")
client.send_file = AsyncMock(return_value="$file222:example.org")
client.send_sticker = AsyncMock(return_value="$sticker333:example.org")
client.redact = AsyncMock()
client.react = AsyncMock()
client.set_typing = AsyncMock()
client.send_receipt = AsyncMock()
client.set_fully_read_marker = AsyncMock()
return client
# ---------------------------------------------------------------------------
# Data class tests
# ---------------------------------------------------------------------------
class TestSendResult:
def test_success(self) -> None:
r = SendResult(success=True, event_id="$abc:example.org")
assert r.success is True
assert r.event_id == "$abc:example.org"
assert r.error == ""
def test_failure(self) -> None:
r = SendResult(success=False, error="Rate limited")
assert r.success is False
assert r.error == "Rate limited"
assert r.event_id == ""
def test_default_values(self) -> None:
r = SendResult(success=True)
assert r.event_id == ""
assert r.error == ""
# ---------------------------------------------------------------------------
# Core messaging
# ---------------------------------------------------------------------------
class TestSendText:
@pytest.mark.asyncio
async def test_send_simple(self, mock_client) -> None:
result = await send_text(mock_client, "!room:example.org", "Hello world")
assert result.success is True
assert result.event_id == "$event123:example.org"
mock_client.send_text.assert_called_once_with(
"!room:example.org",
text="Hello world",
html=None,
)
@pytest.mark.asyncio
async def test_send_with_html(self, mock_client) -> None:
result = await send_text(
mock_client, "!room:example.org", "Hello", html="<b>Hello</b>"
)
assert result.success is True
mock_client.send_text.assert_called_once_with(
"!room:example.org",
text="Hello",
html="<b>Hello</b>",
)
@pytest.mark.asyncio
async def test_send_with_reply(self, mock_client) -> None:
result = await send_text(
mock_client, "!room:example.org", "Replying!", reply_to="$orig:example.org"
)
assert result.success is True
call_kwargs = mock_client.send_text.call_args[1]
assert "relates_to" in call_kwargs
assert call_kwargs["relates_to"]["m.in_reply_to"]["event_id"] == "$orig:example.org"
@pytest.mark.asyncio
async def test_send_in_thread(self, mock_client) -> None:
result = await send_text(
mock_client, "!room:example.org", "Thread reply", thread_root="$thread:example.org"
)
assert result.success is True
call_kwargs = mock_client.send_text.call_args[1]
assert "relates_to" in call_kwargs
assert call_kwargs["relates_to"]["rel_type"] == "m.thread"
assert call_kwargs["relates_to"]["event_id"] == "$thread:example.org"
@pytest.mark.asyncio
async def test_send_failure(self, mock_client) -> None:
mock_client.send_text.side_effect = Exception("Network error")
result = await send_text(mock_client, "!room:example.org", "fail")
assert result.success is False
assert "Network error" in result.error
class TestSendNotice:
@pytest.mark.asyncio
async def test_send_notice(self, mock_client) -> None:
result = await send_notice(mock_client, "!room:example.org", "Notice text")
assert result.success is True
assert result.event_id == "$event456:example.org"
@pytest.mark.asyncio
async def test_send_notice_with_html(self, mock_client) -> None:
result = await send_notice(
mock_client, "!room:example.org", "Notice", html="<i>Notice</i>"
)
assert result.success is True
@pytest.mark.asyncio
async def test_send_notice_failure(self, mock_client) -> None:
mock_client.send_message.side_effect = Exception("Fail")
result = await send_notice(mock_client, "!room:example.org", "fail")
assert result.success is False
class TestSendEmote:
@pytest.mark.asyncio
async def test_send_emote(self, mock_client) -> None:
result = await send_emote(mock_client, "!room:example.org", "waves")
assert result.success is True
assert result.event_id == "$emote000:example.org"
mock_client.send_emote.assert_called_once_with("!room:example.org", "waves")
@pytest.mark.asyncio
async def test_send_emote_failure(self, mock_client) -> None:
mock_client.send_emote.side_effect = Exception("Fail")
result = await send_emote(mock_client, "!room:example.org", "fail")
assert result.success is False
# ---------------------------------------------------------------------------
# Media
# ---------------------------------------------------------------------------
class TestSendImage:
@pytest.mark.asyncio
async def test_send_image(self, mock_client) -> None:
result = await send_image(mock_client, "!room:example.org", "/tmp/photo.jpg")
assert result.success is True
assert result.event_id == "$img111:example.org"
mock_client.send_image.assert_called_once_with(
"!room:example.org",
"/tmp/photo.jpg",
caption=None,
thread_root=None,
)
@pytest.mark.asyncio
async def test_send_image_with_caption(self, mock_client) -> None:
result = await send_image(
mock_client, "!room:example.org", "/tmp/photo.jpg", caption="My photo"
)
assert result.success is True
@pytest.mark.asyncio
async def test_send_image_failure(self, mock_client) -> None:
mock_client.send_image.side_effect = Exception("Upload failed")
result = await send_image(mock_client, "!room:example.org", "/tmp/bad.jpg")
assert result.success is False
class TestSendFile:
@pytest.mark.asyncio
async def test_send_file(self, mock_client) -> None:
result = await send_file(
mock_client, "!room:example.org", "/tmp/doc.pdf", filename="report.pdf"
)
assert result.success is True
assert result.event_id == "$file222:example.org"
@pytest.mark.asyncio
async def test_send_file_failure(self, mock_client) -> None:
mock_client.send_file.side_effect = Exception("Upload failed")
result = await send_file(mock_client, "!room:example.org", "/tmp/bad.pdf")
assert result.success is False
class TestSendSticker:
@pytest.mark.asyncio
async def test_send_sticker(self, mock_client) -> None:
result = await send_sticker(mock_client, "!room:example.org", "/tmp/sticker.png")
assert result.success is True
mock_client.send_sticker.assert_called_once_with("!room:example.org", "/tmp/sticker.png")
@pytest.mark.asyncio
async def test_send_sticker_failure(self, mock_client) -> None:
mock_client.send_sticker.side_effect = Exception("Fail")
result = await send_sticker(mock_client, "!room:example.org", "/tmp/sticker.png")
assert result.success is False
# ---------------------------------------------------------------------------
# Editing & redaction
# ---------------------------------------------------------------------------
class TestEditMessage:
@pytest.mark.asyncio
async def test_edit_message(self, mock_client) -> None:
result = await edit_message(
mock_client, "!room:example.org", "$orig:example.org", "Fixed text"
)
assert result.success is True
assert result.event_id == "$edit789:example.org"
@pytest.mark.asyncio
async def test_edit_message_with_html(self, mock_client) -> None:
result = await edit_message(
mock_client,
"!room:example.org",
"$orig:example.org",
"Fixed **bold**",
new_html="Fixed <b>bold</b>",
)
assert result.success is True
@pytest.mark.asyncio
async def test_edit_message_failure(self, mock_client) -> None:
mock_client.send_message_event.side_effect = Exception("Fail")
result = await edit_message(
mock_client, "!room:example.org", "$orig:example.org", "edit"
)
assert result.success is False
class TestRedactMessage:
@pytest.mark.asyncio
async def test_redact_success(self, mock_client) -> None:
ok = await redact_message(mock_client, "!room:example.org", "$msg:example.org")
assert ok is True
@pytest.mark.asyncio
async def test_redact_with_reason(self, mock_client) -> None:
ok = await redact_message(
mock_client, "!room:example.org", "$msg:example.org", reason="Offensive"
)
assert ok is True
@pytest.mark.asyncio
async def test_redact_failure(self, mock_client) -> None:
mock_client.redact.side_effect = Exception("No permission")
ok = await redact_message(mock_client, "!room:example.org", "$msg:example.org")
assert ok is False
# ---------------------------------------------------------------------------
# Reactions
# ---------------------------------------------------------------------------
class TestSendReaction:
@pytest.mark.asyncio
async def test_send_reaction(self, mock_client) -> None:
ok = await send_reaction(
mock_client, "!room:example.org", "$msg:example.org", "👍"
)
assert ok is True
mock_client.react.assert_called_once_with("!room:example.org", "$msg:example.org", "👍")
@pytest.mark.asyncio
async def test_send_reaction_failure(self, mock_client) -> None:
mock_client.react.side_effect = Exception("Fail")
ok = await send_reaction(mock_client, "!room:example.org", "$msg:example.org", "❤️")
assert ok is False
# ---------------------------------------------------------------------------
# Typing & receipts
# ---------------------------------------------------------------------------
class TestSendTyping:
@pytest.mark.asyncio
async def test_typing_on(self, mock_client) -> None:
ok = await send_typing(mock_client, "!room:example.org", typing=True, timeout=5000)
assert ok is True
mock_client.set_typing.assert_called_once_with(
"!room:example.org", typing=True, timeout=5000
)
@pytest.mark.asyncio
async def test_typing_off(self, mock_client) -> None:
ok = await send_typing(mock_client, "!room:example.org", typing=False)
assert ok is True
@pytest.mark.asyncio
async def test_typing_failure(self, mock_client) -> None:
mock_client.set_typing.side_effect = Exception("Fail")
ok = await send_typing(mock_client, "!room:example.org")
assert ok is False
class TestSendReadReceipt:
@pytest.mark.asyncio
async def test_send_receipt(self, mock_client) -> None:
ok = await send_read_receipt(mock_client, "!room:example.org", "$msg:example.org")
assert ok is True
mock_client.send_receipt.assert_called_once_with(
"!room:example.org", "$msg:example.org", "m.read"
)
@pytest.mark.asyncio
async def test_send_receipt_failure(self, mock_client) -> None:
mock_client.send_receipt.side_effect = Exception("Fail")
ok = await send_read_receipt(mock_client, "!room:example.org", "$msg:example.org")
assert ok is False
class TestSetFullyRead:
@pytest.mark.asyncio
async def test_set_fully_read(self, mock_client) -> None:
ok = await set_fully_read(mock_client, "!room:example.org", "$msg:example.org")
assert ok is True
mock_client.set_fully_read_marker.assert_called_once_with(
"!room:example.org", "$msg:example.org"
)
@pytest.mark.asyncio
async def test_set_fully_read_failure(self, mock_client) -> None:
mock_client.set_fully_read_marker.side_effect = Exception("Fail")
ok = await set_fully_read(mock_client, "!room:example.org", "$msg:example.org")
assert ok is False