from __future__ import annotations import time from unittest.mock import AsyncMock, MagicMock import pytest from yuxi.channels.adapters.nostr.send import NostrSender from yuxi.channels.models import ChannelIdentity, ChannelResponse, ChannelType, DeliveryResult class TestNostrSenderTypingAndMedia: @pytest.fixture def mock_components(self): crypto = MagicMock() crypto.build_and_sign_event.return_value = { "id": "typing_event", "kind": 4, "content": "typing", "tags": [], "created_at": int(time.time()), } crypto.encrypt_nip17 = AsyncMock(return_value='{"id":"gw","kind":1059}') crypto.encrypt_nip04.return_value = "encrypted_content" relay_manager = MagicMock() relay_manager.broadcast = AsyncMock(return_value=1) from yuxi.channels.adapters.nostr.config import NostrConfig config = NostrConfig(relays=["wss://test.relay"]) return crypto, relay_manager, config @pytest.mark.asyncio async def test_send_typing_success(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) result = await sender.send_typing("dm:receiver_hex", "receiver_hex") assert result.success is True @pytest.mark.asyncio async def test_send_typing_throttled(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) sender._typing_throttle["dm:receiver_hex"] = time.monotonic() result = await sender.send_typing("dm:receiver_hex", "receiver_hex") assert result.success is True assert result.message_id is None @pytest.mark.asyncio async def test_send_media_text_basic(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) result = await sender.send_media_text( "https://example.com/img.jpg", "Check this!", "receiver_hex" ) assert result.success is True @pytest.mark.asyncio async def test_send_media_text_group(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) result = await sender.send_media_text( "https://example.com/file.pdf", "Document", "", "group" ) assert result.success is True class TestNostrSenderEditDeleteExtended: @pytest.fixture def mock_components(self): crypto = MagicMock() crypto.build_and_sign_event.return_value = { "id": "edit_event_id", "kind": 4, "content": "edited", "tags": [], "created_at": int(time.time()), } crypto.encrypt_nip17 = AsyncMock(return_value='{"id":"gw","kind":1059}') crypto.encrypt_nip04.return_value = "encrypted_content" relay_manager = MagicMock() relay_manager.broadcast = AsyncMock(return_value=0) from yuxi.channels.adapters.nostr.config import NostrConfig config = NostrConfig(relays=["wss://test.relay"]) return crypto, relay_manager, config @pytest.mark.asyncio async def test_send_edit_all_relays_fail(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) result = await sender.send_edit("original_id", "new_content", "receiver_hex") assert result.success is False assert result.error is not None @pytest.mark.asyncio async def test_send_delete_all_relays_fail(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) result = await sender.send_delete("original_id") assert result.success is False @pytest.mark.asyncio async def test_remove_reaction_all_relays_fail(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) result = await sender.remove_reaction("target_id", "target_pubkey") assert result.success is False @pytest.mark.asyncio async def test_send_reaction_all_relays_fail(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) result = await sender.send_reaction("target_id", "target_pubkey", "👍") assert result.success is False @pytest.mark.asyncio async def test_send_media_text_all_relays_fail(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) result = await sender.send_media_text("https://img.url", "caption", "receiver_hex") assert result.success is False class TestNostrSenderChunkingExtended: @pytest.fixture def mock_components(self): crypto = MagicMock() relay_manager = MagicMock() from yuxi.channels.adapters.nostr.config import NostrConfig config = NostrConfig(relays=["wss://test.relay"]) return crypto, relay_manager, config def test_chunk_at_whitespace_fallback(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) chunk_size = 4096 text = "A" * (chunk_size - 5) + " " + "B" * 100 chunks = sender._chunk_text(text) assert len(chunks) == 2 def test_chunk_no_newline_no_space(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) chunk_size = 4096 text = "A" * (chunk_size + 200) chunks = sender._chunk_text(text) assert len(chunks) == 2 def test_chunk_multiple_splits(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) chunk_size = 4096 part = "A" * (chunk_size - 1000) text = part + "\n" + part + "\n" + part chunks = sender._chunk_text(text) assert len(chunks) == 3 def test_chunk_space_split_in_second_half(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) chunk_size = 4096 text = "A" * (chunk_size + 500) + " B" * 100 chunks = sender._chunk_text(text) assert len(chunks) == 2 class TestMarkdownTableExtended: def test_table_with_alignment_all_left(self): from yuxi.channels.adapters.nostr.send import NostrSender markdown = "| A | B | C |\n| :--- | :--- | :--- |\n| 1 | 2 | 3 |\n" result = NostrSender.convert_markdown_tables(markdown) assert "|" not in result assert "A" in result assert "1" in result def test_table_with_mixed_alignment(self): from yuxi.channels.adapters.nostr.send import NostrSender markdown = "| Left | Center | Right |\n| :--- | :---: | ---: |\n| a | b | c |\n| d | e | f |\n" result = NostrSender.convert_markdown_tables(markdown) assert "|" not in result assert "a" in result assert "f" in result def test_no_table_mixed_content(self): from yuxi.channels.adapters.nostr.send import NostrSender text = "Some intro text\n| A | B |\n| --- | --- |\n| 1 | 2 |\nMore text after" result = NostrSender.convert_markdown_tables(text) assert "Some intro" in result assert "More text" in result def test_multiple_tables(self): from yuxi.channels.adapters.nostr.send import NostrSender markdown = "| X | Y |\n| --- | --- |\n| 1 | 2 |\n\n| P | Q |\n| --- | --- |\n| 3 | 4 |\n" result = NostrSender.convert_markdown_tables(markdown) lines = result.strip().split("\n") assert len(lines) >= 4 class TestNostrSenderSendProgress: @pytest.fixture def mock_components(self): crypto = MagicMock() crypto.build_and_sign_event.return_value = { "id": "chunk_event", "kind": 4, "content": "chunk_content", "tags": [], "created_at": int(time.time()), } crypto.encrypt_nip17 = AsyncMock(return_value='{"id":"gw","kind":1059}') crypto.encrypt_nip04.return_value = "encrypted_content" relay_manager = MagicMock() relay_manager.broadcast = AsyncMock(return_value=1) from yuxi.channels.adapters.nostr.config import NostrConfig config = NostrConfig(relays=["wss://test.relay"]) return crypto, relay_manager, config @pytest.mark.asyncio async def test_send_long_message_chunked(self, mock_components): crypto, relay_manager, config = mock_components sender = NostrSender(crypto, relay_manager, config) long_content = "A" * 5000 identity = ChannelIdentity( channel_id="nostr", channel_type=ChannelType.NOSTR, channel_user_id="sender_hex", channel_chat_id="dm:receiver_hex", ) response = ChannelResponse(identity=identity, content=long_content) result = await sender.send(response) assert result.success is True assert result.message_id is not None