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}")