mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-06-11 00:34:46 +02:00
234 lines
7.8 KiB
Python
234 lines
7.8 KiB
Python
"""Tests for setup_wizard pymc_usb / pymc_tcp branches.
|
|
|
|
These verify that when the first-run /setup wizard is finished with one of
|
|
the two pymc_* hardware tiles selected, api_endpoints.setup_wizard() writes
|
|
a config.yaml that matches what get_radio_for_board() expects (see
|
|
repeater/config.py and tests/test_radio_config.py).
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
import types
|
|
|
|
import cherrypy
|
|
import pytest
|
|
import yaml
|
|
|
|
from repeater.web.api_endpoints import APIEndpoints
|
|
|
|
# Minimal initial config.yaml the wizard writes into.
|
|
_BASE_CONFIG = {
|
|
"repeater": {"node_name": "mesh-repeater-01", "security": {"admin_password": "admin123"}},
|
|
"radio": {},
|
|
}
|
|
|
|
_BASE_REQUEST = {
|
|
"node_name": "pymc-test",
|
|
"admin_password": "supersecret",
|
|
"radio_preset": {
|
|
"frequency": 869.618,
|
|
"spreading_factor": 8,
|
|
"bandwidth": 62.5,
|
|
"coding_rate": 8,
|
|
"tx_power": 22,
|
|
},
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def wizard_env(tmp_path, monkeypatch):
|
|
"""Bootstrap a tempdir with config.yaml + radio-settings.json + mocked cherrypy."""
|
|
config_path = tmp_path / "config.yaml"
|
|
with open(config_path, "w") as f:
|
|
yaml.safe_dump(_BASE_CONFIG, f)
|
|
|
|
radio_settings = {
|
|
"hardware": {
|
|
"pymc_usb": {
|
|
"name": "pymc_usb modem (USB-CDC)",
|
|
"radio_type": "pymc_usb",
|
|
"tx_power": 22,
|
|
"preamble_length": 16,
|
|
},
|
|
"pymc_tcp": {
|
|
"name": "pymc_tcp modem (Wi-Fi / Ethernet)",
|
|
"radio_type": "pymc_tcp",
|
|
"tx_power": 22,
|
|
"preamble_length": 16,
|
|
},
|
|
}
|
|
}
|
|
with open(tmp_path / "radio-settings.json", "w") as f:
|
|
json.dump(radio_settings, f)
|
|
|
|
# resolve_storage_dir() returns the directory of config_path when the
|
|
# config has no explicit storage_dir set — that's exactly what we want
|
|
# so the wizard finds our radio-settings.json next to config.yaml.
|
|
config = {
|
|
"storage_dir": str(tmp_path),
|
|
"repeater": {
|
|
"node_name": "mesh-repeater-01",
|
|
"security": {"admin_password": "admin123"},
|
|
},
|
|
}
|
|
endpoints = APIEndpoints(config=config, config_path=str(config_path))
|
|
|
|
# Stub the post-wizard service restart — we don't want a real systemctl call.
|
|
fake_service_utils = types.ModuleType("repeater.service_utils")
|
|
fake_service_utils.restart_service = lambda: None
|
|
monkeypatch.setitem(sys.modules, "repeater.service_utils", fake_service_utils)
|
|
|
|
def _set_request(body):
|
|
# cherrypy.request is a thread-local — populate the bits the handler reads.
|
|
cherrypy.request.method = "POST"
|
|
cherrypy.request.json = body
|
|
|
|
return tmp_path, config_path, endpoints, _set_request
|
|
|
|
|
|
def _read_yaml(path):
|
|
with open(path) as f:
|
|
return yaml.safe_load(f)
|
|
|
|
|
|
# ─── pymc_usb ─────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_wizard_pymc_usb_defaults(wizard_env):
|
|
tmp_path, config_path, endpoints, set_request = wizard_env
|
|
|
|
body = dict(_BASE_REQUEST, hardware_key="pymc_usb")
|
|
set_request(body)
|
|
|
|
result = endpoints.setup_wizard()
|
|
|
|
assert result["success"] is True
|
|
assert result["config"]["radio_type"] == "pymc_usb"
|
|
assert result["config"]["pymc_usb_port"] == "/dev/ttyACM0"
|
|
assert result["config"]["pymc_usb_baudrate"] == 921600
|
|
|
|
written = _read_yaml(config_path)
|
|
assert written["radio_type"] == "pymc_usb"
|
|
assert written["pymc_usb"]["port"] == "/dev/ttyACM0"
|
|
assert written["pymc_usb"]["baudrate"] == 921600
|
|
assert written["pymc_usb"]["lbt_enabled"] is True
|
|
assert written["pymc_usb"]["lbt_max_attempts"] == 5
|
|
assert written["radio"]["tx_power"] == 22
|
|
assert written["radio"]["preamble_length"] == 16
|
|
# config.py rejects pymc_usb if 'sx1262' / 'ch341' keys leak in — none here.
|
|
assert "sx1262" not in written
|
|
|
|
|
|
def test_wizard_pymc_usb_overrides_from_request(wizard_env):
|
|
tmp_path, config_path, endpoints, set_request = wizard_env
|
|
|
|
body = dict(
|
|
_BASE_REQUEST,
|
|
hardware_key="pymc_usb",
|
|
pymc_usb_port="/dev/ttyUSB0",
|
|
pymc_usb_baudrate=115200,
|
|
)
|
|
set_request(body)
|
|
|
|
result = endpoints.setup_wizard()
|
|
|
|
assert result["success"] is True
|
|
written = _read_yaml(config_path)
|
|
assert written["pymc_usb"]["port"] == "/dev/ttyUSB0"
|
|
assert written["pymc_usb"]["baudrate"] == 115200
|
|
|
|
|
|
# ─── pymc_tcp ─────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_wizard_pymc_tcp_placeholder(wizard_env):
|
|
"""No host in request → wizard writes a sentinel placeholder. config.py
|
|
will then refuse to start with a clear error pointing at pymc_tcp.host."""
|
|
tmp_path, config_path, endpoints, set_request = wizard_env
|
|
|
|
body = dict(_BASE_REQUEST, hardware_key="pymc_tcp")
|
|
set_request(body)
|
|
|
|
result = endpoints.setup_wizard()
|
|
|
|
assert result["success"] is True
|
|
assert result["config"]["radio_type"] == "pymc_tcp"
|
|
assert result["config"]["pymc_tcp_host"] == "REPLACE_WITH_MODEM_HOST"
|
|
assert result["config"]["pymc_tcp_port"] == 5055
|
|
|
|
written = _read_yaml(config_path)
|
|
assert written["radio_type"] == "pymc_tcp"
|
|
assert written["pymc_tcp"]["host"] == "REPLACE_WITH_MODEM_HOST"
|
|
assert written["pymc_tcp"]["port"] == 5055
|
|
assert written["pymc_tcp"]["token"] == ""
|
|
assert written["pymc_tcp"]["connect_timeout"] == 5.0
|
|
assert written["pymc_tcp"]["lbt_enabled"] is True
|
|
# token deliberately stripped from response.
|
|
assert "pymc_tcp_token" not in result["config"]
|
|
|
|
|
|
def test_wizard_pymc_tcp_full_fields(wizard_env):
|
|
tmp_path, config_path, endpoints, set_request = wizard_env
|
|
|
|
body = dict(
|
|
_BASE_REQUEST,
|
|
hardware_key="pymc_tcp",
|
|
pymc_tcp_host="pymc-3e2834.local",
|
|
pymc_tcp_port=6000,
|
|
pymc_tcp_token="hunter2",
|
|
)
|
|
set_request(body)
|
|
|
|
result = endpoints.setup_wizard()
|
|
|
|
assert result["success"] is True
|
|
written = _read_yaml(config_path)
|
|
assert written["pymc_tcp"]["host"] == "pymc-3e2834.local"
|
|
assert written["pymc_tcp"]["port"] == 6000
|
|
assert written["pymc_tcp"]["token"] == "hunter2"
|
|
|
|
|
|
# ─── KISS regression guard ────────────────────────────────────────────
|
|
|
|
|
|
def test_wizard_kiss_branch_unchanged(wizard_env, tmp_path):
|
|
"""Make sure adding the pymc_* branches didn't break the existing KISS path."""
|
|
tmp_path, config_path, endpoints, set_request = wizard_env
|
|
|
|
body = dict(_BASE_REQUEST, hardware_key="kiss")
|
|
set_request(body)
|
|
|
|
result = endpoints.setup_wizard()
|
|
|
|
assert result["success"] is True
|
|
written = _read_yaml(config_path)
|
|
assert written["radio_type"] == "kiss"
|
|
assert written["kiss"]["port"] == "/dev/ttyUSB0"
|
|
assert written["kiss"]["baud_rate"] == 115200
|
|
|
|
|
|
def test_wizard_rejected_after_setup_complete(wizard_env):
|
|
"""setup_wizard should be first-run only once config is already initialized."""
|
|
tmp_path, config_path, endpoints, set_request = wizard_env
|
|
|
|
configured = {
|
|
"repeater": {"node_name": "already-set", "security": {"admin_password": "verysecret"}},
|
|
"radio_type": "pymc_tcp",
|
|
"radio": {
|
|
"frequency": 869618000,
|
|
"spreading_factor": 8,
|
|
"bandwidth": 62500,
|
|
"coding_rate": 8,
|
|
},
|
|
}
|
|
with open(config_path, "w") as f:
|
|
yaml.safe_dump(configured, f)
|
|
|
|
body = dict(_BASE_REQUEST, hardware_key="pymc_tcp", pymc_tcp_host="modem.local")
|
|
set_request(body)
|
|
|
|
result = endpoints.setup_wizard()
|
|
|
|
assert result["success"] is False
|
|
assert "already complete" in result["error"].lower()
|