Files
Timmy-time-dashboard/tests/unit/test_nostr_identity.py
Claude (Opus 4.6) c5e4657e23
Some checks failed
Tests / lint (push) Has been cancelled
Tests / test (push) Has been cancelled
[claude] Timmy Nostr identity — keypair, profile, relay presence (#856) (#1325)
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
2026-03-24 02:22:39 +00:00

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)