1. 移除Telegram格式化测试中未使用的导入项 2. 修复Teams测试用例,添加monkeypatch参数并配置通配符开关 3. 更新钉钉适配器测试,替换弃用的流属性检查 4. 修正Twitch规范化测试,更新ROOMSTATE测试逻辑 5. 重构会话映射测试,完善数据库执行结果模拟 6. 格式化Slack块构建测试的长参数调用 7. 修复LINE适配器测试,更新能力断言和异步锁使用 8. 修正Slack会话解析测试,修复聊天类型判断错误 9. 更新能力测试,补充缺失的字段检查 10. 修复Matrix适配器测试,修正位置参数和配置校验逻辑 11. 为飞书分析模块测试添加跳过标记 12. 新增微信能力、限流、链接格式、会话路由等模块的单元测试 13. 修复Twitch适配器导入路径和测试断言 14. 新增Discord Webhook、Nextcloud Talk、Signal多账户等模块的单元测试 15. 修复Manager阶段测试的导入路径 16. 新增iMessage异常和命令处理的单元测试 17. 新增Nostr健康检查和相关模块的单元测试 18. 新增Signal守护进程和SSE重连相关测试
693 lines
24 KiB
Python
693 lines
24 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json as _json
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from yuxi.channels.adapters.nostr.adapter import NostrAdapter
|
|
from yuxi.channels.models import (
|
|
Attachment,
|
|
ChannelIdentity,
|
|
ChannelMessage,
|
|
ChannelResponse,
|
|
ChannelType,
|
|
ChatType,
|
|
DeliveryResult,
|
|
EventType,
|
|
MessageType,
|
|
)
|
|
|
|
|
|
class AsyncContextManagerMock:
|
|
def __init__(self, return_value=None):
|
|
self.return_value = return_value
|
|
|
|
async def __aenter__(self):
|
|
return self.return_value
|
|
|
|
async def __aexit__(self, *args, **kwargs):
|
|
pass
|
|
|
|
|
|
class TestNostrAdapterSendMethods:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_no_sender(self, adapter):
|
|
identity = ChannelIdentity(
|
|
channel_id="nostr", channel_type=ChannelType.NOSTR,
|
|
channel_user_id="user", channel_chat_id="dm:receiver",
|
|
)
|
|
response = ChannelResponse(identity=identity, content="test")
|
|
result = await adapter.send(response)
|
|
assert result.success is False
|
|
assert "Sender 未初始化" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_with_mock_sender(self, adapter):
|
|
mock_sender = MagicMock()
|
|
mock_sender.send = AsyncMock(return_value=DeliveryResult(success=True, message_id="evt_id"))
|
|
adapter._sender = mock_sender
|
|
identity = ChannelIdentity(
|
|
channel_id="nostr", channel_type=ChannelType.NOSTR,
|
|
channel_user_id="user", channel_chat_id="dm:receiver",
|
|
)
|
|
response = ChannelResponse(identity=identity, content="hello")
|
|
result = await adapter.send(response)
|
|
assert result.success is True
|
|
assert result.message_id == "evt_id"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_no_sender(self, adapter):
|
|
result = await adapter.send_media("dm:receiver", "image", "http://img.url")
|
|
assert result.success is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_message_no_sender(self, adapter):
|
|
result = await adapter.edit_message("dm:receiver", "msg_001", "edited")
|
|
assert result.success is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_message_no_sender(self, adapter):
|
|
result = await adapter.delete_message("dm:receiver", "msg_001")
|
|
assert result.success is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_reaction_no_sender(self, adapter):
|
|
result = await adapter.send_reaction("dm:receiver", "msg_001", "❤")
|
|
assert result.success is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_remove_reaction_no_sender(self, adapter):
|
|
result = await adapter.remove_reaction("dm:receiver", "msg_001")
|
|
assert result.success is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_chat_action_no_sender(self, adapter):
|
|
result = await adapter.send_chat_action("dm:receiver", "typing")
|
|
assert result.success is False
|
|
|
|
|
|
class TestNormalizeInboundExtended:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
def test_normalize_kind5_deletion(self, adapter):
|
|
raw = {
|
|
"id": "del_001",
|
|
"kind": 5,
|
|
"content": "",
|
|
"pubkey": "author_hex",
|
|
"tags": [
|
|
["e", "deleted_event_001"],
|
|
["e", "deleted_event_002"],
|
|
],
|
|
"created_at": 1715000000,
|
|
}
|
|
msg = adapter.normalize_inbound(raw)
|
|
assert msg.event_type == EventType.MESSAGE_DELETED
|
|
assert "deleted_event_001" in msg.content
|
|
|
|
def test_normalize_kind5_deletion_with_p_tag(self, adapter):
|
|
raw = {
|
|
"id": "del_dm",
|
|
"kind": 5,
|
|
"content": "",
|
|
"pubkey": "sender_hex",
|
|
"tags": [
|
|
["e", "evt_to_delete"],
|
|
["p", "receiver_hex"],
|
|
],
|
|
"created_at": 1715000000,
|
|
}
|
|
msg = adapter.normalize_inbound(raw)
|
|
assert msg.chat_type == ChatType.DIRECT
|
|
|
|
def test_normalize_kind7_reaction_removed_empty_content(self, adapter):
|
|
raw = {
|
|
"id": "react_rem_001",
|
|
"kind": 7,
|
|
"content": "",
|
|
"pubkey": "sender_hex",
|
|
"tags": [
|
|
["e", "original_event_id"],
|
|
],
|
|
"created_at": 1715000000,
|
|
}
|
|
msg = adapter.normalize_inbound(raw)
|
|
assert msg.event_type == EventType.REACTION_REMOVED
|
|
|
|
def test_normalize_kind7_reaction_removed_plus(self, adapter):
|
|
raw = {
|
|
"id": "react_rem_002",
|
|
"kind": 7,
|
|
"content": "+",
|
|
"pubkey": "sender_hex",
|
|
"tags": [
|
|
["e", "original_event_id"],
|
|
],
|
|
"created_at": 1715000000,
|
|
}
|
|
msg = adapter.normalize_inbound(raw)
|
|
assert msg.event_type == EventType.REACTION_REMOVED
|
|
|
|
def test_normalize_kind7_reaction_no_e_tag(self, adapter):
|
|
raw = {
|
|
"id": "react_no_e",
|
|
"kind": 7,
|
|
"content": "👍",
|
|
"pubkey": "sender_hex",
|
|
"tags": [],
|
|
"created_at": 1715000000,
|
|
}
|
|
msg = adapter.normalize_inbound(raw)
|
|
assert msg.event_type == EventType.REACTION_ADDED
|
|
|
|
def test_normalize_kind1059_direct(self, adapter):
|
|
raw = {
|
|
"id": "nip17_001",
|
|
"kind": 1059,
|
|
"content": "gift_wrapped_json",
|
|
"pubkey": "sender_hex",
|
|
"tags": [],
|
|
"created_at": 1715000000,
|
|
}
|
|
msg = adapter.normalize_inbound(raw)
|
|
assert msg.chat_type == ChatType.DIRECT
|
|
|
|
def test_normalize_edit_event(self, adapter):
|
|
raw = {
|
|
"id": "edit_001",
|
|
"kind": 1,
|
|
"content": "edited content",
|
|
"pubkey": "editor_hex",
|
|
"tags": [
|
|
["e", "original_event_id", "", "edit"],
|
|
],
|
|
"created_at": 1715000000,
|
|
}
|
|
msg = adapter.normalize_inbound(raw)
|
|
assert msg.event_type == EventType.MESSAGE_UPDATED
|
|
|
|
def test_normalize_kind1_group_multiple_p_tags(self, adapter):
|
|
raw = {
|
|
"id": "group_msg",
|
|
"kind": 1,
|
|
"content": "group chat message",
|
|
"pubkey": "sender_hex",
|
|
"tags": [
|
|
["p", "user1"],
|
|
["p", "user2"],
|
|
["e", "root_event", "", "root"],
|
|
],
|
|
"created_at": 1715000000,
|
|
}
|
|
msg = adapter.normalize_inbound(raw)
|
|
assert msg.chat_type == ChatType.GROUP
|
|
|
|
|
|
class TestExtractAttachments:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
def test_extract_url_tag_attachment(self, adapter):
|
|
raw = {
|
|
"id": "media_001",
|
|
"kind": 1,
|
|
"content": "check this image",
|
|
"pubkey": "sender_hex",
|
|
"tags": [
|
|
["url", "https://example.com/image.jpg"],
|
|
],
|
|
"created_at": 1715000000,
|
|
}
|
|
msg = adapter.normalize_inbound(raw)
|
|
assert len(msg.attachments) == 1
|
|
assert msg.attachments[0].type == "image"
|
|
|
|
def test_extract_imeta_tag_attachment(self, adapter):
|
|
raw = {
|
|
"id": "media_002",
|
|
"kind": 1,
|
|
"content": "",
|
|
"pubkey": "sender_hex",
|
|
"tags": [
|
|
["imeta", "https://example.com/photo.png"],
|
|
],
|
|
"created_at": 1715000000,
|
|
}
|
|
msg = adapter.normalize_inbound(raw)
|
|
assert len(msg.attachments) == 1
|
|
assert msg.attachments[0].type == "image"
|
|
|
|
def test_extract_content_url_fallback(self, adapter):
|
|
raw = {
|
|
"id": "media_003",
|
|
"kind": 1,
|
|
"content": "https://example.com/video.mp4",
|
|
"pubkey": "sender_hex",
|
|
"tags": [],
|
|
"created_at": 1715000000,
|
|
}
|
|
msg = adapter.normalize_inbound(raw)
|
|
assert len(msg.attachments) == 1
|
|
assert msg.attachments[0].type == "video"
|
|
|
|
def test_guess_media_type_audio(self, adapter):
|
|
assert adapter._guess_media_type("song.mp3") == "audio"
|
|
|
|
def test_guess_media_type_file(self, adapter):
|
|
assert adapter._guess_media_type("document.pdf") == "file"
|
|
|
|
def test_no_attachments(self, adapter):
|
|
raw = {
|
|
"id": "no_media",
|
|
"kind": 1,
|
|
"content": "plain text",
|
|
"pubkey": "sender_hex",
|
|
"tags": [],
|
|
"created_at": 1715000000,
|
|
}
|
|
msg = adapter.normalize_inbound(raw)
|
|
assert len(msg.attachments) == 0
|
|
|
|
|
|
class TestExtractMentions:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
def test_extract_mentions_hex(self, adapter):
|
|
hex_key = "79c2cae114ea28a981e7559b4fe7854a473521a8d22a66bbab9fa248eb820ff6"
|
|
mentions = adapter._extract_mentions(f"nostr:{hex_key}")
|
|
assert hex_key in mentions
|
|
|
|
def test_extract_mentions_no_match(self, adapter):
|
|
mentions = adapter._extract_mentions("hello world")
|
|
assert mentions == []
|
|
|
|
def test_extract_mentions_at_prefix(self, adapter):
|
|
mentions = adapter._extract_mentions("hello @npub1user")
|
|
assert len(mentions) == 0
|
|
|
|
|
|
class TestHealthCheckExtended:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_health_check_no_relays(self, adapter):
|
|
adapter._relay_manager = MagicMock()
|
|
adapter._relay_manager.active_count.return_value = (0, 0)
|
|
adapter._nostr_config = MagicMock()
|
|
adapter._nostr_config.relays = []
|
|
adapter._nostr_config.relay_degraded_threshold = 0.5
|
|
result = await adapter.health_check()
|
|
assert result.status == "unhealthy"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_health_check_degraded(self, adapter):
|
|
adapter._relay_manager = MagicMock()
|
|
adapter._relay_manager.active_count.return_value = (1, 10)
|
|
adapter._nostr_config = MagicMock()
|
|
adapter._nostr_config.relays = ["wss://r1"]
|
|
adapter._nostr_config.relay_degraded_threshold = 0.5
|
|
from yuxi.channels.models import HealthStatus
|
|
|
|
adapter._health_tracker.snapshot = MagicMock(return_value=MagicMock(url="wss://r1"))
|
|
result = await adapter.health_check()
|
|
assert result.status in ("degraded", "healthy")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_health_check_healthy(self, adapter):
|
|
adapter._relay_manager = MagicMock()
|
|
adapter._relay_manager.active_count.return_value = (3, 3)
|
|
adapter._nostr_config = MagicMock()
|
|
adapter._nostr_config.relays = ["wss://r1", "wss://r2", "wss://r3"]
|
|
adapter._nostr_config.relay_degraded_threshold = 0.5
|
|
adapter._crypto = MagicMock()
|
|
adapter._crypto.npub = "npub1test"
|
|
adapter._health_tracker.snapshot = MagicMock(return_value=MagicMock(url="wss://r1"))
|
|
result = await adapter.health_check()
|
|
assert result.status == "healthy"
|
|
|
|
|
|
class TestGetStatusSnapshot:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
def test_status_snapshot_no_manager(self, adapter):
|
|
snapshot = adapter.get_status_snapshot()
|
|
assert snapshot["active_relays"] == 0
|
|
assert snapshot["total_relays"] == 0
|
|
assert snapshot["inflight"] == 0
|
|
|
|
def test_status_snapshot_with_manager(self, adapter):
|
|
adapter._relay_manager = MagicMock()
|
|
adapter._relay_manager.active_count.return_value = (2, 4)
|
|
adapter._relay_manager._connections = {}
|
|
adapter._relay_manager._circuit_breakers = {}
|
|
adapter._nostr_config = MagicMock()
|
|
adapter._nostr_config.relays = []
|
|
adapter._crypto = MagicMock()
|
|
adapter._crypto.npub = "npub1test"
|
|
adapter._status = MagicMock()
|
|
adapter._status.value = "connected"
|
|
|
|
mock_snap = MagicMock()
|
|
mock_snap.score = 0.8
|
|
adapter._health_tracker.snapshot = MagicMock(return_value=mock_snap)
|
|
|
|
snapshot = adapter.get_status_snapshot()
|
|
assert snapshot["active_relays"] == 2
|
|
assert snapshot["total_relays"] == 4
|
|
|
|
|
|
class TestGetMetricsSnapshot:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
def test_metrics_snapshot(self, adapter):
|
|
snapshot = adapter.get_metrics_snapshot()
|
|
assert "counters" in snapshot
|
|
assert "gauges" in snapshot
|
|
assert "timestamp" in snapshot
|
|
|
|
|
|
class TestPreConnect:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pre_connect_missing_private_key(self, adapter):
|
|
with patch.object(adapter, "config", {}):
|
|
result = await adapter.pre_connect()
|
|
assert result["status"] == "error"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pre_connect_with_private_key(self, adapter):
|
|
from yuxi.channels.adapters.nostr.crypto import NostrCrypto
|
|
|
|
temp_crypto = NostrCrypto()
|
|
nsec = temp_crypto.nsec()
|
|
|
|
with patch.object(adapter, "config", {"private_key": nsec, "relays": ["wss://test.relay"]}):
|
|
with patch("yuxi.channels.adapters.nostr.adapter.probe_relay") as mock_probe:
|
|
from yuxi.channels.adapters.nostr.probe import ProbeResult
|
|
|
|
mock_probe.return_value = ProbeResult(
|
|
url="wss://test.relay", connected=True, latency_ms=50.0
|
|
)
|
|
result = await adapter.pre_connect()
|
|
assert result["status"] == "ok"
|
|
assert result["npub"].startswith("npub1")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pre_connect_invalid_key(self, adapter):
|
|
with patch.object(adapter, "config", {"private_key": "invalid_key_123", "relays": ["wss://test.relay"]}):
|
|
result = await adapter.pre_connect()
|
|
assert result["status"] == "error"
|
|
|
|
|
|
class TestResolveChatId:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
def test_resolve_chat_id_group_with_root(self, adapter):
|
|
chat_id = adapter._resolve_chat_id(
|
|
"group",
|
|
[["e", "root_id", "", "root"], ["e", "reply_id", "", "reply"]],
|
|
"pubkey",
|
|
)
|
|
assert chat_id == "channel:root_id"
|
|
|
|
def test_resolve_chat_id_group_without_root(self, adapter):
|
|
chat_id = adapter._resolve_chat_id(
|
|
"group",
|
|
[["e", "first_e_tag"]],
|
|
"pubkey",
|
|
)
|
|
assert chat_id == "channel:first_e_tag"
|
|
|
|
def test_resolve_chat_id_group_fallback(self, adapter):
|
|
chat_id = adapter._resolve_chat_id("group", [], "pubkey")
|
|
assert chat_id == "channel:pubkey"
|
|
|
|
def test_resolve_chat_id_direct(self, adapter):
|
|
chat_id = adapter._resolve_chat_id(
|
|
"direct",
|
|
[["p", "receiver_hex", "", "wss://relay"]],
|
|
"sender_hex",
|
|
)
|
|
assert chat_id == "dm:receiver_hex"
|
|
|
|
|
|
class TestExtractReplyTo:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
def test_extract_reply_to_with_reply_tag(self, adapter):
|
|
reply_to = adapter._extract_reply_to([
|
|
["e", "root_id", "", "root"],
|
|
["e", "parent_id", "", "reply"],
|
|
["e", "extra_id"],
|
|
])
|
|
assert reply_to == "extra_id"
|
|
|
|
def test_extract_reply_to_no_e_tags(self, adapter):
|
|
reply_to = adapter._extract_reply_to([["p", "user_hex"]])
|
|
assert reply_to is None
|
|
|
|
def test_extract_reply_to_only_root_and_reply(self, adapter):
|
|
reply_to = adapter._extract_reply_to([
|
|
["e", "root_id", "", "root"],
|
|
["e", "reply_id", "", "reply"],
|
|
])
|
|
assert reply_to == "reply_id"
|
|
|
|
|
|
class TestSendStreamChunk:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stream_chunk_off_mode_not_finished(self, adapter):
|
|
adapter._nostr_config = MagicMock()
|
|
adapter._nostr_config.streaming_mode = "off"
|
|
result = await adapter.send_stream_chunk("dm:receiver", "", "chunk", False)
|
|
assert result.success is True
|
|
assert result.message_id is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stream_chunk_block_mode_not_finished(self, adapter):
|
|
adapter._nostr_config = MagicMock()
|
|
adapter._nostr_config.streaming_mode = "block"
|
|
result = await adapter.send_stream_chunk("dm:receiver", "", "chunk", False)
|
|
assert result.success is True
|
|
assert result.message_id is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stream_chunk_off_mode_finished_no_sender(self, adapter):
|
|
adapter._nostr_config = MagicMock()
|
|
adapter._nostr_config.streaming_mode = "off"
|
|
result = await adapter.send_stream_chunk("dm:receiver", "", "final chunk", True)
|
|
assert result.success is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stream_chunk_progress_mode_not_finished_no_sender(self, adapter):
|
|
adapter._nostr_config = MagicMock()
|
|
adapter._nostr_config.streaming_mode = "progress"
|
|
result = await adapter.send_stream_chunk("dm:receiver", "", "chunk", False)
|
|
assert result.success is False
|
|
|
|
|
|
class TestDownloadMedia:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_download_media_success(self, adapter):
|
|
mock_resp = MagicMock()
|
|
mock_resp.status = 200
|
|
mock_resp.content_length = 100
|
|
mock_resp.read = AsyncMock(return_value=b"fake_image_data")
|
|
|
|
mock_session = AsyncContextManagerMock()
|
|
mock_session.return_value = mock_session
|
|
mock_get_cm = AsyncContextManagerMock(return_value=mock_resp)
|
|
mock_session.get = MagicMock(return_value=mock_get_cm)
|
|
|
|
with patch("yuxi.channels.adapters.nostr.adapter.aiohttp.ClientSession", return_value=mock_session):
|
|
result = await adapter.download_media("https://example.com/img.jpg")
|
|
assert result == b"fake_image_data"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_download_media_too_large(self, adapter):
|
|
mock_resp = MagicMock()
|
|
mock_resp.status = 200
|
|
mock_resp.content_length = 50 * 1024 * 1024
|
|
|
|
mock_session = AsyncContextManagerMock()
|
|
mock_session.return_value = mock_session
|
|
mock_get_cm = AsyncContextManagerMock(return_value=mock_resp)
|
|
mock_session.get = MagicMock(return_value=mock_get_cm)
|
|
|
|
with patch("yuxi.channels.adapters.nostr.adapter.aiohttp.ClientSession", return_value=mock_session):
|
|
with pytest.raises(Exception):
|
|
await adapter.download_media("https://example.com/huge.jpg")
|
|
|
|
|
|
class TestListPeers:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_peers_no_manager(self, adapter):
|
|
result = await adapter.list_peers()
|
|
assert result == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_peers_with_manager(self, adapter):
|
|
adapter._relay_manager = MagicMock()
|
|
adapter._relay_manager.query = AsyncMock(return_value=[
|
|
{"tags": [["p", "pubkey_a"]], "pubkey": "own_pubkey_hex"},
|
|
])
|
|
adapter._crypto = MagicMock()
|
|
adapter._crypto.pubkey_hex.return_value = "own_pubkey_hex"
|
|
result = await adapter.list_peers()
|
|
assert isinstance(result, list)
|
|
|
|
|
|
class TestGetUserInfo:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_user_info_no_manager(self, adapter):
|
|
result = await adapter.get_user_info("pubkey_hex")
|
|
assert result == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_user_info_with_metadata(self, adapter):
|
|
adapter._relay_manager = MagicMock()
|
|
metadata = _json.dumps({
|
|
"name": "Alice",
|
|
"display_name": "Alice W.",
|
|
"picture": "https://example.com/pic.jpg",
|
|
"about": "Hello World",
|
|
"nip05": "alice@example.com",
|
|
})
|
|
adapter._relay_manager.query = AsyncMock(return_value=[{
|
|
"content": metadata,
|
|
"pubkey": "pubkey_hex",
|
|
}])
|
|
result = await adapter.get_user_info("pubkey_hex")
|
|
assert result["name"] == "Alice"
|
|
assert result["nip05"] == "alice@example.com"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_user_info_query_fails(self, adapter):
|
|
adapter._relay_manager = MagicMock()
|
|
adapter._relay_manager.query = AsyncMock(side_effect=Exception("timeout"))
|
|
result = await adapter.get_user_info("pubkey_hex")
|
|
assert result == {"pubkey": "pubkey_hex"}
|
|
|
|
|
|
class TestResolveChatType:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
def test_kind4_direct(self, adapter):
|
|
assert adapter._resolve_chat_type(4, []) == "direct"
|
|
|
|
def test_kind1059_direct(self, adapter):
|
|
assert adapter._resolve_chat_type(1059, []) == "direct"
|
|
|
|
def test_kind5_direct_single_p(self, adapter):
|
|
assert adapter._resolve_chat_type(5, [["p", "single_pubkey"]]) == "direct"
|
|
|
|
def test_kind5_group_multiple_p(self, adapter):
|
|
assert adapter._resolve_chat_type(5, [["p", "p1"], ["p", "p2"]]) == "group"
|
|
|
|
def test_kind7_direct_single(self, adapter):
|
|
assert adapter._resolve_chat_type(7, [["e", "e1"], ["p", "p1"]]) == "direct"
|
|
|
|
def test_kind1_with_root_group(self, adapter):
|
|
assert adapter._resolve_chat_type(1, [["e", "root_id", "", "root"]]) == "group"
|
|
|
|
|
|
class TestBuildGuardPolicy:
|
|
def test_build_guard_policy(self, adapter=None):
|
|
from yuxi.channels.adapters.nostr.config import NostrConfig
|
|
|
|
cfg = NostrConfig()
|
|
result = NostrAdapter._build_guard_policy(cfg)
|
|
assert result.allowed_kinds == {1, 4, 5, 7, 1059}
|
|
assert result.max_ciphertext_bytes == 50_000
|
|
assert result.max_plaintext_bytes == 10_000
|
|
|
|
|
|
class TestNormalizeInboundReactions:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
def test_normalize_kind7_reaction_with_emoji_and_e_tag(self, adapter):
|
|
raw = {
|
|
"id": "reaction_emoji",
|
|
"kind": 7,
|
|
"content": "🎉",
|
|
"pubkey": "sender_hex",
|
|
"tags": [
|
|
["e", "original_event_id"],
|
|
["p", "author_pubkey"],
|
|
],
|
|
"created_at": 1715000000,
|
|
}
|
|
msg = adapter.normalize_inbound(raw)
|
|
assert msg.event_type == EventType.REACTION_ADDED
|
|
assert "🎉" in msg.content
|
|
|
|
|
|
class TestGetSendCache:
|
|
@pytest.fixture
|
|
def adapter(self):
|
|
config = {"private_key": None, "relays": [], "dm_policy": "open"}
|
|
return NostrAdapter(config=config)
|
|
|
|
def test_get_send_cache_empty(self, adapter):
|
|
cache = adapter.get_send_cache()
|
|
assert cache == [] |