mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-07-27 20:12:41 +02:00
c22e4df37d
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.
114 lines
4.0 KiB
Python
114 lines
4.0 KiB
Python
import logging
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from repeater.identity_manager import IdentityManager
|
|
from repeater.main import IdentityConfigurationError, RepeaterDaemon
|
|
|
|
|
|
class _SeedFirstByteIdentity:
|
|
"""Deterministically expose a configured key's first byte as its hash."""
|
|
|
|
def __init__(self, seed: bytes):
|
|
self._public_key = bytes([seed[0]]) + b"P" * 31
|
|
|
|
def get_public_key(self):
|
|
return self._public_key
|
|
|
|
def get_address_bytes(self):
|
|
return self._public_key[:3]
|
|
|
|
|
|
def _config(*, companions=()):
|
|
return {
|
|
"repeater": {"node_name": "n", "identity_key": b"\x10" * 32},
|
|
"logging": {},
|
|
"identities": {"companions": list(companions)},
|
|
}
|
|
|
|
|
|
def test_startup_preflight_rejects_default_repeater_hash_collision():
|
|
daemon = RepeaterDaemon(
|
|
_config(companions=({"name": "comp", "identity_key": "10" * 32},)),
|
|
radio=object(),
|
|
)
|
|
local_identity = _SeedFirstByteIdentity(b"\x10" * 32)
|
|
|
|
with patch("openhop_core.LocalIdentity", _SeedFirstByteIdentity):
|
|
with pytest.raises(IdentityConfigurationError, match="one-byte public-key prefixes"):
|
|
daemon._preflight_configured_local_identities(local_identity)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_companion_set_collision_is_rejected_before_bridge_or_server_creation():
|
|
daemon = RepeaterDaemon(
|
|
_config(
|
|
companions=(
|
|
{"name": "first", "identity_key": "21" * 32},
|
|
{"name": "second", "identity_key": "21" * 32},
|
|
)
|
|
),
|
|
radio=object(),
|
|
)
|
|
daemon.identity_manager = IdentityManager({})
|
|
daemon.repeater_handler = SimpleNamespace(storage=SimpleNamespace(sqlite_handler=object()))
|
|
|
|
with (
|
|
patch("openhop_core.LocalIdentity", _SeedFirstByteIdentity),
|
|
patch("repeater.companion.RepeaterCompanionBridge") as bridge_cls,
|
|
patch("repeater.companion.CompanionFrameServer") as server_cls,
|
|
pytest.raises(IdentityConfigurationError, match="second"),
|
|
):
|
|
await daemon._load_companion_identities()
|
|
|
|
bridge_cls.assert_not_called()
|
|
server_cls.assert_not_called()
|
|
assert daemon.companion_bridges == {}
|
|
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())
|
|
daemon.identity_manager = IdentityManager({})
|
|
daemon.identity_manager.register_identity(
|
|
"repeater", _SeedFirstByteIdentity(b"\x33" * 32), {}, "repeater"
|
|
)
|
|
|
|
comp_config = {"name": "comp", "identity_key": "33" * 32, "settings": {}}
|
|
with (
|
|
patch("openhop_core.LocalIdentity", _SeedFirstByteIdentity),
|
|
patch("repeater.companion.RepeaterCompanionBridge") as bridge_cls,
|
|
patch("repeater.companion.CompanionFrameServer") as server_cls,
|
|
pytest.raises(ValueError, match="Cannot add companion"),
|
|
):
|
|
await daemon.add_companion_from_config(comp_config)
|
|
|
|
bridge_cls.assert_not_called()
|
|
server_cls.assert_not_called()
|
|
assert daemon.companion_bridges == {}
|