from __future__ import annotations import time from unittest.mock import MagicMock, patch import pytest from yuxi.channels.adapters.nostr.adapter import NostrAdapter from yuxi.channels.adapters.nostr.config import NostrConfig, GuardPolicyConfig from yuxi.channels.adapters.nostr.guard import GuardPolicy, NostrGuard, RateLimiter, SeenTracker from yuxi.channels.adapters.nostr.health import RelayHealthTracker from yuxi.channels.adapters.nostr.relay_manager import RelayManager from yuxi.channels.models import ( ChannelIdentity, ChannelMessage, ChannelResponse, ChannelType, ChatType, EventType, MessageType, ) class TestNostrConfig: def test_from_empty_dict(self): cfg = NostrConfig.from_dict(None) assert cfg.private_key == "" assert len(cfg.relays) >= 4 assert cfg.dm_policy == "pairing" def test_from_dict_defaults(self): cfg = NostrConfig.from_dict({}) assert cfg.streaming_mode == "block" assert cfg.nip17_enabled is True def test_from_dict_with_accounts(self): cfg = NostrConfig.from_dict({ "accounts": { "default": { "private_key": "nsec1test", "relays": ["wss://test.relay"], "dm_policy": "open", } } }) assert cfg.private_key == "nsec1test" assert len(cfg.relays) == 1 assert cfg.relays[0] == "wss://test.relay" assert cfg.dm_policy == "open" def test_from_flat_dict(self): cfg = NostrConfig.from_dict({ "private_key": "hex_key_123", "relays": ["wss://a.relay", "wss://b.relay"], }) assert cfg.private_key == "hex_key_123" assert len(cfg.relays) == 2 class TestNostrAdapterBasic: @pytest.fixture def adapter(self): config = { "private_key": None, "relays": [], "dm_policy": "open", } return NostrAdapter(config=config) def test_channel_id(self, adapter): assert adapter.channel_id == "nostr" def test_channel_type(self, adapter): assert adapter.channel_type == ChannelType.NOSTR def test_capabilities(self, adapter): assert adapter.supports_streaming is True assert adapter.streaming_modes == ["off", "block", "progress"] assert adapter.max_media_size_mb == 10 assert adapter.supports_markdown is False class TestNormalizeInbound: @pytest.fixture def adapter(self): config = {"private_key": None, "relays": [], "dm_policy": "open"} return NostrAdapter(config=config) def test_normalize_kind1_text_note(self, adapter): raw = { "id": "evt001", "kind": 1, "content": "hello nostr", "pubkey": "79c2cae114ea28a981e7559b4fe7854a473521a8d22a66bbab9fa248eb820ff6", "tags": [], "created_at": 1715000000, "sig": "sig_data", } msg = adapter.normalize_inbound(raw) assert isinstance(msg, ChannelMessage) assert msg.content == "hello nostr" assert msg.identity.channel_user_id == raw["pubkey"] assert msg.identity.channel_message_id == "evt001" assert msg.message_type == MessageType.TEXT def test_normalize_kind1_group_chat(self, adapter): raw = { "id": "evt002", "kind": 1, "content": "hello group", "pubkey": "abc123", "tags": [ ["e", "root_event_001", "", "root"], ], "created_at": 1715000000, } msg = adapter.normalize_inbound(raw) assert msg.chat_type == ChatType.GROUP assert "channel:root_event_001" in msg.identity.channel_chat_id def test_normalize_kind4_direct_message(self, adapter): raw = { "id": "dm001", "kind": 4, "content": "encrypted_content_here", "pubkey": "sender_pubkey_hex", "tags": [ ["p", "receiver_pubkey_hex"], ], "created_at": 1715000000, } msg = adapter.normalize_inbound(raw) assert msg.chat_type == ChatType.DIRECT assert "dm:receiver_pubkey_hex" in msg.identity.channel_chat_id def test_normalize_kind1_direct_with_p_tag(self, adapter): raw = { "id": "direct_text", "kind": 1, "content": "direct message via kind1", "pubkey": "sender_hex", "tags": [ ["p", "receiver_hex"], ], "created_at": 1715000000, } msg = adapter.normalize_inbound(raw) assert msg.chat_type == ChatType.DIRECT def test_normalize_with_reply_tags(self, adapter): raw = { "id": "reply_msg", "kind": 1, "content": "this is a reply", "pubkey": "pub_hex", "tags": [ ["e", "parent_id_001"], ], "created_at": 1715000000, } msg = adapter.normalize_inbound(raw) assert msg.reply_to_message_id == "parent_id_001" def test_normalize_with_nip10_reply_chain(self, adapter): raw = { "id": "nip10_reply", "kind": 1, "content": "nip10 style reply", "pubkey": "pub_hex", "tags": [ ["e", "root_id", "", "root"], ["e", "parent_id", "", "reply"], ["e", "extra_ref"], ], "created_at": 1715000000, } msg = adapter.normalize_inbound(raw) assert msg.reply_to_message_id == "extra_ref" def test_resolve_chat_id_direct(self, adapter): raw = { "id": "test_chat_id", "kind": 4, "content": "enc", "pubkey": "sender_hex", "tags": [["p", "receiver_hex"]], "created_at": 1715000000, } msg = adapter.normalize_inbound(raw) assert msg.identity.channel_chat_id == "dm:receiver_hex" class TestFormatOutbound: @pytest.fixture def adapter(self): config = {"private_key": None, "relays": [], "dm_policy": "open"} return NostrAdapter(config=config) def test_format_outbound_returns_dict(self, adapter): identity = ChannelIdentity( channel_id="nostr", channel_type=ChannelType.NOSTR, channel_user_id="user_hex", channel_chat_id="dm:receiver_hex", ) response = ChannelResponse(identity=identity, content="test reply") result = adapter.format_outbound(response) assert isinstance(result, dict) assert result["content"] == "test reply" assert result["identity"]["channel_id"] == "nostr" class TestHealthCheck: @pytest.fixture def adapter(self): config = {"private_key": None, "relays": [], "dm_policy": "open"} return NostrAdapter(config=config) @pytest.mark.asyncio async def test_health_check_unhealthy_no_manager(self, adapter): result = await adapter.health_check() assert result.status == "unhealthy" class TestSeenTracker: def test_is_seen_new_event(self): tracker = SeenTracker(max_size=100) assert tracker.is_seen("event_001") is False def test_mark_and_is_seen(self): tracker = SeenTracker(max_size=100) tracker.mark_seen("event_001") assert tracker.is_seen("event_001") is True def test_size_eviction(self): tracker = SeenTracker(max_size=3) for i in range(5): tracker.mark_seen(f"event_{i}") assert len(tracker) == 3 def test_is_seen_after_ttl(self): tracker = SeenTracker(max_size=100, ttl_sec=0) tracker.mark_seen("event_001") assert tracker.is_seen("event_001") is False class TestRateLimiter: def test_within_limit(self): limiter = RateLimiter(window_ms=10_000, max_per_sender=5, max_global=20) for _ in range(3): assert limiter.check_and_record("pubkey_a") is True def test_sender_limit_exceeded(self): limiter = RateLimiter(window_ms=60_000, max_per_sender=2, max_global=20) assert limiter.check_and_record("pubkey_a") is True assert limiter.check_and_record("pubkey_a") is True assert limiter.check_and_record("pubkey_a") is False def test_global_limit_exceeded(self): limiter = RateLimiter(window_ms=60_000, max_per_sender=10, max_global=2) assert limiter.check_and_record("pubkey_a") is True assert limiter.check_and_record("pubkey_b") is True assert limiter.check_and_record("pubkey_c") is False def test_separate_senders(self): limiter = RateLimiter(window_ms=60_000, max_per_sender=1, max_global=20) assert limiter.check_and_record("pubkey_a") is True assert limiter.check_and_record("pubkey_a") is False assert limiter.check_and_record("pubkey_b") is True class TestNostrGuard: @pytest.fixture def guard(self): return NostrGuard(own_pubkey="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") def test_pass_valid_event(self, guard): now = int(time.time()) event = { "id": "valid_event_001", "kind": 1, "content": "hello", "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "created_at": now, } assert guard.check(event) is None def test_reject_self_message(self, guard): now = int(time.time()) event = { "id": "self_event", "kind": 1, "content": "echo", "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "created_at": now, } assert guard.check(event) == "self-message (echo)" def test_reject_duplicate(self, guard): now = int(time.time()) event = { "id": "dup_event", "kind": 1, "content": "hello", "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "created_at": now, } assert guard.check(event) is None assert guard.check(event) == "duplicate event" def test_reject_disallowed_kind(self, guard): now = int(time.time()) event = { "id": "bad_kind", "kind": 999, "content": "hello", "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "created_at": now, } assert guard.check(event) == "disallowed kind: 999" def test_reject_future_event(self, guard): future = int(time.time()) + 3600 event = { "id": "future_event", "kind": 1, "content": "from the future", "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "created_at": future, } reason = guard.check(event) assert reason is not None assert "future event" in reason def test_reject_oversized_content(self, guard): guard.policy.max_ciphertext_bytes = 10 now = int(time.time()) event = { "id": "big_event", "kind": 4, "content": "x" * 100, "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "created_at": now, } assert guard.check(event) == "ciphertext too large: 100 > 10" class TestRelayHealthTracker: def test_default_score(self): tracker = RelayHealthTracker() assert tracker.get_score("wss://relay.example.com") == 0.5 def test_score_increases_with_success(self): tracker = RelayHealthTracker() tracker.record_success("wss://relay.example.com", latency_ms=100) score = tracker.get_score("wss://relay.example.com") assert score > 0.5 def test_score_decreases_with_failure(self): tracker = RelayHealthTracker() tracker.record_success("wss://relay.example.com", latency_ms=100) good_score = tracker.get_score("wss://relay.example.com") for _ in range(10): tracker.record_failure("wss://relay.example.com") bad_score = tracker.get_score("wss://relay.example.com") assert bad_score < good_score def test_sorted_relays(self): tracker = RelayHealthTracker() tracker.record_success("wss://good.relay", latency_ms=50) tracker.record_success("wss://good.relay", latency_ms=60) tracker.record_failure("wss://bad.relay") tracker.record_failure("wss://bad.relay") tracker.record_success("wss://bad.relay", latency_ms=500) sorted_relays = tracker.sorted_relays(["wss://good.relay", "wss://bad.relay"]) assert sorted_relays[0] == "wss://good.relay" def test_snapshot(self): tracker = RelayHealthTracker() tracker.record_success("wss://relay.example.com", latency_ms=200) snapshot = tracker.snapshot("wss://relay.example.com") assert snapshot.url == "wss://relay.example.com" assert snapshot.total_successes == 1 assert snapshot.total_failures == 0 assert snapshot.avg_latency_ms == 200.0 class TestNostrConfigNew: def test_from_dict_with_guard_policy(self): cfg = NostrConfig.from_dict({ "accounts": { "default": { "private_key": "nsec1guardtest", "guard_policy": { "allowed_kinds": [1, 4], "max_ciphertext_bytes": 20000, "max_future_skew_sec": 60, "rate_limit_window_ms": 5000, "rate_limit_max_per_sender_per_window": 10, } } } }) assert cfg.guard_policy.allowed_kinds == [1, 4] assert cfg.guard_policy.max_ciphertext_bytes == 20000 assert cfg.guard_policy.max_future_skew_sec == 60 assert cfg.guard_policy.rate_limit.window_ms == 5000 assert cfg.guard_policy.rate_limit.max_per_sender_per_window == 10 def test_from_dict_nip42_auth(self): cfg = NostrConfig.from_dict({ "accounts": { "default": { "private_key": "nsec1test", "nip42_auth_enabled": True, "nip42_auth_urls": ["wss://auth.relay"], "backfill_window_sec": 300, } } }) assert cfg.nip42_auth_enabled is True assert cfg.nip42_auth_urls == ["wss://auth.relay"] assert cfg.backfill_window_sec == 300 class TestNormalizeInboundKind7: @pytest.fixture def adapter(self): config = {"private_key": None, "relays": [], "dm_policy": "open"} return NostrAdapter(config=config) def test_normalize_kind7_reaction(self, adapter): raw = { "id": "reaction_001", "kind": 7, "content": "❤", "pubkey": "sender_hex", "tags": [ ["e", "original_event_id"], ["p", "author_pubkey"], ], "created_at": 1715000000, } msg = adapter.normalize_inbound(raw) assert msg.content == "reacted with ❤" assert msg.event_type == EventType.REACTION_ADDED class TestCapabilitiesAndMeta: def test_capabilities_reactions(self): assert NostrAdapter.capabilities.reactions is True def test_capabilities_edit(self): assert NostrAdapter.capabilities.edit is True def test_capabilities_unsend(self): assert NostrAdapter.capabilities.unsend is True def test_capabilities_reply(self): assert NostrAdapter.capabilities.reply is True def test_capabilities_media(self): assert NostrAdapter.capabilities.media is False def test_meta_selection_label(self): assert NostrAdapter.meta.selection_label == "Nostr (Decentralized)" def test_meta_blurb(self): assert "NIP-04" in NostrAdapter.meta.blurb def test_meta_docs_path(self): assert NostrAdapter.meta.docs_path == "/docs/channels/nostr" def test_meta_order(self): assert NostrAdapter.meta.order == 60 class TestRelayManagerPerRelayCB: def test_per_relay_circuit_breakers_created(self): urls = ["wss://relay1.example.com", "wss://relay2.example.com"] manager = RelayManager(urls) assert len(manager._circuit_breakers) == 2 for url in urls: assert manager.get_circuit_breaker(url) is not None def test_set_pubkey_filter(self): urls = ["wss://relay1.example.com"] manager = RelayManager(urls) manager.set_pubkey_filter(["abc123"]) filters = manager._build_subscription_filters( [{"kinds": [1, 4], "since": 1000}] ) assert "#p" in filters[0] assert filters[0]["#p"] == ["abc123"]