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

236 lines
7.5 KiB
Python

from __future__ import annotations
import json
import random
import string
import time
from typing import Any
import pytest
from yuxi.channels.adapters.nostr.adapter import NostrAdapter
from yuxi.channels.adapters.nostr.guard import GuardPolicy, NostrGuard, SeenTracker, RateLimiter
from yuxi.channels.adapters.nostr.crypto import normalize_pubkey
def random_hex(length: int = 64) -> str:
return "".join(random.choices("0123456789abcdef", k=length))
def random_event_id() -> str:
return random_hex(64)
def random_pubkey() -> str:
return random_hex(64)
def make_fuzz_event(overrides: dict[str, Any] | None = None) -> dict:
base = {
"id": random_event_id(),
"kind": random.choice([1, 4, 5, 7]),
"content": "".join(random.choices(string.printable, k=random.randint(0, 5000))),
"pubkey": random_pubkey(),
"tags": [],
"created_at": int(time.time()) + random.randint(-3600, 3600),
}
if overrides:
base.update(overrides)
return base
class TestNormalizeInboundFuzz:
@pytest.fixture
def adapter(self):
return NostrAdapter(config={"private_key": None, "relays": [], "dm_policy": "open"})
def test_normalize_handles_random_events(self, adapter):
for i in range(100):
event = make_fuzz_event()
try:
msg = adapter.normalize_inbound(event)
assert msg is not None
assert msg.content is not None
except Exception as e:
pytest.fail(f"normalize_inbound crashed on iteration {i}: {e}\nEvent: {event}")
def test_normalize_handles_empty_content(self, adapter):
event = make_fuzz_event({"content": ""})
msg = adapter.normalize_inbound(event)
assert msg is not None
def test_normalize_handles_missing_fields(self, adapter):
event: dict = {}
msg = adapter.normalize_inbound(event)
assert msg is not None
def test_normalize_handles_none_tags(self, adapter):
event = make_fuzz_event({"tags": None})
try:
msg = adapter.normalize_inbound(event)
assert msg is not None
except Exception:
pass
def test_normalize_handles_malformed_tags(self, adapter):
event = make_fuzz_event({"tags": ["not_a_list", 123, None, []]})
try:
msg = adapter.normalize_inbound(event)
assert msg is not None
except Exception:
pass
def test_normalize_handles_large_content(self, adapter):
event = make_fuzz_event({"content": "x" * 1_000_000})
try:
msg = adapter.normalize_inbound(event)
assert msg is not None
except Exception:
pass
def test_normalize_handles_all_kinds(self, adapter):
for kind in [0, 1, 2, 3, 4, 5, 6, 7, 8, 1059, 10000, 30000, 99999]:
event = make_fuzz_event({"kind": kind})
try:
msg = adapter.normalize_inbound(event)
assert msg is not None
except Exception:
pass
def test_normalize_handles_unicode_content(self, adapter):
unicode_chars = "日本語 한국어 中文 العربية עברית 🌍✨🔥"
event = make_fuzz_event({"content": unicode_chars})
msg = adapter.normalize_inbound(event)
assert unicode_chars in msg.content
def test_normalize_handles_binary_like_content(self, adapter):
event = make_fuzz_event({"content": "\x00\x01\x02\xff\xfe"})
try:
msg = adapter.normalize_inbound(event)
assert msg is not None
except Exception:
pass
class TestGuardFuzz:
@pytest.fixture
def guard(self):
return NostrGuard(own_pubkey=random_hex())
def test_guard_handles_random_events(self, guard):
for i in range(200):
event = make_fuzz_event()
try:
result = guard.check(event)
assert result is None or isinstance(result, str)
except Exception as e:
pytest.fail(f"guard.check crashed on iteration {i}: {e}")
def test_guard_handles_empty_event(self, guard):
result = guard.check({})
assert result == "missing event id"
def test_guard_handles_missing_pubkey(self, guard):
event = make_fuzz_event()
event.pop("pubkey", None)
try:
guard.check(event)
except Exception:
pass
def test_guard_handles_missing_created_at(self, guard):
event = make_fuzz_event()
event.pop("created_at", None)
try:
guard.check(event)
except Exception:
pass
class TestRateLimiterFuzz:
def test_rate_limiter_handles_many_senders(self):
limiter = RateLimiter(window_ms=10_000, max_per_sender=20, max_global=1000)
senders = set()
for i in range(5000):
sender = random_pubkey()
senders.add(sender)
try:
limiter.check_and_record(sender)
except Exception as e:
pytest.fail(f"RateLimiter crashed on iteration {i}: {e}")
assert len(senders) > 0
def test_rate_limiter_burst(self):
limiter = RateLimiter(window_ms=10_000, max_per_sender=5, max_global=200)
sender = random_hex()
accepted = 0
rejected = 0
for _ in range(100):
if limiter.check_and_record(sender):
accepted += 1
else:
rejected += 1
assert rejected > 0
class TestSeenTrackerFuzz:
def test_seen_tracker_large_volume(self):
tracker = SeenTracker(max_size=10_000)
for i in range(15_000):
event_id = random_hex(20)
tracker.mark_seen(event_id)
assert len(tracker) <= 10_000
def test_seen_tracker_ttl_eviction(self):
tracker = SeenTracker(max_size=100_000, ttl_sec=0)
for _ in range(500):
event_id = random_hex(20)
tracker.mark_seen(event_id)
assert len(tracker) == 0
class TestNormalizePubkeyFuzz:
def test_normalize_pubkey_handles_various_inputs(self):
test_inputs = [
"",
None,
" ",
"npub1invalid",
"nprofile1bad",
"nostr:",
random_hex(64),
random_hex(32),
random_hex(128),
"not_a_pubkey",
"https://example.com",
"a" * 1000,
]
for inp in test_inputs:
try:
result = normalize_pubkey(inp)
assert isinstance(result, str)
except Exception as e:
pytest.fail(f"normalize_pubkey crashed on input '{inp}': {e}")
class TestMarkdownTableFuzz:
def test_convert_tables_handles_various_formats(self):
from yuxi.channels.adapters.nostr.send import NostrSender
inputs = [
"| A | B |\n| --- | --- |\n| 1 | 2 |",
"no table here",
"| single |\n| --- |\n| val |",
"| a | b | c | d | e | f |\n| --- | --- | --- | --- | --- | --- |\n| 1 | 2 | 3 | 4 | 5 | 6 |",
"| :--- | :---: | ---: |\n| a | b | c |",
"| Header ||\n| --- | --- |\n| val |",
"",
"x" * 10000,
]
for inp in inputs:
try:
result = NostrSender.convert_markdown_tables(inp)
assert isinstance(result, str)
except Exception as e:
pytest.fail(f"convert_markdown_tables crashed on input: {e}")