From 586d29011366ec24420f6466070207c7054f7f41 Mon Sep 17 00:00:00 2001 From: Corvo <60719165+brothercorvo@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:22:15 -0300 Subject: [PATCH] Normalize LXMF client config paths --- reticulum_openapi/client.py | 45 +++++++++++++++++++++++++++++--- tests/test_client_extra.py | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/reticulum_openapi/client.py b/reticulum_openapi/client.py index 9e3d1f5..6c136ae 100644 --- a/reticulum_openapi/client.py +++ b/reticulum_openapi/client.py @@ -63,17 +63,24 @@ def __init__( timeout: float = 10.0, shared_instance_rpc_key: Optional[str] = None, ): - self.reticulum = RNS.Reticulum(config_path) + config_directory = self._normalise_config_directory(config_path) + if config_directory is not None: + try: + Path(config_directory).expanduser().mkdir(parents=True, exist_ok=True) + except OSError: + pass + + self.reticulum = RNS.Reticulum(config_directory) self._shared_instance_rpc_key: Optional[bytes] = None if shared_instance_rpc_key is not None: key_bytes = self._decode_shared_instance_rpc_key(shared_instance_rpc_key) self.reticulum.rpc_key = key_bytes self._shared_instance_rpc_key = key_bytes - storage_path = storage_path or (RNS.Reticulum.storagepath + "/lxmf_client") - self.router = LXMF.LXMRouter(storagepath=storage_path) + resolved_storage_path = self._normalise_storage_path(storage_path) + self.router = LXMF.LXMRouter(storagepath=resolved_storage_path) self.router.register_delivery_callback(self._callback) if identity is None: - identity = load_or_create_identity(config_path) + identity = load_or_create_identity(config_directory or config_path) self.identity = identity self.source_identity = self.router.register_delivery_identity( identity, display_name=display_name, stamp_cost=0 @@ -94,6 +101,36 @@ def __init__( self._link_events: Dict[bytes, asyncio.Event] = {} self._links: Dict[bytes, RNS.Link] = {} + @staticmethod + def _normalise_config_directory( + config_path: Optional[str], + ) -> Optional[str]: + """Return a configuration directory compatible with Reticulum.""" + + if not config_path: + return None + + candidate = Path(config_path).expanduser() + if candidate.is_file(): + return str(candidate.parent) + if candidate.name == "config": + return str(candidate.parent) + return str(candidate) + + @staticmethod + def _normalise_storage_path(storage_path: Optional[str]) -> str: + """Return a filesystem path for LXMF client storage.""" + + if storage_path: + resolved = Path(storage_path).expanduser() + else: + resolved = Path(RNS.Reticulum.storagepath).expanduser() / "lxmf_client" + try: + resolved.mkdir(parents=True, exist_ok=True) + except OSError: + pass + return str(resolved) + def _get_link_lock(self, dest_hash: bytes) -> asyncio.Lock: """Return a lock guarding link creation for ``dest_hash``.""" diff --git a/tests/test_client_extra.py b/tests/test_client_extra.py index cd1cc9e..1143331 100644 --- a/tests/test_client_extra.py +++ b/tests/test_client_extra.py @@ -1,5 +1,6 @@ import asyncio import contextlib +from pathlib import Path from types import SimpleNamespace from unittest.mock import AsyncMock, Mock @@ -70,6 +71,56 @@ def fake_register(handler): assert register_calls["handler"].aspect_filter == "lxmf" +def test_client_normalises_config_directory(tmp_path, monkeypatch): + called = {} + + storage_root = tmp_path / "storage_root" + + class DummyReticulum: + storagepath = str(storage_root) + + def __init__(self, config_path=None): + called["config_path"] = config_path + + class DummyIdentity: + def __init__(self): + self.hash = b"h" + + class DummyRouter: + def __init__(self, storagepath=None): + self.storagepath = storagepath + + def register_delivery_callback(self, cb): + self.cb = cb + + def register_delivery_identity(self, ident, display_name=None, stamp_cost=0): + return ident + + def handle_outbound(self, msg): + pass + + monkeypatch.setattr(client_module.RNS, "Reticulum", DummyReticulum) + monkeypatch.setattr(client_module.RNS, "Identity", DummyIdentity) + monkeypatch.setattr(client_module.LXMF, "LXMRouter", DummyRouter) + monkeypatch.setattr(client_module.LXMF, "LXMessage", object) + monkeypatch.setattr( + client_module, + "load_or_create_identity", + lambda *a, **k: DummyIdentity(), + ) + + config_dir = tmp_path / "config" + config_dir.mkdir() + config_file = config_dir / "config" + config_file.write_text("", encoding="utf-8") + + client_module.LXMFClient(config_path=str(config_file)) + + assert called["config_path"] == str(config_dir) + expected_storage = Path(storage_root) / "lxmf_client" + assert expected_storage.is_dir() + + @pytest.mark.asyncio async def test_send_command_bytes_payload(monkeypatch): loop = asyncio.get_running_loop()