import struct import time from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch import pytest from repeater.handler_helpers.path import PathHelper from repeater.handler_helpers.protocol_request import ProtocolRequestHelper from repeater.handler_helpers.text import TextHelper class _FakeId: def __init__(self, pubkey: bytes): self._pubkey = pubkey def get_public_key(self): return self._pubkey class _FakeClient: def __init__(self, pubkey: bytes, shared_secret: bytes, permissions=0): self.id = _FakeId(pubkey) self.shared_secret = shared_secret self.permissions = permissions self.out_path = bytearray() self.out_path_len = -1 class _FakeACL: def __init__(self, clients): self._clients = list(clients) def get_all_clients(self): return self._clients class _PathPacket: def __init__(self, payload: bytes): self.payload = bytearray(payload) class _ReqPacket: def __init__(self, payload: bytes): self.payload = bytearray(payload) self.mark_do_not_retransmit = MagicMock() @pytest.mark.asyncio async def test_path_helper_updates_client_out_path_on_valid_decrypt(): client = _FakeClient(pubkey=bytes([0x22]) + b"x" * 31, shared_secret=b"k" * 32) acl = _FakeACL([client]) helper = PathHelper(acl_dict={0x11: acl}) # Payload: dest(0x11), src(0x22), mac+data... packet = _PathPacket(payload=b"\x11\x22\xAA\xBB\xCC") with patch("pymc_core.protocol.crypto.CryptoUtils.mac_then_decrypt", return_value=b"\x02\x99\x88\x01"): handled = await helper.process_path_packet(packet) assert handled is False assert client.out_path_len == 2 assert bytes(client.out_path) == b"\x99\x88" assert isinstance(client.last_activity, int) @pytest.mark.asyncio async def test_path_helper_returns_false_for_non_matching_or_invalid_inputs(): client = _FakeClient(pubkey=bytes([0x22]) + b"x" * 31, shared_secret=b"k" * 32) acl = _FakeACL([client]) helper = PathHelper(acl_dict={0x11: acl}) assert await helper.process_path_packet(_PathPacket(payload=b"\x11")) is False assert await helper.process_path_packet(_PathPacket(payload=b"\x33\x22\xAA\xBB")) is False no_secret_client = _FakeClient(pubkey=bytes([0x22]) + b"x" * 31, shared_secret=b"") helper_no_secret = PathHelper(acl_dict={0x11: _FakeACL([no_secret_client])}) assert await helper_no_secret.process_path_packet(_PathPacket(payload=b"\x11\x22\xAA\xBB")) is False with patch("pymc_core.protocol.crypto.CryptoUtils.mac_then_decrypt", return_value=None): assert await helper.process_path_packet(_PathPacket(payload=b"\x11\x22\xAA\xBB")) is False @pytest.mark.asyncio async def test_protocol_request_process_routes_and_marks_no_retransmit(): injector = AsyncMock(return_value=True) helper = ProtocolRequestHelper(identity_manager=MagicMock(), packet_injector=injector) assert await helper.process_request_packet(_ReqPacket(payload=b"\x01")) is False pkt_unknown = _ReqPacket(payload=b"\x99\x01") assert await helper.process_request_packet(pkt_unknown) is False dest = 0x42 response_packet = object() async def _core_handler(_packet): return response_packet helper.handlers[dest] = {"handler": _core_handler} pkt = _ReqPacket(payload=bytes([dest, 0x01, 0x02])) with patch("repeater.handler_helpers.protocol_request.asyncio.sleep", new_callable=AsyncMock): handled = await helper.process_request_packet(pkt) assert handled is True pkt.mark_do_not_retransmit.assert_called_once() injector.assert_awaited_once_with(response_packet, wait_for_ack=False) @pytest.mark.asyncio async def test_protocol_request_process_exception_returns_false(): helper = ProtocolRequestHelper(identity_manager=MagicMock(), packet_injector=AsyncMock()) async def _boom(_packet): raise RuntimeError("oops") helper.handlers[0x33] = {"handler": _boom} pkt = _ReqPacket(payload=b"\x33\x01") assert await helper.process_request_packet(pkt) is False def test_protocol_request_handle_get_status_builds_56_byte_payload(): engine = SimpleNamespace( start_time=time.time() - 120, rx_count=7, forwarded_count=5, sent_flood_count=2, sent_direct_count=3, recv_flood_count=4, recv_direct_count=1, direct_dup_count=6, flood_dup_count=8, airtime_mgr=SimpleNamespace(total_airtime_ms=9300, total_rx_airtime_ms=4200), ) radio = SimpleNamespace( get_noise_floor=lambda: -110, get_last_rssi=lambda: -70, get_last_snr=lambda: 2.5, crc_error_count=11, ) helper = ProtocolRequestHelper( identity_manager=MagicMock(), packet_injector=AsyncMock(), radio=radio, engine=engine, ) data = helper._handle_get_status(client=None, timestamp=0, req_data=b"") assert isinstance(data, (bytes, bytearray)) assert len(data) == 56 def test_protocol_request_access_list_admin_and_reserved_rules(): admin = SimpleNamespace(is_admin=lambda: True) not_admin = SimpleNamespace(is_admin=lambda: False) c1 = _FakeClient(pubkey=b"A" * 32, shared_secret=b"k" * 32, permissions=0x02) c2 = _FakeClient(pubkey=b"B" * 32, shared_secret=b"k" * 32, permissions=0x00) acl = _FakeACL([c1, c2]) helper = ProtocolRequestHelper(identity_manager=MagicMock(), packet_injector=AsyncMock()) assert helper._handle_get_access_list(not_admin, 0, b"\x00\x00", acl) is None assert helper._handle_get_access_list(admin, 0, b"\x01\x00", acl) is None out = helper._handle_get_access_list(admin, 0, b"\x00\x00", acl) assert isinstance(out, bytes) # One active entry only: 6-byte key prefix + 1-byte perms assert len(out) == 7 assert out[-1] == 0x02 def test_protocol_request_get_neighbours_sort_and_pagination(): neighbors = { "AA" * 16: {"is_repeater": True, "zero_hop": True, "last_seen": time.time() - 1, "snr": 5.0}, "BB" * 16: {"is_repeater": True, "zero_hop": True, "last_seen": time.time() - 10, "snr": 1.0}, "CC" * 16: {"is_repeater": False, "zero_hop": True, "last_seen": time.time() - 1, "snr": 9.0}, } storage = SimpleNamespace(get_neighbors=lambda: neighbors) helper = ProtocolRequestHelper( identity_manager=MagicMock(), packet_injector=AsyncMock(), neighbor_tracker=SimpleNamespace(storage=storage), ) # version=0, count=2, offset=0, order_by=2(strongest), pubkey_prefix_len=4, random=0 req = bytes([0, 2]) + struct.pack("