From 8ca48cd6bc45906ca763ac710f6a9a51ed03155d Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Mon, 16 Feb 2026 19:06:09 -0800 Subject: [PATCH] Use actual pubkey matching for path update, not default, and don't action the serial path update events --- app/event_handlers.py | 40 +++++++++++++++---- tests/test_event_handlers.py | 75 +++++++++++++++++++++++++++++------- 2 files changed, 95 insertions(+), 20 deletions(-) diff --git a/app/event_handlers.py b/app/event_handlers.py index cd729df..e1e1aac 100644 --- a/app/event_handlers.py +++ b/app/event_handlers.py @@ -189,15 +189,41 @@ async def on_rx_log_data(event: "Event") -> None: async def on_path_update(event: "Event") -> None: """Handle path update events.""" payload = event.payload - logger.debug("Path update for %s", payload.get("pubkey_prefix")) + public_key = str(payload.get("public_key", "")).lower() + pubkey_prefix = str(payload.get("pubkey_prefix", "")).lower() - pubkey_prefix = payload.get("pubkey_prefix", "") - path = payload.get("path", "") - path_len = payload.get("path_len", -1) + contact: Contact | None = None + if public_key: + logger.debug("Path update for %s", public_key[:12]) + contact = await ContactRepository.get_by_key(public_key) + elif pubkey_prefix: + # Legacy compatibility: older payloads may only include a prefix. + logger.debug("Path update for prefix %s", pubkey_prefix) + contact = await ContactRepository.get_by_key_prefix(pubkey_prefix) + else: + logger.debug("PATH_UPDATE missing public_key/pubkey_prefix, skipping") + return - existing = await ContactRepository.get_by_key_prefix(pubkey_prefix) - if existing: - await ContactRepository.update_path(existing.public_key, path, path_len) + if not contact: + return + + # PATH_UPDATE is a serial control push event from firmware (not an RF packet). + # Current meshcore payloads only include public_key for this event. + # RF route/path bytes are handled via RX_LOG_DATA -> process_raw_packet, + # so if path fields are absent here we treat this as informational only. + path = payload.get("path") + path_len = payload.get("path_len") + if path is None or path_len is None: + logger.debug("PATH_UPDATE for %s has no path payload, skipping DB update", contact.public_key[:12]) + return + + try: + normalized_path_len = int(path_len) + except (TypeError, ValueError): + logger.warning("Invalid path_len in PATH_UPDATE for %s: %r", contact.public_key[:12], path_len) + return + + await ContactRepository.update_path(contact.public_key, str(path), normalized_path_len) async def on_new_contact(event: "Event") -> None: diff --git a/tests/test_event_handlers.py b/tests/test_event_handlers.py index c28b5ad..a80e796 100644 --- a/tests/test_event_handlers.py +++ b/tests/test_event_handlers.py @@ -455,7 +455,7 @@ class TestOnPathUpdate: @pytest.mark.asyncio async def test_updates_path_for_existing_contact(self, test_db): - """Path is updated when the contact exists in the database.""" + """Path is updated when the contact exists and payload includes full key.""" from app.event_handlers import on_path_update await ContactRepository.upsert( @@ -469,7 +469,7 @@ class TestOnPathUpdate: class MockEvent: payload = { - "pubkey_prefix": "aaaaaa", + "public_key": "aa" * 32, "path": "0102", "path_len": 2, } @@ -489,7 +489,7 @@ class TestOnPathUpdate: class MockEvent: payload = { - "pubkey_prefix": "unknown", + "public_key": "cc" * 32, "path": "0102", "path_len": 2, } @@ -498,8 +498,8 @@ class TestOnPathUpdate: await on_path_update(MockEvent()) @pytest.mark.asyncio - async def test_uses_defaults_for_missing_payload_fields(self, test_db): - """Missing payload fields fall back to defaults (empty path, -1 length).""" + async def test_legacy_prefix_payload_still_supported(self, test_db): + """Legacy prefix payloads still update path when uniquely resolvable.""" from app.event_handlers import on_path_update await ContactRepository.upsert( @@ -511,20 +511,69 @@ class TestOnPathUpdate: } ) + class MockEvent: + payload = { + "pubkey_prefix": "bbbbbb", + "path": "0a0b", + "path_len": 2, + } + + await on_path_update(MockEvent()) + + contact = await ContactRepository.get_by_key("bb" * 32) + assert contact is not None + assert contact.last_path == "0a0b" + assert contact.last_path_len == 2 + + @pytest.mark.asyncio + async def test_missing_path_fields_does_not_modify_contact(self, test_db): + """Current PATH_UPDATE payloads without path fields should not mutate DB path.""" + from app.event_handlers import on_path_update + + await ContactRepository.upsert( + { + "public_key": "dd" * 32, + "name": "Dana", + "type": 1, + "flags": 0, + } + ) + await ContactRepository.update_path("dd" * 32, "beef", 2) + + class MockEvent: + payload = {"public_key": "dd" * 32} + + await on_path_update(MockEvent()) + + contact = await ContactRepository.get_by_key("dd" * 32) + assert contact is not None + assert contact.last_path == "beef" + assert contact.last_path_len == 2 + + @pytest.mark.asyncio + async def test_missing_identity_fields_noop(self, test_db): + """PATH_UPDATE with no key fields should be a no-op.""" + from app.event_handlers import on_path_update + + await ContactRepository.upsert( + { + "public_key": "ee" * 32, + "name": "Eve", + "type": 1, + "flags": 0, + } + ) + await ContactRepository.update_path("ee" * 32, "abcd", 2) + class MockEvent: payload = {} await on_path_update(MockEvent()) - # With empty prefix, get_by_key_prefix("") should return None since - # no key starts with "" uniquely (if multiple contacts exist) or - # the single contact if only one. But with prefix="", the LIKE query - # matches all contacts. With exactly one contact, it returns it. - # The update_path call sets path="" and path_len=-1. - contact = await ContactRepository.get_by_key("bb" * 32) + contact = await ContactRepository.get_by_key("ee" * 32) assert contact is not None - assert contact.last_path == "" - assert contact.last_path_len == -1 + assert contact.last_path == "abcd" + assert contact.last_path_len == 2 class TestOnNewContact: