Co-authored-by: Claude (Opus 4.6) <claude@hermes.local> Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
273 lines
9.7 KiB
Python
273 lines
9.7 KiB
Python
"""Unit tests for infrastructure.nostr.identity."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from infrastructure.nostr.identity import AnnounceResult, NostrIdentityManager
|
|
from infrastructure.nostr.keypair import generate_keypair
|
|
|
|
|
|
@pytest.fixture()
|
|
def manager():
|
|
return NostrIdentityManager()
|
|
|
|
|
|
@pytest.fixture()
|
|
def kp():
|
|
return generate_keypair()
|
|
|
|
|
|
class TestAnnounceResult:
|
|
def test_any_relay_ok_false_when_empty(self):
|
|
r = AnnounceResult()
|
|
assert r.any_relay_ok is False
|
|
|
|
def test_any_relay_ok_true_when_one_ok(self):
|
|
r = AnnounceResult(relay_results={"wss://a": True, "wss://b": False})
|
|
assert r.any_relay_ok is True
|
|
|
|
def test_to_dict_keys(self):
|
|
r = AnnounceResult(kind_0_ok=True, relay_results={"wss://a": True})
|
|
d = r.to_dict()
|
|
assert set(d) == {"kind_0", "kind_31990", "relays"}
|
|
|
|
|
|
class TestGetKeypair:
|
|
def test_returns_none_when_no_privkey(self, manager):
|
|
mock_settings = MagicMock(nostr_privkey="")
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
assert manager.get_keypair() is None
|
|
|
|
def test_returns_keypair_when_configured(self, manager, kp):
|
|
mock_settings = MagicMock(nostr_privkey=kp.privkey_hex)
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
result = manager.get_keypair()
|
|
assert result is not None
|
|
assert result.pubkey_hex == kp.pubkey_hex
|
|
|
|
def test_returns_none_on_invalid_key(self, manager):
|
|
mock_settings = MagicMock(nostr_privkey="not_a_valid_key")
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
assert manager.get_keypair() is None
|
|
|
|
|
|
class TestGetRelayUrls:
|
|
def test_empty_string_returns_empty_list(self, manager):
|
|
mock_settings = MagicMock(nostr_relays="")
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
assert manager.get_relay_urls() == []
|
|
|
|
def test_single_relay(self, manager):
|
|
mock_settings = MagicMock(nostr_relays="wss://relay.damus.io")
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
urls = manager.get_relay_urls()
|
|
assert urls == ["wss://relay.damus.io"]
|
|
|
|
def test_multiple_relays(self, manager):
|
|
mock_settings = MagicMock(nostr_relays="wss://a.com,wss://b.com, wss://c.com ")
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
urls = manager.get_relay_urls()
|
|
assert urls == ["wss://a.com", "wss://b.com", "wss://c.com"]
|
|
|
|
|
|
class TestBuildProfileEvent:
|
|
def test_kind_is_0(self, manager, kp):
|
|
mock_settings = MagicMock(
|
|
nostr_profile_name="Timmy",
|
|
nostr_profile_about="",
|
|
nostr_profile_picture="",
|
|
nostr_nip05="",
|
|
)
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
ev = manager.build_profile_event(kp)
|
|
assert ev["kind"] == 0
|
|
|
|
def test_content_contains_name(self, manager, kp):
|
|
mock_settings = MagicMock(
|
|
nostr_profile_name="Timmy",
|
|
nostr_profile_about="A great AI agent",
|
|
nostr_profile_picture="",
|
|
nostr_nip05="",
|
|
)
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
ev = manager.build_profile_event(kp)
|
|
profile = json.loads(ev["content"])
|
|
assert profile["name"] == "Timmy"
|
|
|
|
def test_nip05_included_when_set(self, manager, kp):
|
|
mock_settings = MagicMock(
|
|
nostr_profile_name="Timmy",
|
|
nostr_profile_about="",
|
|
nostr_profile_picture="",
|
|
nostr_nip05="timmy@tower.local",
|
|
)
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
ev = manager.build_profile_event(kp)
|
|
profile = json.loads(ev["content"])
|
|
assert profile["nip05"] == "timmy@tower.local"
|
|
|
|
def test_nip05_omitted_when_empty(self, manager, kp):
|
|
mock_settings = MagicMock(
|
|
nostr_profile_name="Timmy",
|
|
nostr_profile_about="",
|
|
nostr_profile_picture="",
|
|
nostr_nip05="",
|
|
)
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
ev = manager.build_profile_event(kp)
|
|
profile = json.loads(ev["content"])
|
|
assert "nip05" not in profile
|
|
|
|
def test_default_name_when_blank(self, manager, kp):
|
|
mock_settings = MagicMock(
|
|
nostr_profile_name="",
|
|
nostr_profile_about="",
|
|
nostr_profile_picture="",
|
|
nostr_nip05="",
|
|
)
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
ev = manager.build_profile_event(kp)
|
|
profile = json.loads(ev["content"])
|
|
assert profile["name"] == "Timmy" # default
|
|
|
|
|
|
class TestBuildCapabilityEvent:
|
|
def test_kind_is_31990(self, manager, kp):
|
|
mock_settings = MagicMock(
|
|
nostr_profile_name="Timmy",
|
|
nostr_profile_about="",
|
|
nostr_profile_picture="",
|
|
nostr_nip05="",
|
|
)
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
ev = manager.build_capability_event(kp)
|
|
assert ev["kind"] == 31990
|
|
|
|
def test_has_d_tag(self, manager, kp):
|
|
mock_settings = MagicMock(
|
|
nostr_profile_name="Timmy",
|
|
nostr_profile_about="",
|
|
nostr_profile_picture="",
|
|
nostr_nip05="",
|
|
)
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
ev = manager.build_capability_event(kp)
|
|
d_tags = [t for t in ev["tags"] if t[0] == "d"]
|
|
assert d_tags
|
|
assert d_tags[0][1] == "timmy-mission-control"
|
|
|
|
def test_content_is_json(self, manager, kp):
|
|
mock_settings = MagicMock(
|
|
nostr_profile_name="Timmy",
|
|
nostr_profile_about="",
|
|
nostr_profile_picture="",
|
|
nostr_nip05="",
|
|
)
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
ev = manager.build_capability_event(kp)
|
|
parsed = json.loads(ev["content"])
|
|
assert "name" in parsed
|
|
assert "capabilities" in parsed
|
|
|
|
|
|
class TestAnnounce:
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_result_when_no_privkey(self, manager):
|
|
mock_settings = MagicMock(
|
|
nostr_privkey="",
|
|
nostr_relays="",
|
|
nostr_profile_name="Timmy",
|
|
nostr_profile_about="",
|
|
nostr_profile_picture="",
|
|
nostr_nip05="",
|
|
)
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
result = await manager.announce()
|
|
assert result.kind_0_ok is False
|
|
assert result.kind_31990_ok is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_result_when_no_relays(self, manager, kp):
|
|
mock_settings = MagicMock(
|
|
nostr_privkey=kp.privkey_hex,
|
|
nostr_relays="",
|
|
nostr_profile_name="Timmy",
|
|
nostr_profile_about="",
|
|
nostr_profile_picture="",
|
|
nostr_nip05="",
|
|
)
|
|
with patch("infrastructure.nostr.identity.settings", mock_settings):
|
|
result = await manager.announce()
|
|
assert result.kind_0_ok is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_publishes_kind0_and_kind31990(self, manager, kp):
|
|
mock_settings = MagicMock(
|
|
nostr_privkey=kp.privkey_hex,
|
|
nostr_relays="wss://relay.test",
|
|
nostr_profile_name="Timmy",
|
|
nostr_profile_about="Test agent",
|
|
nostr_profile_picture="",
|
|
nostr_nip05="timmy@test",
|
|
)
|
|
with (
|
|
patch("infrastructure.nostr.identity.settings", mock_settings),
|
|
patch(
|
|
"infrastructure.nostr.identity.publish_to_relays",
|
|
new=AsyncMock(return_value={"wss://relay.test": True}),
|
|
) as mock_publish,
|
|
):
|
|
result = await manager.announce()
|
|
|
|
assert mock_publish.call_count == 2 # kind 0 + kind 31990
|
|
assert result.kind_0_ok is True
|
|
assert result.kind_31990_ok is True
|
|
assert result.relay_results["wss://relay.test"] is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_degrades_gracefully_on_relay_failure(self, manager, kp):
|
|
mock_settings = MagicMock(
|
|
nostr_privkey=kp.privkey_hex,
|
|
nostr_relays="wss://relay.test",
|
|
nostr_profile_name="Timmy",
|
|
nostr_profile_about="",
|
|
nostr_profile_picture="",
|
|
nostr_nip05="",
|
|
)
|
|
with (
|
|
patch("infrastructure.nostr.identity.settings", mock_settings),
|
|
patch(
|
|
"infrastructure.nostr.identity.publish_to_relays",
|
|
new=AsyncMock(return_value={"wss://relay.test": False}),
|
|
),
|
|
):
|
|
result = await manager.announce()
|
|
|
|
assert result.kind_0_ok is False
|
|
assert result.kind_31990_ok is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_never_raises_on_exception(self, manager, kp):
|
|
mock_settings = MagicMock(
|
|
nostr_privkey=kp.privkey_hex,
|
|
nostr_relays="wss://relay.test",
|
|
nostr_profile_name="Timmy",
|
|
nostr_profile_about="",
|
|
nostr_profile_picture="",
|
|
nostr_nip05="",
|
|
)
|
|
with (
|
|
patch("infrastructure.nostr.identity.settings", mock_settings),
|
|
patch(
|
|
"infrastructure.nostr.identity.publish_to_relays",
|
|
new=AsyncMock(side_effect=Exception("relay exploded")),
|
|
),
|
|
):
|
|
# Must not raise
|
|
result = await manager.announce()
|
|
assert isinstance(result, AnnounceResult)
|