"""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="Hello" ) assert result.success is True mock_client.send_text.assert_called_once_with( "!room:example.org", text="Hello", html="Hello", ) @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="Notice" ) 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 bold", ) 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