refactor(repeater): consolidate identity collision validation

The startup preflight re-implemented the identity collision rules in
main.py, reconstructing IdentityManager state by string-parsing its
'type:name' labels, and every configured identity was parsed and
constructed twice (once for preflight, once for loading), duplicating
config warnings on each start.

Move batch validation into IdentityManager.validate_specs(), which
checks a batch of IdentitySpec entries against registered identities
and against each other without mutating state, and relocate
IdentityConfigurationError next to it. The daemon now parses room
server and companion configs once during preflight and the identity
loaders reuse the cached specs.
This commit is contained in:
agessaman
2026-07-15 00:12:42 -07:00
parent 1b5cc71ed5
commit c22e4df37d
6 changed files with 186 additions and 62 deletions
+1
View File
@@ -147,6 +147,7 @@ class TestCompanionInitSurfacesLoadFailure:
},
}
daemon = RepeaterDaemon(config, radio=object())
daemon.identity_manager = IdentityManager({})
daemon.router = SimpleNamespace(inject_packet=AsyncMock())
daemon.repeater_handler = SimpleNamespace(
storage=SimpleNamespace(sqlite_handler=sqlite), radio_config={}
@@ -1,3 +1,4 @@
import logging
from types import SimpleNamespace
from unittest.mock import patch
@@ -68,6 +69,28 @@ async def test_companion_set_collision_is_rejected_before_bridge_or_server_creat
assert daemon.companion_frame_servers == []
@pytest.mark.asyncio
async def test_invalid_config_entry_logs_once_across_preflight_and_load(caplog):
"""Preflight parses the config once and the loaders reuse the cached
specs, so an invalid entry produces exactly one error per startup."""
daemon = RepeaterDaemon(
_config(companions=({"name": "bad", "identity_key": "not-hex"},)),
radio=object(),
)
daemon.identity_manager = IdentityManager({})
local_identity = _SeedFirstByteIdentity(b"\x10" * 32)
with (
patch("openhop_core.LocalIdentity", _SeedFirstByteIdentity),
caplog.at_level(logging.ERROR, logger="RepeaterDaemon"),
):
daemon._preflight_configured_local_identities(local_identity)
await daemon._load_companion_identities()
invalid_key_logs = [r for r in caplog.records if "invalid hex" in r.getMessage()]
assert len(invalid_key_logs) == 1
@pytest.mark.asyncio
async def test_hot_added_companion_collision_is_rejected_before_stateful_setup():
daemon = RepeaterDaemon(_config(), radio=object())
+60 -1
View File
@@ -1,4 +1,6 @@
from repeater.identity_manager import IdentityManager
import pytest
from repeater.identity_manager import IdentityConfigurationError, IdentityManager, IdentitySpec
class _FakeIdentity:
@@ -67,3 +69,60 @@ def test_identity_manager_list_handles_none_identity_fields():
listed = mgr.list_identities()
assert listed[0]["address"] == "N/A"
assert listed[0]["public_key"] is None
def test_validate_specs_rejects_intra_batch_hash_collision():
mgr = IdentityManager(config={})
id_a = _FakeIdentity(bytes([0x11]) + b"A" * 31)
id_b = _FakeIdentity(bytes([0x11]) + b"B" * 31)
with pytest.raises(IdentityConfigurationError, match="one-byte public-key prefixes"):
mgr.validate_specs(
[
IdentitySpec("alpha", id_a, {}, "repeater"),
IdentitySpec("beta", id_b, {}, "companion"),
]
)
def test_validate_specs_rejects_intra_batch_duplicate_name():
mgr = IdentityManager(config={})
id_a = _FakeIdentity(bytes([0x11]) + b"A" * 31)
id_b = _FakeIdentity(bytes([0x22]) + b"B" * 31)
with pytest.raises(IdentityConfigurationError, match="repeater:alpha"):
mgr.validate_specs(
[
IdentitySpec("alpha", id_a, {}, "repeater"),
IdentitySpec("alpha", id_b, {}, "companion"),
]
)
def test_validate_specs_rejects_registered_collisions_without_mutation():
mgr = IdentityManager(config={})
id_a = _FakeIdentity(bytes([0x11]) + b"A" * 31)
id_hash_collision = _FakeIdentity(bytes([0x11]) + b"B" * 31)
id_name_collision = _FakeIdentity(bytes([0x22]) + b"C" * 31)
assert mgr.register_identity("alpha", id_a, {}, "repeater") is True
with pytest.raises(IdentityConfigurationError, match="conflicts"):
mgr.validate_specs([IdentitySpec("beta", id_hash_collision, {}, "companion")])
with pytest.raises(IdentityConfigurationError, match="already registered"):
mgr.validate_specs([IdentitySpec("alpha", id_name_collision, {}, "companion")])
# Validation never registers anything.
assert mgr.get_identity_by_hash(0x22) is None
assert mgr.get_identity_by_name("beta") is None
def test_validate_specs_accepts_distinct_batch():
mgr = IdentityManager(config={})
mgr.validate_specs(
[
IdentitySpec("alpha", _FakeIdentity(bytes([0x11]) + b"A" * 31), {}, "repeater"),
IdentitySpec("beta", _FakeIdentity(bytes([0x22]) + b"B" * 31), {}, "room_server"),
IdentitySpec("gamma", _FakeIdentity(bytes([0x33]) + b"C" * 31), {}, "companion"),
]
)
+2 -1
View File
@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from repeater.identity_manager import IdentityManager
from repeater.main import RepeaterDaemon
@@ -43,7 +44,7 @@ async def test_load_additional_identities_valid_and_invalid_entries():
}
daemon = RepeaterDaemon(cfg, radio=object())
daemon.identity_manager = SimpleNamespace(list_identities=lambda: [1, 2])
daemon.identity_manager = IdentityManager({})
daemon._register_identity_everywhere = MagicMock(return_value=True)
with patch("openhop_core.LocalIdentity", _FakeLocalIdentity):