新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
330 lines
11 KiB
Python
330 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from yuxi.channels.models import ChannelIdentity, ChannelResponse, ChannelType, DeliveryResult
|
|
|
|
|
|
class TestNostrSenderFormatOutbound:
|
|
@pytest.fixture
|
|
def mock_components(self):
|
|
crypto = MagicMock()
|
|
crypto.build_and_sign_event.return_value = {
|
|
"id": "test_event_id",
|
|
"pubkey": "sender_hex",
|
|
"kind": 4,
|
|
"content": "test_content",
|
|
"tags": [],
|
|
"created_at": int(time.time()),
|
|
}
|
|
relay_manager = MagicMock()
|
|
relay_manager.broadcast = AsyncMock(return_value=2)
|
|
|
|
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_format_outbound_direct_message(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
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="hello")
|
|
event = await sender.format_outbound(response)
|
|
|
|
assert event["kind"] == 4
|
|
assert isinstance(event["content"], str)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_format_outbound_group_message(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
identity = ChannelIdentity(
|
|
channel_id="nostr",
|
|
channel_type=ChannelType.NOSTR,
|
|
channel_user_id="sender_hex",
|
|
channel_chat_id="channel:group_001",
|
|
)
|
|
response = ChannelResponse(identity=identity, content="hello group")
|
|
event = await sender.format_outbound(response)
|
|
|
|
assert event["kind"] == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_format_outbound_with_reply(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
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="reply text",
|
|
reply_to_message_id="parent_event_001",
|
|
)
|
|
event = await sender.format_outbound(response)
|
|
|
|
tags = event.get("tags", [])
|
|
e_tags = [t for t in tags if t[0] == "e"]
|
|
assert len(e_tags) >= 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_format_outbound_markdown_table_conversion(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
config.markdown_table_mode = "convert"
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
identity = ChannelIdentity(
|
|
channel_id="nostr",
|
|
channel_type=ChannelType.NOSTR,
|
|
channel_user_id="sender_hex",
|
|
channel_chat_id="dm:receiver_hex",
|
|
)
|
|
markdown_with_table = "| A | B |\n| --- | --- |\n| 1 | 2 |\n"
|
|
response = ChannelResponse(identity=identity, content=markdown_with_table)
|
|
event = await sender.format_outbound(response)
|
|
|
|
assert "|" not in event["content"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_format_outbound_markdown_no_conversion_when_off(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
config.markdown_table_mode = "off"
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
identity = ChannelIdentity(
|
|
channel_id="nostr",
|
|
channel_type=ChannelType.NOSTR,
|
|
channel_user_id="sender_hex",
|
|
channel_chat_id="dm:receiver_hex",
|
|
)
|
|
markdown_with_table = "plain text without tables"
|
|
response = ChannelResponse(identity=identity, content=markdown_with_table)
|
|
event = await sender.format_outbound(response)
|
|
|
|
assert "plain text" in event["content"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_success(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
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="hello world")
|
|
result = await sender.send(response)
|
|
|
|
assert result.success is True
|
|
assert result.message_id == "test_event_id"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_all_relays_fail(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
relay_manager.broadcast = AsyncMock(return_value=0)
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
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="hello world")
|
|
result = await sender.send(response)
|
|
|
|
assert result.success is False
|
|
|
|
|
|
class TestNostrSenderReactions:
|
|
@pytest.fixture
|
|
def mock_components(self):
|
|
crypto = MagicMock()
|
|
crypto.build_and_sign_event.return_value = {
|
|
"id": "reaction_event_id",
|
|
"kind": 7,
|
|
"content": "❤",
|
|
"tags": [],
|
|
"created_at": int(time.time()),
|
|
}
|
|
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_reaction_success(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
result = await sender.send_reaction("target_event_001", "target_pubkey_hex", "👍")
|
|
assert result.success is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_remove_reaction_success(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
result = await sender.remove_reaction("target_event_001", "target_pubkey_hex")
|
|
assert result.success is True
|
|
|
|
|
|
class TestNostrSenderEditDelete:
|
|
@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()),
|
|
}
|
|
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_edit_success(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
result = await sender.send_edit("original_event_001", "edited content", "receiver_hex")
|
|
assert result.success is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_delete_success(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
result = await sender.send_delete("original_event_001")
|
|
assert result.success is True
|
|
|
|
|
|
class TestTextChunking:
|
|
@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_short_text(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
chunks = sender._chunk_text("short text")
|
|
assert len(chunks) == 1
|
|
assert chunks[0] == "short text"
|
|
|
|
def test_chunk_long_text(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
chunk_size = 4096
|
|
base = "A" * (chunk_size - 10) + "\n"
|
|
text = base + "B" * 100
|
|
chunks = sender._chunk_text(text)
|
|
assert len(chunks) == 2
|
|
|
|
def test_chunk_at_newline_preferred(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
line1 = "A" * 2000 + "\n"
|
|
line2 = "B" * 3000
|
|
text = line1 + line2
|
|
chunks = sender._chunk_text(text)
|
|
assert len(chunks) == 2
|
|
|
|
def test_chunk_exact_limit(self, mock_components):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
crypto, relay_manager, config = mock_components
|
|
sender = NostrSender(crypto, relay_manager, config)
|
|
|
|
text = "A" * 4096
|
|
chunks = sender._chunk_text(text)
|
|
assert len(chunks) == 1
|
|
|
|
|
|
class TestMarkdownTableConversion:
|
|
def test_simple_table_converted(self):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
markdown = "| Name | Age |\n| --- | --- |\n| Alice | 30 |\n| Bob | 25 |\n"
|
|
result = NostrSender.convert_markdown_tables(markdown)
|
|
assert "|" not in result
|
|
assert "Alice" in result
|
|
assert "Bob" in result
|
|
|
|
def test_no_table_unchanged(self):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
text = "plain text without any tables"
|
|
result = NostrSender.convert_markdown_tables(text)
|
|
assert result == text
|
|
|
|
def test_aligned_table_converted(self):
|
|
from yuxi.channels.adapters.nostr.send import NostrSender
|
|
|
|
markdown = "| Left | Center | Right |\n| :--- | :---: | ---: |\n| a | b | c |\n"
|
|
result = NostrSender.convert_markdown_tables(markdown)
|
|
assert "|" not in result |