新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
265 lines
10 KiB
Python
265 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from yuxi.channels.adapters.nostr.config import NostrConfig, NostrConfigError
|
|
from yuxi.channels.adapters.nostr.crypto import NostrCryptoError
|
|
from yuxi.channels.adapters.nostr.probe import ProbeResult
|
|
|
|
|
|
class TestNostrConfigValidation:
|
|
def test_valid_config_passes_validation(self):
|
|
cfg = NostrConfig(
|
|
private_key="nsec1test",
|
|
relays=["wss://test.relay"],
|
|
dm_policy="pairing",
|
|
streaming_mode="block",
|
|
)
|
|
assert cfg.dm_policy == "pairing"
|
|
|
|
def test_invalid_dm_policy_raises_error(self):
|
|
with pytest.raises(NostrConfigError, match="dm_policy 值无效"):
|
|
NostrConfig(dm_policy="invalid_policy")
|
|
|
|
def test_invalid_streaming_mode_raises_error(self):
|
|
with pytest.raises(NostrConfigError, match="streaming_mode 值无效"):
|
|
NostrConfig(streaming_mode="invalid_mode")
|
|
|
|
def test_invalid_reconnect_interval_raises_error(self):
|
|
with pytest.raises(NostrConfigError, match="reconnect_interval_sec"):
|
|
NostrConfig(reconnect_interval_sec=0)
|
|
|
|
def test_invalid_relay_timeout_raises_error(self):
|
|
with pytest.raises(NostrConfigError, match="relay_timeout_sec"):
|
|
NostrConfig(relay_timeout_sec=0)
|
|
|
|
def test_invalid_degraded_threshold_raises_error(self):
|
|
with pytest.raises(NostrConfigError, match="relay_degraded_threshold"):
|
|
NostrConfig(relay_degraded_threshold=0)
|
|
|
|
def test_invalid_degraded_threshold_above_one_raises_error(self):
|
|
with pytest.raises(NostrConfigError, match="relay_degraded_threshold"):
|
|
NostrConfig(relay_degraded_threshold=1.5)
|
|
|
|
def test_invalid_relay_url_raises_error(self):
|
|
with pytest.raises(NostrConfigError, match="Relay URL 必须以 ws:// 或 wss:// 开头"):
|
|
NostrConfig(relays=["http://invalid.relay"])
|
|
|
|
def test_all_valid_dm_policies_pass(self):
|
|
for policy in ("pairing", "open", "whitelist"):
|
|
cfg = NostrConfig(dm_policy=policy)
|
|
assert cfg.dm_policy == policy
|
|
|
|
def test_all_valid_streaming_modes_pass(self):
|
|
for mode in ("off", "block", "progress"):
|
|
cfg = NostrConfig(streaming_mode=mode)
|
|
assert cfg.streaming_mode == mode
|
|
|
|
def test_from_dict_with_invalid_config_still_validates(self):
|
|
with pytest.raises(NostrConfigError):
|
|
NostrConfig.from_dict({
|
|
"accounts": {"default": {"dm_policy": "bad"}}
|
|
})
|
|
|
|
def test_wss_relay_url_valid(self):
|
|
cfg = NostrConfig(relays=["wss://relay.example.com"])
|
|
assert len(cfg.relays) == 1
|
|
|
|
def test_ws_relay_url_valid(self):
|
|
cfg = NostrConfig(relays=["ws://relay.example.com"])
|
|
assert len(cfg.relays) == 1
|
|
|
|
|
|
class TestNostrCryptoDecryptNip17:
|
|
@pytest.mark.asyncio
|
|
async def test_decrypt_nip17_raises_on_none_unwrapped(self):
|
|
with patch(
|
|
"yuxi.channels.adapters.nostr.crypto.Event"
|
|
) as mock_event_class, patch(
|
|
"yuxi.channels.adapters.nostr.crypto.UnwrappedGift"
|
|
) as mock_gift_class:
|
|
mock_event = MagicMock()
|
|
mock_event_class.from_json.return_value = mock_event
|
|
mock_gift = MagicMock()
|
|
mock_gift.from_gift_wrap.return_value = None
|
|
mock_gift_class.from_gift_wrap.return_value = None
|
|
|
|
from yuxi.channels.adapters.nostr.crypto import NostrCrypto
|
|
|
|
crypto = create_crypto_instance()
|
|
|
|
with pytest.raises(NostrCryptoError, match="Gift Wrap 解封返回空"):
|
|
await crypto.decrypt_nip17('{"id":"test"}')
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_decrypt_nip17_raises_on_none_rumor(self):
|
|
with patch(
|
|
"yuxi.channels.adapters.nostr.crypto.Event"
|
|
) as mock_event_class, patch(
|
|
"yuxi.channels.adapters.nostr.crypto.UnwrappedGift"
|
|
) as mock_gift_class:
|
|
mock_event = MagicMock()
|
|
mock_event_class.from_json.return_value = mock_event
|
|
mock_gift = MagicMock()
|
|
mock_gift.from_gift_wrap.return_value = mock_gift
|
|
mock_gift_class.from_gift_wrap.return_value = mock_gift
|
|
mock_gift.rumor.return_value = None
|
|
|
|
from yuxi.channels.adapters.nostr.crypto import NostrCrypto
|
|
|
|
crypto = create_crypto_instance()
|
|
|
|
with pytest.raises(NostrCryptoError, match="Rumor 为空"):
|
|
await crypto.decrypt_nip17('{"id":"test"}')
|
|
|
|
|
|
class TestProbeResult:
|
|
def test_probe_result_is_dataclass(self):
|
|
result = ProbeResult(url="wss://test.relay", connected=True, latency_ms=100.0)
|
|
assert result.url == "wss://test.relay"
|
|
assert result.connected is True
|
|
assert result.latency_ms == 100.0
|
|
assert result.error is None
|
|
|
|
def test_probe_result_with_error(self):
|
|
result = ProbeResult(
|
|
url="wss://bad.relay", connected=False, error="Connection refused"
|
|
)
|
|
assert result.connected is False
|
|
assert result.error == "Connection refused"
|
|
|
|
def test_probe_result_repr(self):
|
|
result = ProbeResult(url="wss://test.relay", connected=True, latency_ms=50.0)
|
|
repr_str = repr(result)
|
|
assert "ProbeResult" in repr_str
|
|
|
|
|
|
def create_crypto_instance():
|
|
from yuxi.channels.adapters.nostr.crypto import NostrCrypto
|
|
|
|
with patch(
|
|
"yuxi.channels.adapters.nostr.crypto.Keys"
|
|
) as mock_keys_class:
|
|
mock_keys = MagicMock()
|
|
mock_public_key = MagicMock()
|
|
mock_public_key.to_bech32.return_value = "npub1test"
|
|
mock_public_key.to_hex.return_value = "abcdef"
|
|
mock_secret_key = MagicMock()
|
|
mock_secret_key.to_bech32.return_value = "nsec1test"
|
|
mock_keys.public_key.return_value = mock_public_key
|
|
mock_keys.secret_key.return_value = mock_secret_key
|
|
mock_keys_class.parse.return_value = mock_keys
|
|
return NostrCrypto("nsec1test")
|
|
|
|
|
|
class TestHealthCheckGuards:
|
|
@pytest.mark.asyncio
|
|
async def test_health_check_total_zero_returns_unhealthy(self):
|
|
from yuxi.channels.adapters.nostr.adapter import NostrAdapter
|
|
|
|
adapter = NostrAdapter(config={"private_key": None, "relays": []})
|
|
adapter._nostr_config = NostrConfig(relays=[])
|
|
adapter._relay_manager = MagicMock()
|
|
adapter._relay_manager.active_count.return_value = (0, 0)
|
|
|
|
result = await adapter.health_check()
|
|
assert result.status == "unhealthy"
|
|
assert "没有 Relay" in result.last_error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_health_check_no_crypto_still_healthy(self):
|
|
from yuxi.channels.adapters.nostr.adapter import NostrAdapter
|
|
|
|
adapter = NostrAdapter(config={"private_key": None, "relays": []})
|
|
adapter._nostr_config = NostrConfig(relays=["wss://test.relay"])
|
|
adapter._relay_manager = MagicMock()
|
|
adapter._relay_manager.active_count.return_value = (2, 3)
|
|
adapter._crypto = None
|
|
|
|
result = await adapter.health_check()
|
|
assert result.status == "healthy"
|
|
assert result.metadata["npub"] == ""
|
|
|
|
|
|
class TestAnalysisCoordinator:
|
|
@pytest.mark.asyncio
|
|
async def test_coordinator_generates_complete_report(self):
|
|
from yuxi.channels.adapters.nostr.analysis.coordinator import (
|
|
AnalysisCoordinator,
|
|
)
|
|
|
|
coordinator = AnalysisCoordinator("package/yuxi/channels/adapters/nostr")
|
|
report = await coordinator.run()
|
|
|
|
assert report.module_name == "NostrAdapter"
|
|
assert len(report.sub_agent_reports) == 3
|
|
assert report.sub_agent_reports[0].agent_id == "web_researcher"
|
|
assert report.sub_agent_reports[1].agent_id == "code_analyzer"
|
|
assert report.sub_agent_reports[2].agent_id == "problem_finder"
|
|
assert all(r.status == "completed" for r in report.sub_agent_reports)
|
|
assert len(report.code_issues) > 0
|
|
assert len(report.fix_proposals) > 0
|
|
assert report.generated_at != ""
|
|
|
|
def test_report_generator_markdown_output(self):
|
|
from yuxi.channels.adapters.nostr.analysis.coordinator import (
|
|
AnalysisCoordinator,
|
|
)
|
|
from yuxi.channels.adapters.nostr.analysis.reporter import ReportGenerator
|
|
import asyncio
|
|
|
|
coordinator = AnalysisCoordinator("package/yuxi/channels/adapters/nostr")
|
|
report = asyncio.run(coordinator.run())
|
|
md = ReportGenerator.to_markdown(report)
|
|
|
|
assert "# NostrAdapter 功能欠缺分析报告" in md
|
|
assert "缺失的核心功能点" in md
|
|
assert "未实现的接口或方法" in md
|
|
assert "与行业标准的差距" in md
|
|
assert "潜在的性能瓶颈" in md
|
|
assert "代码问题详细清单" in md
|
|
assert "代码修复方案" in md
|
|
assert "子Agent报告摘要" in md
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_web_researcher_produces_findings(self):
|
|
from yuxi.channels.adapters.nostr.analysis.web_researcher import (
|
|
WebResearcher,
|
|
)
|
|
|
|
researcher = WebResearcher()
|
|
report = await researcher.research()
|
|
|
|
assert report.status == "completed"
|
|
assert len(report.findings) > 0
|
|
assert report.findings[0].category in (
|
|
"NIP标准协议", "DM加密演变", "Relay管理", "事件处理", "同类实现参考"
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_code_analyzer_finds_interface_gaps(self):
|
|
from yuxi.channels.adapters.nostr.analysis.code_analyzer import CodeAnalyzer
|
|
|
|
analyzer = CodeAnalyzer("package/yuxi/channels/adapters/nostr")
|
|
report = await analyzer.analyze()
|
|
|
|
assert report.status == "completed"
|
|
assert len(report.findings) >= 10
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_problem_finder_detects_critical_issues(self):
|
|
from yuxi.channels.adapters.nostr.analysis.problem_finder import (
|
|
ProblemFinder,
|
|
)
|
|
|
|
finder = ProblemFinder("package/yuxi/channels/adapters/nostr")
|
|
report = await finder.analyze()
|
|
|
|
assert report.status == "completed"
|
|
assert len(report.findings) >= 20
|
|
critical_count = sum(
|
|
1 for f in report.findings if hasattr(f, "severity") and f.severity == "critical"
|
|
)
|
|
assert critical_count >= 1 |