mirror of
https://github.com/pdxlocations/contact.git
synced 2026-10-31 00:01:57 +01:00
164 lines
7.5 KiB
Python
164 lines
7.5 KiB
Python
from argparse import Namespace
|
|
from contextlib import ExitStack
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
from contact.ui import control_ui
|
|
from contact.utilities.shared_state import app_state, interface_state
|
|
|
|
from tests.test_support import reset_shared_state
|
|
|
|
|
|
class ControlUiTests(unittest.TestCase):
|
|
def test_channel_list_renders_names_without_changing_slot_keys(self):
|
|
from meshtastic.protobuf import channel_pb2, config_pb2, module_config_pb2
|
|
from contact.ui.menus import generate_menu_from_protobuf
|
|
from contact.utilities.shared_state import app_state, menu_state
|
|
|
|
channels = [channel_pb2.Channel(settings=channel_pb2.ChannelSettings(name=name))
|
|
for name in ("Local", "Local", "", " ")]
|
|
node = SimpleNamespace(
|
|
localConfig=config_pb2.Config(), moduleConfig=module_config_pb2.ModuleConfig(),
|
|
getChannelByChannelIndex=lambda i: channels[i] if i < len(channels) else None,
|
|
)
|
|
menu_state.menu_path = ["Main Menu", "Channels"]
|
|
menu_state.current_menu = generate_menu_from_protobuf(None, node=node)["Main Menu"]["Channels"]
|
|
win, pad = mock.Mock(), mock.Mock()
|
|
win.getbegyx.return_value = (2, 2)
|
|
win.getmaxyx.return_value = (10, 80)
|
|
with ExitStack() as stack:
|
|
for name, value in (("LINES", 24), ("COLS", 100)):
|
|
stack.enter_context(mock.patch.object(control_ui.curses, name, value, create=True))
|
|
stack.enter_context(mock.patch.object(control_ui.curses, "newwin", return_value=win))
|
|
stack.enter_context(mock.patch.object(control_ui.curses, "newpad", return_value=pad))
|
|
stack.enter_context(mock.patch.object(control_ui.curses, "curs_set"))
|
|
stack.enter_context(mock.patch.object(control_ui, "get_menu_width", return_value=80))
|
|
for name in ("get_color", "draw_help_window", "draw_arrows"):
|
|
stack.enter_context(mock.patch.object(control_ui, name, return_value=0))
|
|
control_ui.display_menu()
|
|
labels = [call.args[2].strip() for call in pad.addstr.call_args_list]
|
|
self.assertEqual(labels, ["Local", "Local", "Channel 3", "Channel 4"])
|
|
self.assertEqual(list(menu_state.current_menu), [f"Channel {i}" for i in range(1, 5)])
|
|
|
|
def setUp(self) -> None:
|
|
reset_shared_state()
|
|
|
|
def tearDown(self) -> None:
|
|
reset_shared_state()
|
|
|
|
def test_reconnect_interface_with_splash_replaces_interface(self) -> None:
|
|
old_interface = mock.Mock()
|
|
new_interface = mock.Mock()
|
|
stdscr = mock.Mock()
|
|
app_state.connection_args = Namespace(host="mesh.local", port=None, ble=None)
|
|
|
|
with mock.patch.object(control_ui, "reconnect_interface", return_value=new_interface) as reconnect:
|
|
with mock.patch.object(control_ui, "redraw_main_ui_after_reconnect") as redraw:
|
|
result = control_ui.reconnect_interface_with_splash(stdscr, old_interface)
|
|
|
|
old_interface.close.assert_called_once_with()
|
|
reconnect.assert_called_once_with(app_state.connection_args)
|
|
redraw.assert_called_once_with(stdscr)
|
|
self.assertIs(result, new_interface)
|
|
self.assertIs(interface_state.interface, new_interface)
|
|
|
|
def test_reconnect_after_admin_action_runs_action_then_reconnects(self) -> None:
|
|
stdscr = mock.Mock()
|
|
interface = mock.Mock()
|
|
new_interface = mock.Mock()
|
|
action = mock.Mock()
|
|
|
|
with mock.patch.object(control_ui, "reconnect_interface_with_splash", return_value=new_interface) as reconnect:
|
|
result = control_ui.reconnect_after_admin_action(
|
|
stdscr, interface, action, "Factory Reset Requested by menu"
|
|
)
|
|
|
|
action.assert_called_once_with()
|
|
reconnect.assert_called_once_with(stdscr, interface)
|
|
self.assertIs(result, new_interface)
|
|
|
|
def test_verify_remote_admin_establishes_an_authorized_session(self) -> None:
|
|
node = SimpleNamespace(
|
|
nodeNum=123,
|
|
ensureSessionKey=mock.Mock(),
|
|
iface=SimpleNamespace(_getOrCreateByNum=mock.Mock(return_value={"adminSessionPassKey": b"key"})),
|
|
)
|
|
status_callback = mock.Mock()
|
|
|
|
with mock.patch.object(control_ui, "_request_remote_with_timeout") as request:
|
|
control_ui.verify_remote_admin(node, status_callback=status_callback)
|
|
|
|
status_callback.assert_called_once_with("Checking remote-admin permission…")
|
|
request.assert_called_once_with(node.ensureSessionKey, cancel_callback=None)
|
|
node.iface._getOrCreateByNum.assert_called_once_with(123)
|
|
|
|
def test_verify_remote_admin_rejects_missing_session_key(self) -> None:
|
|
node = SimpleNamespace(
|
|
nodeNum=123,
|
|
ensureSessionKey=mock.Mock(),
|
|
iface=SimpleNamespace(_getOrCreateByNum=mock.Mock(return_value={})),
|
|
)
|
|
|
|
with mock.patch.object(control_ui, "_request_remote_with_timeout"):
|
|
with self.assertRaises(PermissionError):
|
|
control_ui.verify_remote_admin(node)
|
|
|
|
def test_redraw_main_ui_after_reconnect_refreshes_channels_nodes_and_layout(self) -> None:
|
|
stdscr = mock.Mock()
|
|
|
|
with mock.patch("contact.utilities.utils.get_channels") as get_channels:
|
|
with mock.patch("contact.utilities.utils.refresh_node_list") as refresh_node_list:
|
|
with mock.patch("contact.ui.contact_ui.handle_resize") as handle_resize:
|
|
control_ui.redraw_main_ui_after_reconnect(stdscr)
|
|
|
|
get_channels.assert_called_once_with()
|
|
refresh_node_list.assert_called_once_with()
|
|
handle_resize.assert_called_once_with(stdscr, False)
|
|
|
|
def test_request_factory_reset_uses_library_helper_when_supported(self) -> None:
|
|
node = mock.Mock()
|
|
|
|
control_ui.request_factory_reset(node)
|
|
|
|
node.factoryReset.assert_called_once_with(full=False)
|
|
node.ensureSessionKey.assert_not_called()
|
|
node._sendAdmin.assert_not_called()
|
|
|
|
def test_request_factory_reset_uses_library_helper_for_full_reset_when_supported(self) -> None:
|
|
node = mock.Mock()
|
|
|
|
control_ui.request_factory_reset(node, full=True)
|
|
|
|
node.factoryReset.assert_called_once_with(full=True)
|
|
node.ensureSessionKey.assert_not_called()
|
|
node._sendAdmin.assert_not_called()
|
|
|
|
def test_request_factory_reset_falls_back_to_int_valued_admin_message(self) -> None:
|
|
node = mock.Mock()
|
|
node.factoryReset.side_effect = TypeError(
|
|
"Field meshtastic.protobuf.AdminMessage.factory_reset_config: Expected an int, got a boolean."
|
|
)
|
|
node.iface = SimpleNamespace(localNode=node)
|
|
|
|
control_ui.request_factory_reset(node)
|
|
|
|
node.ensureSessionKey.assert_called_once_with()
|
|
sent_message = node._sendAdmin.call_args.args[0]
|
|
self.assertEqual(sent_message.factory_reset_config, 1)
|
|
self.assertIsNone(node._sendAdmin.call_args.kwargs["onResponse"])
|
|
|
|
def test_request_factory_reset_full_falls_back_to_int_valued_admin_message(self) -> None:
|
|
node = mock.Mock()
|
|
node.factoryReset.side_effect = TypeError(
|
|
"Field meshtastic.protobuf.AdminMessage.factory_reset_device: Expected an int, got a boolean."
|
|
)
|
|
node.iface = SimpleNamespace(localNode=node)
|
|
|
|
control_ui.request_factory_reset(node, full=True)
|
|
|
|
node.ensureSessionKey.assert_called_once_with()
|
|
sent_message = node._sendAdmin.call_args.args[0]
|
|
self.assertEqual(sent_message.factory_reset_device, 1)
|
|
self.assertIsNone(node._sendAdmin.call_args.kwargs["onResponse"])
|