mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-01 16:45:46 +00:00
356 lines
12 KiB
Python
356 lines
12 KiB
Python
import json
|
|
import shutil
|
|
import tempfile
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
import RNS
|
|
|
|
from meshchatx.meshchat import ReticulumMeshChat
|
|
|
|
|
|
class ConfigDict(dict):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.write_called = False
|
|
|
|
def write(self):
|
|
self.write_called = True
|
|
return True
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_dir():
|
|
path = tempfile.mkdtemp()
|
|
try:
|
|
yield path
|
|
finally:
|
|
shutil.rmtree(path)
|
|
|
|
|
|
def build_identity():
|
|
identity = MagicMock(spec=RNS.Identity)
|
|
identity.hash = b"test_hash_32_bytes_long_01234567"
|
|
identity.hexhash = identity.hash.hex()
|
|
identity.get_private_key.return_value = b"test_private_key"
|
|
return identity
|
|
|
|
|
|
async def find_route_handler(app_instance, path, method):
|
|
for route in app_instance.get_routes():
|
|
if route.path == path and route.method == method:
|
|
return route.handler
|
|
return None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reticulum_discovery_get_and_patch(temp_dir):
|
|
config = ConfigDict(
|
|
{
|
|
"reticulum": {
|
|
"discover_interfaces": "true",
|
|
"interface_discovery_sources": "abc,def",
|
|
"interface_discovery_whitelist": "tcp-*,10.0.*",
|
|
"interface_discovery_blacklist": "tcp-bad,*:9999",
|
|
"required_discovery_value": "16",
|
|
"autoconnect_discovered_interfaces": "2",
|
|
"network_identity": "/tmp/net_id",
|
|
},
|
|
"interfaces": {},
|
|
},
|
|
)
|
|
|
|
with (
|
|
patch("meshchatx.meshchat.generate_ssl_certificate"),
|
|
patch("RNS.Reticulum") as mock_rns,
|
|
patch("RNS.Transport"),
|
|
patch("LXMF.LXMRouter"),
|
|
):
|
|
mock_reticulum = mock_rns.return_value
|
|
mock_reticulum.config = config
|
|
mock_reticulum.configpath = "/tmp/mock_config"
|
|
mock_reticulum.is_connected_to_shared_instance = False
|
|
mock_reticulum.transport_enabled.return_value = True
|
|
|
|
app_instance = ReticulumMeshChat(
|
|
identity=build_identity(),
|
|
storage_dir=temp_dir,
|
|
reticulum_config_dir=temp_dir,
|
|
)
|
|
|
|
get_handler = await find_route_handler(
|
|
app_instance,
|
|
"/api/v1/reticulum/discovery",
|
|
"GET",
|
|
)
|
|
patch_handler = await find_route_handler(
|
|
app_instance,
|
|
"/api/v1/reticulum/discovery",
|
|
"PATCH",
|
|
)
|
|
assert get_handler and patch_handler
|
|
|
|
# GET returns current reticulum discovery config
|
|
get_response = await get_handler(MagicMock())
|
|
get_data = json.loads(get_response.body)
|
|
assert get_data["discovery"]["discover_interfaces"] == "true"
|
|
assert get_data["discovery"]["interface_discovery_sources"] == "abc,def"
|
|
assert get_data["discovery"]["interface_discovery_whitelist"] == "tcp-*,10.0.*"
|
|
assert (
|
|
get_data["discovery"]["interface_discovery_blacklist"] == "tcp-bad,*:9999"
|
|
)
|
|
assert get_data["discovery"]["required_discovery_value"] == "16"
|
|
assert get_data["discovery"]["autoconnect_discovered_interfaces"] == "2"
|
|
assert get_data["discovery"]["network_identity"] == "/tmp/net_id"
|
|
|
|
# PATCH updates and persists
|
|
new_config = {
|
|
"discover_interfaces": False,
|
|
"interface_discovery_sources": "",
|
|
"interface_discovery_whitelist": "peer-*,172.16.*",
|
|
"interface_discovery_blacklist": "",
|
|
"required_discovery_value": 18,
|
|
"autoconnect_discovered_interfaces": 5,
|
|
"network_identity": "/tmp/other_id",
|
|
}
|
|
|
|
class PatchRequest:
|
|
@staticmethod
|
|
async def json():
|
|
return new_config
|
|
|
|
patch_response = await patch_handler(PatchRequest())
|
|
patch_data = json.loads(patch_response.body)
|
|
assert patch_data["discovery"]["discover_interfaces"] is False
|
|
assert patch_data["discovery"]["interface_discovery_sources"] is None
|
|
assert (
|
|
patch_data["discovery"]["interface_discovery_whitelist"]
|
|
== "peer-*,172.16.*"
|
|
)
|
|
assert patch_data["discovery"]["interface_discovery_blacklist"] is None
|
|
assert patch_data["discovery"]["required_discovery_value"] == 18
|
|
assert patch_data["discovery"]["autoconnect_discovered_interfaces"] == 5
|
|
assert patch_data["discovery"]["network_identity"] == "/tmp/other_id"
|
|
assert config["reticulum"]["discover_interfaces"] is False
|
|
assert "interface_discovery_sources" not in config["reticulum"]
|
|
assert config["reticulum"]["interface_discovery_whitelist"] == "peer-*,172.16.*"
|
|
assert "interface_discovery_blacklist" not in config["reticulum"]
|
|
assert config["reticulum"]["required_discovery_value"] == 18
|
|
assert config["reticulum"]["autoconnect_discovered_interfaces"] == 5
|
|
assert config["reticulum"]["network_identity"] == "/tmp/other_id"
|
|
assert config.write_called
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_discovered_interfaces_respect_whitelist_and_blacklist(temp_dir):
|
|
config = ConfigDict(
|
|
{
|
|
"reticulum": {
|
|
"interface_discovery_whitelist": "peer-*,10.0.*",
|
|
"interface_discovery_blacklist": "*:9999,bad-*",
|
|
},
|
|
"interfaces": {},
|
|
},
|
|
)
|
|
|
|
with (
|
|
patch("meshchatx.meshchat.generate_ssl_certificate"),
|
|
patch("RNS.Reticulum") as mock_rns,
|
|
patch("RNS.Transport"),
|
|
patch("LXMF.LXMRouter"),
|
|
patch("meshchatx.meshchat.InterfaceDiscovery") as mock_discovery_cls,
|
|
):
|
|
mock_reticulum = mock_rns.return_value
|
|
mock_reticulum.config = config
|
|
mock_reticulum.configpath = "/tmp/mock_config"
|
|
mock_reticulum.is_connected_to_shared_instance = False
|
|
mock_reticulum.transport_enabled.return_value = True
|
|
mock_reticulum.get_interface_stats.return_value = {"interfaces": []}
|
|
|
|
mock_discovery = mock_discovery_cls.return_value
|
|
mock_discovery.list_discovered_interfaces.return_value = [
|
|
{
|
|
"name": "peer-good-1",
|
|
"type": "TCPClientInterface",
|
|
"reachable_on": "10.0.0.7",
|
|
"port": 4242,
|
|
},
|
|
{
|
|
"name": "peer-blocked-port",
|
|
"type": "TCPClientInterface",
|
|
"reachable_on": "10.0.0.8",
|
|
"port": 9999,
|
|
},
|
|
{
|
|
"name": "bad-node",
|
|
"type": "TCPClientInterface",
|
|
"reachable_on": "10.0.0.9",
|
|
"port": 4242,
|
|
},
|
|
{
|
|
"name": "other-network",
|
|
"type": "TCPClientInterface",
|
|
"reachable_on": "192.168.1.10",
|
|
"port": 4242,
|
|
},
|
|
]
|
|
|
|
app_instance = ReticulumMeshChat(
|
|
identity=build_identity(),
|
|
storage_dir=temp_dir,
|
|
reticulum_config_dir=temp_dir,
|
|
)
|
|
|
|
handler = await find_route_handler(
|
|
app_instance,
|
|
"/api/v1/reticulum/discovered-interfaces",
|
|
"GET",
|
|
)
|
|
assert handler
|
|
|
|
response = await handler(MagicMock())
|
|
data = json.loads(response.body)
|
|
interfaces = data["interfaces"]
|
|
|
|
assert len(interfaces) == 1
|
|
assert interfaces[0]["name"] == "peer-good-1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_discovery_patch_sanitizes_whitelist_blacklist_values(temp_dir):
|
|
config = ConfigDict({"reticulum": {}, "interfaces": {}})
|
|
|
|
with (
|
|
patch("meshchatx.meshchat.generate_ssl_certificate"),
|
|
patch("RNS.Reticulum") as mock_rns,
|
|
patch("RNS.Transport"),
|
|
patch("LXMF.LXMRouter"),
|
|
):
|
|
mock_reticulum = mock_rns.return_value
|
|
mock_reticulum.config = config
|
|
mock_reticulum.configpath = "/tmp/mock_config"
|
|
mock_reticulum.is_connected_to_shared_instance = False
|
|
mock_reticulum.transport_enabled.return_value = True
|
|
|
|
app_instance = ReticulumMeshChat(
|
|
identity=build_identity(),
|
|
storage_dir=temp_dir,
|
|
reticulum_config_dir=temp_dir,
|
|
)
|
|
|
|
patch_handler = await find_route_handler(
|
|
app_instance,
|
|
"/api/v1/reticulum/discovery",
|
|
"PATCH",
|
|
)
|
|
assert patch_handler
|
|
|
|
payload = {
|
|
"interface_discovery_whitelist": "peer-1,\npeer-1,host:4242,\r\n,\n",
|
|
"interface_discovery_blacklist": ["bad-node", "bad-node", "evil,\nentry"],
|
|
}
|
|
|
|
class PatchRequest:
|
|
@staticmethod
|
|
async def json():
|
|
return payload
|
|
|
|
response = await patch_handler(PatchRequest())
|
|
data = json.loads(response.body)
|
|
|
|
assert data["discovery"]["interface_discovery_whitelist"] == "peer-1,host:4242"
|
|
assert (
|
|
data["discovery"]["interface_discovery_blacklist"] == "bad-node,evilentry"
|
|
)
|
|
assert (
|
|
config["reticulum"]["interface_discovery_whitelist"] == "peer-1,host:4242"
|
|
)
|
|
assert (
|
|
config["reticulum"]["interface_discovery_blacklist"] == "bad-node,evilentry"
|
|
)
|
|
assert config.write_called
|
|
|
|
|
|
def test_filter_discovered_interfaces_handles_non_list_inputs():
|
|
result = ReticulumMeshChat.filter_discovered_interfaces(
|
|
{"unexpected": "shape"},
|
|
"peer-*",
|
|
"bad-*",
|
|
)
|
|
assert result == {"unexpected": "shape"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_interface_add_includes_discovery_fields(temp_dir):
|
|
config = ConfigDict({"reticulum": {}, "interfaces": {}})
|
|
|
|
with (
|
|
patch("meshchatx.meshchat.generate_ssl_certificate"),
|
|
patch("RNS.Reticulum") as mock_rns,
|
|
patch("RNS.Transport"),
|
|
patch("LXMF.LXMRouter"),
|
|
):
|
|
mock_reticulum = mock_rns.return_value
|
|
mock_reticulum.config = config
|
|
mock_reticulum.configpath = "/tmp/mock_config"
|
|
mock_reticulum.is_connected_to_shared_instance = False
|
|
mock_reticulum.transport_enabled.return_value = True
|
|
|
|
app_instance = ReticulumMeshChat(
|
|
identity=build_identity(),
|
|
storage_dir=temp_dir,
|
|
reticulum_config_dir=temp_dir,
|
|
)
|
|
|
|
add_handler = await find_route_handler(
|
|
app_instance,
|
|
"/api/v1/reticulum/interfaces/add",
|
|
"POST",
|
|
)
|
|
assert add_handler
|
|
|
|
payload = {
|
|
"allow_overwriting_interface": False,
|
|
"name": "TestIface",
|
|
"type": "TCPClientInterface",
|
|
"target_host": "example.com",
|
|
"target_port": "4242",
|
|
"discoverable": "yes",
|
|
"discovery_name": "Region A",
|
|
"announce_interval": 720,
|
|
"reachable_on": "/usr/bin/get_ip.sh",
|
|
"discovery_stamp_value": 22,
|
|
"discovery_encrypt": True,
|
|
"publish_ifac": True,
|
|
"latitude": 10.1,
|
|
"longitude": 20.2,
|
|
"height": 30,
|
|
"discovery_frequency": 915000000,
|
|
"discovery_bandwidth": 125000,
|
|
"discovery_modulation": "LoRa",
|
|
}
|
|
|
|
class AddRequest:
|
|
@staticmethod
|
|
async def json():
|
|
return payload
|
|
|
|
response = await add_handler(AddRequest())
|
|
data = json.loads(response.body)
|
|
assert "Interface has been added" in data["message"]
|
|
saved = config["interfaces"]["TestIface"]
|
|
assert saved["discoverable"] == "yes"
|
|
assert saved["discovery_name"] == "Region A"
|
|
assert saved["announce_interval"] == 720
|
|
assert saved["reachable_on"] == "/usr/bin/get_ip.sh"
|
|
assert saved["discovery_stamp_value"] == 22
|
|
assert saved["discovery_encrypt"] is True
|
|
assert saved["publish_ifac"] is True
|
|
assert saved["latitude"] == 10.1
|
|
assert saved["longitude"] == 20.2
|
|
assert saved["height"] == 30
|
|
assert saved["discovery_frequency"] == 915000000
|
|
assert saved["discovery_bandwidth"] == 125000
|
|
assert saved["discovery_modulation"] == "LoRa"
|
|
assert config.write_called
|