mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
184 lines
5.7 KiB
Python
184 lines
5.7 KiB
Python
|
|
# SPDX-License-Identifier: 0BSD
|
||
|
|
|
||
|
|
"""Oracle: Interfaces tiles must include every on-disk [[section]]."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import shutil
|
||
|
|
import tempfile
|
||
|
|
from unittest.mock import MagicMock, patch
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import RNS
|
||
|
|
|
||
|
|
from meshchatx.meshchat import ReticulumMeshChat
|
||
|
|
from meshchatx.src.backend.interface_config_parser import InterfaceConfigParser
|
||
|
|
|
||
|
|
|
||
|
|
DOTTED_CONFIG = """\
|
||
|
|
[reticulum]
|
||
|
|
enable_transport = False
|
||
|
|
|
||
|
|
[interfaces]
|
||
|
|
[[Default Interface]]
|
||
|
|
type = AutoInterface
|
||
|
|
interface_enabled = True
|
||
|
|
[[artyom.ddns.net]]
|
||
|
|
type = TCPClientInterface
|
||
|
|
interface_enabled = True
|
||
|
|
target_host = 10.100.11.12
|
||
|
|
target_port = 4242
|
||
|
|
name = artyom.ddns.net
|
||
|
|
[[Catz-Node (TCP)]]
|
||
|
|
type = TCPClientInterface
|
||
|
|
interface_enabled = True
|
||
|
|
target_host = 1.2.3.4
|
||
|
|
target_port = 4242
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def temp_dir():
|
||
|
|
dir_path = tempfile.mkdtemp()
|
||
|
|
yield dir_path
|
||
|
|
shutil.rmtree(dir_path)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_rns_minimal():
|
||
|
|
with (
|
||
|
|
patch("RNS.Reticulum") as mock_rns,
|
||
|
|
patch("RNS.Transport"),
|
||
|
|
patch("LXMF.LXMRouter"),
|
||
|
|
patch("meshchatx.meshchat.get_file_path", return_value="/tmp/mock_path"),
|
||
|
|
):
|
||
|
|
mock_rns_instance = mock_rns.return_value
|
||
|
|
mock_rns_instance.configpath = "/tmp/mock_config"
|
||
|
|
mock_rns_instance.is_connected_to_shared_instance = False
|
||
|
|
mock_rns_instance.transport_enabled.return_value = True
|
||
|
|
|
||
|
|
mock_id = MagicMock(spec=RNS.Identity)
|
||
|
|
mock_id.hash = b"test_hash_32_bytes_long_01234567"
|
||
|
|
mock_id.hexhash = mock_id.hash.hex()
|
||
|
|
mock_id.get_private_key.return_value = b"test_private_key"
|
||
|
|
yield mock_id
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def app(mock_rns_minimal, temp_dir):
|
||
|
|
with patch("meshchatx.meshchat.generate_ssl_certificate"):
|
||
|
|
instance = ReticulumMeshChat(
|
||
|
|
identity=mock_rns_minimal,
|
||
|
|
storage_dir=temp_dir,
|
||
|
|
reticulum_config_dir=os.path.join(temp_dir, ".reticulum"),
|
||
|
|
)
|
||
|
|
instance.reticulum.config = {
|
||
|
|
"reticulum": {},
|
||
|
|
"interfaces": {
|
||
|
|
"Default Interface": {
|
||
|
|
"type": "AutoInterface",
|
||
|
|
"interface_enabled": "True",
|
||
|
|
},
|
||
|
|
"Catz-Node (TCP)": {
|
||
|
|
"type": "TCPClientInterface",
|
||
|
|
"interface_enabled": "True",
|
||
|
|
"target_host": "1.2.3.4",
|
||
|
|
"target_port": "4242",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
yield instance
|
||
|
|
|
||
|
|
|
||
|
|
def _find_handler(app_instance, method, path):
|
||
|
|
for route in app_instance.get_routes():
|
||
|
|
if route.path == path and route.method == method:
|
||
|
|
return route.handler
|
||
|
|
raise AssertionError(f"Handler not found: {method} {path}")
|
||
|
|
|
||
|
|
|
||
|
|
def _write_disk_config(app_instance, text=DOTTED_CONFIG):
|
||
|
|
path = app_instance._reticulum_config_file_path()
|
||
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||
|
|
with open(path, "w", encoding="utf-8") as handle:
|
||
|
|
handle.write(text)
|
||
|
|
return path
|
||
|
|
|
||
|
|
|
||
|
|
def _oracle_disk_names(text):
|
||
|
|
return {iface["name"] for iface in InterfaceConfigParser.parse(text)}
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_get_interfaces_includes_disk_only_dotted_name(app):
|
||
|
|
_write_disk_config(app)
|
||
|
|
oracle_names = _oracle_disk_names(DOTTED_CONFIG)
|
||
|
|
assert "artyom.ddns.net" in oracle_names
|
||
|
|
assert "artyom.ddns.net" not in app.reticulum.config["interfaces"]
|
||
|
|
|
||
|
|
handler = _find_handler(app, "GET", "/api/v1/reticulum/interfaces")
|
||
|
|
response = await handler(MagicMock())
|
||
|
|
assert response.status == 200
|
||
|
|
body = json.loads(response.body)
|
||
|
|
listed = set(body["interfaces"].keys())
|
||
|
|
assert oracle_names <= listed
|
||
|
|
assert body["interfaces"]["artyom.ddns.net"]["type"] == "TCPClientInterface"
|
||
|
|
assert body["interfaces"]["artyom.ddns.net"]["target_host"] == "10.100.11.12"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_delete_removes_disk_only_dotted_name(app):
|
||
|
|
from RNS.vendor.configobj import ConfigObj
|
||
|
|
|
||
|
|
path = _write_disk_config(app)
|
||
|
|
live = ConfigObj(path)
|
||
|
|
del live["interfaces"]["artyom.ddns.net"]
|
||
|
|
live.filename = path
|
||
|
|
app.reticulum.config = live
|
||
|
|
app.reticulum.configpath = path
|
||
|
|
assert "artyom.ddns.net" not in app.reticulum.config["interfaces"]
|
||
|
|
|
||
|
|
handler = _find_handler(app, "POST", "/api/v1/reticulum/interfaces/delete")
|
||
|
|
|
||
|
|
class Request:
|
||
|
|
@staticmethod
|
||
|
|
async def json():
|
||
|
|
return {"name": "artyom.ddns.net"}
|
||
|
|
|
||
|
|
response = await handler(Request())
|
||
|
|
assert response.status == 200, json.loads(response.body)
|
||
|
|
assert "artyom.ddns.net" not in app.reticulum.config["interfaces"]
|
||
|
|
|
||
|
|
remaining_names = _oracle_disk_names(open(path, encoding="utf-8").read())
|
||
|
|
assert "artyom.ddns.net" not in remaining_names
|
||
|
|
assert "Default Interface" in remaining_names
|
||
|
|
assert "Catz-Node (TCP)" in remaining_names
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_raw_put_replaces_live_interfaces_from_file(app):
|
||
|
|
handler = _find_handler(app, "PUT", "/api/v1/reticulum/config/raw")
|
||
|
|
|
||
|
|
class Request:
|
||
|
|
@staticmethod
|
||
|
|
async def json():
|
||
|
|
return {"content": DOTTED_CONFIG}
|
||
|
|
|
||
|
|
response = await handler(Request())
|
||
|
|
assert response.status == 200, json.loads(response.body)
|
||
|
|
live_names = set(app.reticulum.config["interfaces"].keys())
|
||
|
|
assert live_names == _oracle_disk_names(DOTTED_CONFIG)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_get_skips_non_dict_interface_values(app):
|
||
|
|
app.reticulum.config["interfaces"]["broken"] = "not-a-section"
|
||
|
|
handler = _find_handler(app, "GET", "/api/v1/reticulum/interfaces")
|
||
|
|
response = await handler(MagicMock())
|
||
|
|
assert response.status == 200
|
||
|
|
body = json.loads(response.body)
|
||
|
|
assert "broken" not in body["interfaces"]
|
||
|
|
assert "Default Interface" in body["interfaces"]
|