diff --git a/tests/backend/test_user_guidance_autointerface.py b/tests/backend/test_user_guidance_autointerface.py new file mode 100644 index 0000000..cedcb00 --- /dev/null +++ b/tests/backend/test_user_guidance_autointerface.py @@ -0,0 +1,158 @@ +# SPDX-License-Identifier: 0BSD + +"""Tests for AutoInterface bind-failure detection in user guidance. +""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +from meshchatx.meshchat import ReticulumMeshChat + + +def _make_app(config_interfaces): + """Build a minimal app stub exposing only what the helpers touch.""" + app = SimpleNamespace() + app.reticulum = SimpleNamespace( + config={"interfaces": dict(config_interfaces)}, + transport_enabled=lambda: True, + ) + app._get_interfaces_section = ( + ReticulumMeshChat._get_interfaces_section.__get__(app) + ) + app._detect_failed_autointerfaces = ( + ReticulumMeshChat._detect_failed_autointerfaces.__get__(app) + ) + return app + + +class _FakeAutoInterface: + pass + + +_FakeAutoInterface.__name__ = "AutoInterface" + + +class _FakeTCPClientInterface: + pass + + +_FakeTCPClientInterface.__name__ = "TCPClientInterface" + + +def test_detects_enabled_autointerface_that_failed_to_start(): + app = _make_app( + { + "Default Interface": {"type": "AutoInterface", "enabled": "yes"}, + }, + ) + with patch("meshchatx.meshchat.RNS.Transport") as transport: + transport.interfaces = [] + assert app._detect_failed_autointerfaces() == ["Default Interface"] + + +def test_running_autointerface_is_not_reported(): + app = _make_app( + { + "Default Interface": {"type": "AutoInterface", "enabled": "yes"}, + }, + ) + with patch("meshchatx.meshchat.RNS.Transport") as transport: + transport.interfaces = [_FakeAutoInterface()] + assert app._detect_failed_autointerfaces() == [] + + +def test_disabled_autointerface_is_ignored(): + app = _make_app( + { + "Default Interface": {"type": "AutoInterface", "enabled": "no"}, + }, + ) + with patch("meshchatx.meshchat.RNS.Transport") as transport: + transport.interfaces = [] + assert app._detect_failed_autointerfaces() == [] + + +def test_non_auto_interface_does_not_trigger_warning(): + app = _make_app( + { + "Hub": { + "type": "TCPClientInterface", + "enabled": "yes", + "target_host": "127.0.0.1", + "target_port": "4242", + }, + }, + ) + with patch("meshchatx.meshchat.RNS.Transport") as transport: + transport.interfaces = [_FakeTCPClientInterface()] + assert app._detect_failed_autointerfaces() == [] + + +def test_empty_interfaces_section_is_safe(): + app = _make_app({}) + with patch("meshchatx.meshchat.RNS.Transport") as transport: + transport.interfaces = [] + assert app._detect_failed_autointerfaces() == [] + + +def test_guidance_message_emitted_for_failed_autointerface(): + app = _make_app( + { + "Default Interface": {"type": "AutoInterface", "enabled": "yes"}, + }, + ) + app.config = MagicMock() + app.config.auto_announce_enabled.get.return_value = True + app.build_user_guidance_messages = ( + ReticulumMeshChat.build_user_guidance_messages.__get__(app) + ) + + with patch("meshchatx.meshchat.RNS.Transport") as transport: + transport.interfaces = [] + guidance = app.build_user_guidance_messages() + + autointerface_msgs = [g for g in guidance if g["id"] == "autointerface_bind_failed"] + assert len(autointerface_msgs) == 1 + msg = autointerface_msgs[0] + assert msg["severity"] == "warning" + assert msg["action_route"] == "/interfaces" + assert "Default Interface" in msg["description"] + assert "Sideband" in msg["description"] + + +def test_guidance_message_absent_when_autointerface_running(): + app = _make_app( + { + "Default Interface": {"type": "AutoInterface", "enabled": "yes"}, + }, + ) + app.config = MagicMock() + app.config.auto_announce_enabled.get.return_value = True + app.build_user_guidance_messages = ( + ReticulumMeshChat.build_user_guidance_messages.__get__(app) + ) + + with patch("meshchatx.meshchat.RNS.Transport") as transport: + transport.interfaces = [_FakeAutoInterface()] + guidance = app.build_user_guidance_messages() + + assert not any(g["id"] == "autointerface_bind_failed" for g in guidance) + + +@pytest.mark.parametrize("enabled_value", ["yes", "true", "1", "Yes", "TRUE"]) +def test_truthy_enabled_values_are_recognised(enabled_value): + app = _make_app( + { + "Default Interface": { + "type": "AutoInterface", + "enabled": enabled_value, + }, + }, + ) + with patch("meshchatx.meshchat.RNS.Transport") as transport: + transport.interfaces = [] + assert app._detect_failed_autointerfaces() == ["Default Interface"]