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

246 lines
8.4 KiB
Python

from __future__ import annotations
import json
import time
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
class TestEventVerification:
def test_verify_event_valid(self):
from yuxi.channels.adapters.nostr.crypto import NostrCrypto
with patch("yuxi.channels.adapters.nostr.crypto.Keys") as mk:
mk.parse.return_value = _make_mock_keys()
crypto = NostrCrypto("nsec1test")
raw = {
"id": "a" * 64,
"pubkey": "b" * 64,
"kind": 1,
"content": "hello",
"tags": [],
"created_at": int(time.time()),
"sig": "c" * 128,
}
with patch("yuxi.channels.adapters.nostr.crypto.Event") as me:
mock_event = MagicMock()
mock_event.verify.return_value = None
me.from_json.return_value = mock_event
assert crypto.verify_event(raw) is True
def test_verify_event_invalid_rejected(self):
from yuxi.channels.adapters.nostr.crypto import NostrCrypto
with patch("yuxi.channels.adapters.nostr.crypto.Keys") as mk:
mk.parse.return_value = _make_mock_keys()
crypto = NostrCrypto("nsec1test")
raw = {"id": "x" * 64, "pubkey": "y" * 64}
with patch("yuxi.channels.adapters.nostr.crypto.Event") as me:
mock_event = MagicMock()
mock_event.verify.side_effect = Exception("bad sig")
me.from_json.return_value = mock_event
assert crypto.verify_event(raw) is False
def test_verify_event_strict_id_mismatch_rejected(self):
from yuxi.channels.adapters.nostr.crypto import NostrCrypto
with patch("yuxi.channels.adapters.nostr.crypto.Keys") as mk:
mk.parse.return_value = _make_mock_keys()
crypto = NostrCrypto("nsec1test")
raw = {"id": "wrong_id", "pubkey": "b" * 64}
with patch("yuxi.channels.adapters.nostr.crypto.Event") as me:
mock_event = MagicMock()
mock_event.id.return_value.to_hex.return_value = "real_id"
mock_event.id.return_value.to_bech32.return_value = "note1real"
me.from_json.return_value = mock_event
assert crypto.verify_event_strict(raw) is False
def test_verify_event_strict_passes_with_matching_id(self):
from yuxi.channels.adapters.nostr.crypto import NostrCrypto
with patch("yuxi.channels.adapters.nostr.crypto.Keys") as mk:
mk.parse.return_value = _make_mock_keys()
crypto = NostrCrypto("nsec1test")
event_id = "a" * 64
raw = {"id": event_id, "pubkey": "b" * 64}
with patch("yuxi.channels.adapters.nostr.crypto.Event") as me:
mock_event = MagicMock()
mock_event.id.return_value.to_hex.return_value = event_id
mock_event.id.return_value.to_bech32.return_value = "note1other"
mock_event.verify.return_value = None
me.from_json.return_value = mock_event
assert crypto.verify_event_strict(raw) is True
@pytest.mark.asyncio
async def test_adapter_rejects_invalid_event(self):
from yuxi.channels.adapters.nostr.adapter import NostrAdapter
from yuxi.channels.adapters.nostr.config import NostrConfig
adapter = NostrAdapter(config={"private_key": None, "relays": []})
adapter._nostr_config = NostrConfig(relays=[])
crypto_mock = MagicMock()
crypto_mock.verify_event.return_value = False
adapter._crypto = crypto_mock
raw = {"id": "fake_id_123"}
await adapter._handle_relay_event(raw)
class TestMultiRelayQuery:
@pytest.mark.asyncio
async def test_query_aggregates_multiple_relays(self):
from yuxi.channels.adapters.nostr.relay_manager import RelayManager
mgr = RelayManager(relay_urls=["wss://a.relay", "wss://b.relay"])
ws_a = AsyncMock()
ws_a.open = True
ws_a.send = AsyncMock()
ws_b = AsyncMock()
ws_b.open = True
ws_b.send = AsyncMock()
mgr._connections = {"wss://a.relay": ws_a, "wss://b.relay": ws_b}
event_a = json.dumps(
[
"EVENT",
"sub",
{"id": "evt_a", "pubkey": "p1", "kind": 1, "content": "from a", "tags": [], "created_at": 1},
]
)
eose = json.dumps(["EOSE", "sub"])
event_b = json.dumps(
[
"EVENT",
"sub",
{"id": "evt_b", "pubkey": "p2", "kind": 1, "content": "from b", "tags": [], "created_at": 2},
]
)
ws_a.recv = AsyncMock(side_effect=[event_a, eose, TimeoutError()])
ws_b.recv = AsyncMock(side_effect=[event_b, eose, TimeoutError()])
results = await mgr.query([{"kinds": [1]}], timeout=1.0)
assert len(results) == 2
result_ids = {r["id"] for r in results}
assert result_ids == {"evt_a", "evt_b"}
@pytest.mark.asyncio
async def test_query_dedup_across_relays(self):
from yuxi.channels.adapters.nostr.relay_manager import RelayManager
mgr = RelayManager(relay_urls=["wss://a.relay", "wss://b.relay"])
ws_a = AsyncMock()
ws_a.open = True
ws_a.send = AsyncMock()
ws_b = AsyncMock()
ws_b.open = True
ws_b.send = AsyncMock()
mgr._connections = {"wss://a.relay": ws_a, "wss://b.relay": ws_b}
dup_event = json.dumps(
["EVENT", "sub", {"id": "duplicate", "kind": 1, "content": "dup", "tags": [], "created_at": 1}]
)
eose = json.dumps(["EOSE", "sub"])
ws_a.recv = AsyncMock(side_effect=[dup_event, eose, TimeoutError()])
ws_b.recv = AsyncMock(side_effect=[dup_event, eose, TimeoutError()])
results = await mgr.query([{"kinds": [1]}], timeout=1.0)
assert len(results) == 1
assert results[0]["id"] == "duplicate"
@pytest.mark.asyncio
async def test_query_no_active_connections_returns_empty(self):
from yuxi.channels.adapters.nostr.relay_manager import RelayManager
mgr = RelayManager(relay_urls=["wss://a.relay"])
mgr._connections = {}
results = await mgr.query([{"kinds": [1]}])
assert results == []
class TestPersistentCursor:
@pytest.mark.asyncio
async def test_subscribe_first_call_uses_24h_ago(self):
from yuxi.channels.adapters.nostr.monitor import NostrMonitor
crypto_mock = MagicMock()
relay_mgr = MagicMock()
relay_mgr.subscribe = AsyncMock()
monitor = NostrMonitor(crypto_mock, relay_mgr)
now = int(time.time())
with patch("time.time", return_value=now):
await monitor.subscribe("pairing")
relay_mgr.subscribe.assert_called_once()
filters = relay_mgr.subscribe.call_args[0][0]
since = filters[0]["since"]
assert now - 86400 <= since <= now
assert monitor._last_subscribe_ts == now
@pytest.mark.asyncio
async def test_subscribe_second_call_uses_previous_ts(self):
from yuxi.channels.adapters.nostr.monitor import NostrMonitor
crypto_mock = MagicMock()
relay_mgr = MagicMock()
relay_mgr.subscribe = AsyncMock()
monitor = NostrMonitor(crypto_mock, relay_mgr)
monitor._last_subscribe_ts = 1000
now = 5000
with patch("time.time", return_value=now):
await monitor.subscribe("pairing")
filters = relay_mgr.subscribe.call_args[0][0]
assert filters[0]["since"] == 1000
assert monitor._last_subscribe_ts == now
@pytest.mark.asyncio
async def test_subscribe_updates_timestamp_each_call(self):
from yuxi.channels.adapters.nostr.monitor import NostrMonitor
crypto_mock = MagicMock()
relay_mgr = MagicMock()
relay_mgr.subscribe = AsyncMock()
monitor = NostrMonitor(crypto_mock, relay_mgr)
with patch("time.time", return_value=1000):
await monitor.subscribe("pairing")
assert monitor._last_subscribe_ts == 1000
with patch("time.time", return_value=5000):
await monitor.subscribe("pairing")
assert monitor._last_subscribe_ts == 5000
def _make_mock_keys():
mk = MagicMock()
mpk = MagicMock()
mpk.to_bech32.return_value = "npub1test"
mpk.to_hex.return_value = "a" * 64
msk = MagicMock()
msk.to_bech32.return_value = "nsec1test"
mk.public_key.return_value = mpk
mk.secret_key.return_value = msk
return mk