ForcePilot/backend/test/unit/channels/test_nostr_integration.py
Kris 3264900bc9 test: 新增多渠道单元测试用例并配置测试环境变量
新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块
同时在测试配置中添加了测试用的OpenAI API密钥环境变量
2026-05-12 00:56:47 +08:00

199 lines
6.7 KiB
Python

from __future__ import annotations
import time
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from yuxi.channels.adapters.nostr.adapter import NostrAdapter
from yuxi.channels.adapters.nostr.config import NostrConfig
from yuxi.channels.models import (
ChannelIdentity,
ChannelMessage,
ChannelResponse,
ChannelType,
ChatType,
EventType,
MessageType,
)
class TestNostrAdapterIntegration:
@pytest.fixture
def adapter(self):
config = {"private_key": None, "relays": ["wss://test.relay"], "dm_policy": "open"}
return NostrAdapter(config=config)
def test_capabilities_after_p0_calibration(self, adapter):
assert adapter.capabilities.media is False
assert adapter.capabilities.reactions is True
assert adapter.capabilities.edit is True
assert adapter.capabilities.unsend is True
assert adapter.capabilities.reply is True
def test_channel_meta(self, adapter):
assert adapter.meta.id == "nostr"
assert adapter.meta.label == "Nostr"
assert "NIP-04" in adapter.meta.blurb
def test_normalize_inbound_full_flow(self, adapter):
now = int(time.time())
raw = {
"id": "int_test_001",
"kind": 1,
"content": "integration test message",
"pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb",
"tags": [["e", "root_event", "", "root"]],
"created_at": now,
}
msg = adapter.normalize_inbound(raw)
assert isinstance(msg, ChannelMessage)
assert msg.content == "integration test message"
assert msg.event_type == EventType.MESSAGE_RECEIVED
assert msg.chat_type == ChatType.GROUP
def test_normalize_kind1_direct(self, adapter):
raw = {
"id": "direct_001",
"kind": 1,
"content": "direct message",
"pubkey": "sender_hex_64",
"tags": [["p", "receiver_hex_64"]],
"created_at": int(time.time()),
}
msg = adapter.normalize_inbound(raw)
assert msg.chat_type == ChatType.DIRECT
@pytest.mark.asyncio
async def test_handle_relay_event_full_flow(self, adapter):
adapter._nostr_config = NostrConfig(relays=[])
crypto_mock = MagicMock()
crypto_mock.verify_event.return_value = True
crypto_mock.pubkey_hex.return_value = "own_pubkey_hex_64"
adapter._crypto = crypto_mock
guard_mock = MagicMock()
guard_mock.check.return_value = None
guard_mock.inflight = frozenset()
guard_mock.done_processing = MagicMock()
guard_mock._seen = MagicMock()
guard_mock._seen.__len__ = MagicMock(return_value=0)
adapter._guard = guard_mock
handle_called = False
async def fake_handle(msg):
nonlocal handle_called
handle_called = True
adapter._handle_message = fake_handle
now = int(time.time())
raw = {
"id": "handle_test_001",
"kind": 1,
"content": "test event",
"pubkey": "b" * 64,
"tags": [],
"created_at": now,
}
await adapter._handle_relay_event(raw)
assert handle_called is True
@pytest.mark.asyncio
async def test_handle_relay_event_rejects_invalid_sig(self, adapter):
adapter._nostr_config = NostrConfig(relays=[])
crypto_mock = MagicMock()
crypto_mock.verify_event.return_value = False
adapter._crypto = crypto_mock
handle_called = False
adapter._handle_message = AsyncMock()
adapter._handle_message.side_effect = lambda _: setattr(
type(self), "_flag", True
)
raw = {"id": "bad_sig_event", "kind": 1, "content": "bad"}
await adapter._handle_relay_event(raw)
@pytest.mark.asyncio
async def test_handle_relay_event_nip17_decryption(self, adapter):
adapter._nostr_config = NostrConfig(relays=[])
crypto_mock = MagicMock()
crypto_mock.verify_event.return_value = True
crypto_mock.pubkey_hex.return_value = "own_pubkey_hex_64"
crypto_mock.decrypt_nip17 = AsyncMock(return_value="decrypted nip17 content")
adapter._crypto = crypto_mock
guard_mock = MagicMock()
guard_mock.check.return_value = None
guard_mock.inflight = frozenset()
guard_mock.done_processing = MagicMock()
guard_mock._seen = MagicMock()
guard_mock._seen.__len__ = MagicMock(return_value=0)
adapter._guard = guard_mock
received_msg = None
async def capture_handle(msg):
nonlocal received_msg
received_msg = msg
adapter._handle_message = capture_handle
now = int(time.time())
raw = {
"id": "nip17_test_001",
"kind": 1059,
"content": "encrypted_wrapped_json",
"pubkey": "b" * 64,
"tags": [],
"created_at": now,
}
await adapter._handle_relay_event(raw)
assert received_msg is not None
assert received_msg.content == "decrypted nip17 content"
def test_resolve_state_dir(self, adapter):
state_dir = adapter._resolve_state_dir()
assert ".forcepilot" in state_dir
assert "nostr" in state_dir
class TestNostrIntegrationMediaCalibration:
def test_send_media_returns_error_when_no_sender(self):
adapter = NostrAdapter(config={"private_key": None, "relays": []})
from yuxi.channels.adapters.nostr.adapter import DeliveryResult
result = asyncio_get(adapter.send_media("dm:test", "image", "http://example.com/img.jpg"))
assert result.success is False
def test_download_media_returns_bytes(self):
adapter = NostrAdapter(config={"private_key": None, "relays": []})
import aiohttp
with patch("aiohttp.ClientSession.get") as mock_get:
mock_resp = MagicMock()
mock_resp.status = 200
mock_resp.content_length = 100
mock_resp.read = AsyncMock(return_value=b"fake_image_data")
mock_get.return_value.__aenter__ = AsyncMock(return_value=mock_resp)
mock_get.return_value.__aexit__ = AsyncMock()
result = asyncio_get(adapter.download_media("http://example.com/img.jpg"))
assert result == b"fake_image_data"
def asyncio_get(coro):
import asyncio
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(coro)
import concurrent.futures
future = asyncio.run_coroutine_threadsafe(coro, loop)
return future.result(timeout=5)