ForcePilot/backend/test/unit/channels/test_nostr_send_ext.py

248 lines
9.3 KiB
Python
Raw Normal View History

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