8faf5c1c79
Import a Z-Wave JS UI (zwavejs2mqtt) network over the MQTT gateway API, mirroring the existing Zigbee pipeline: - New Z-Wave Import modal + sidebar entry (broker, prefix, gateway name) - coordinator/router/end-device typing with mesh tree from node neighbors - import to Pending section or straight to canvas - Pending Devices gains a Z-Wave source filter - shared mqtt_common helpers extracted from the zigbee service ha-relevant: yes
184 lines
5.6 KiB
Python
184 lines
5.6 KiB
Python
"""Unit tests for the shared MQTT helpers in mqtt_common."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import ssl
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from app.services.mqtt_common import (
|
|
_build_tls_context,
|
|
_sanitize_mqtt_error,
|
|
request_response,
|
|
)
|
|
from app.services.mqtt_common import test_connection as _test_connection
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _sanitize_mqtt_error — never leak credentials
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_sanitize_auth_error() -> None:
|
|
msg = _sanitize_mqtt_error(Exception("Not authorized: bad username for user=admin pwd=secret"))
|
|
assert msg == "Authentication failed"
|
|
assert "secret" not in msg
|
|
|
|
|
|
def test_sanitize_refused() -> None:
|
|
assert _sanitize_mqtt_error(Exception("Connection refused")) == "Connection refused by broker"
|
|
|
|
|
|
def test_sanitize_dns() -> None:
|
|
msg = _sanitize_mqtt_error(Exception("nodename nor servname provided: broker.lan"))
|
|
assert msg == "Broker hostname could not be resolved"
|
|
assert "broker.lan" not in msg
|
|
|
|
|
|
def test_sanitize_tls() -> None:
|
|
assert _sanitize_mqtt_error(
|
|
Exception("[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed")
|
|
) == "TLS handshake failed"
|
|
|
|
|
|
def test_sanitize_timeout() -> None:
|
|
assert _sanitize_mqtt_error(Exception("operation timed out")) == "Connection to broker timed out"
|
|
|
|
|
|
def test_sanitize_unknown_falls_back() -> None:
|
|
msg = _sanitize_mqtt_error(Exception("mqtt://admin:hunter2@broker weird"))
|
|
assert msg == "MQTT connection failed"
|
|
assert "hunter2" not in msg
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _build_tls_context
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_tls_secure_verifies() -> None:
|
|
ctx = _build_tls_context(insecure=False)
|
|
assert ctx.check_hostname is True
|
|
assert ctx.verify_mode == ssl.CERT_REQUIRED
|
|
|
|
|
|
def test_tls_insecure_disables_verification() -> None:
|
|
ctx = _build_tls_context(insecure=True)
|
|
assert ctx.check_hostname is False
|
|
assert ctx.verify_mode == ssl.CERT_NONE
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# request_response (mocked aiomqtt)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_SAMPLE = {"success": True, "result": []}
|
|
|
|
|
|
def _fake_client_factory(topic: str, payload: dict):
|
|
class _FakeMessage:
|
|
_yielded = False
|
|
|
|
def __init__(self) -> None:
|
|
self.topic = topic
|
|
self.payload = json.dumps(payload).encode()
|
|
|
|
def __aiter__(self):
|
|
return self
|
|
|
|
async def __anext__(self):
|
|
if self._yielded:
|
|
raise StopAsyncIteration
|
|
self._yielded = True
|
|
return self
|
|
|
|
class _FakeClient:
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *_):
|
|
pass
|
|
|
|
async def subscribe(self, _topic: str) -> None:
|
|
pass
|
|
|
|
async def publish(self, _topic: str, _payload: str) -> None:
|
|
pass
|
|
|
|
@property
|
|
def messages(self):
|
|
return _FakeMessage()
|
|
|
|
return _FakeClient
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_response_success() -> None:
|
|
topic = "zwave/_CLIENTS/ZWAVE_GATEWAY-zwavejs2mqtt/api/getNodes"
|
|
with patch("app.services.mqtt_common.aiomqtt") as mock_aiomqtt:
|
|
mock_aiomqtt.Client.return_value = _fake_client_factory(topic, _SAMPLE)()
|
|
mock_aiomqtt.MqttError = Exception
|
|
out = await request_response(
|
|
"localhost", 1883, "req/topic", topic, {"args": []}
|
|
)
|
|
assert out == _SAMPLE
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_response_connection_error() -> None:
|
|
class _FakeClient:
|
|
async def __aenter__(self):
|
|
raise Exception("Connection refused")
|
|
|
|
async def __aexit__(self, *_):
|
|
pass
|
|
|
|
with patch("app.services.mqtt_common.aiomqtt") as mock_aiomqtt:
|
|
mock_aiomqtt.Client.return_value = _FakeClient()
|
|
mock_aiomqtt.MqttError = Exception
|
|
with pytest.raises(ConnectionError):
|
|
await request_response("bad", 1883, "req", "resp", {})
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_response_passes_tls_context() -> None:
|
|
topic = "resp"
|
|
with patch("app.services.mqtt_common.aiomqtt") as mock_aiomqtt:
|
|
mock_aiomqtt.Client.return_value = _fake_client_factory(topic, _SAMPLE)()
|
|
mock_aiomqtt.MqttError = Exception
|
|
await request_response("h", 8883, "req", topic, {}, tls=True, tls_insecure=True)
|
|
ctx = mock_aiomqtt.Client.call_args.kwargs["tls_context"]
|
|
assert ctx.verify_mode == ssl.CERT_NONE
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_test_connection_success() -> None:
|
|
class _FakeClient:
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *_):
|
|
pass
|
|
|
|
with patch("app.services.mqtt_common.aiomqtt") as mock_aiomqtt:
|
|
mock_aiomqtt.Client.return_value = _FakeClient()
|
|
mock_aiomqtt.MqttError = Exception
|
|
assert await _test_connection("localhost", 1883) is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_test_connection_failure() -> None:
|
|
class _FakeClient:
|
|
async def __aenter__(self):
|
|
raise Exception("refused")
|
|
|
|
async def __aexit__(self, *_):
|
|
pass
|
|
|
|
with patch("app.services.mqtt_common.aiomqtt") as mock_aiomqtt:
|
|
mock_aiomqtt.Client.return_value = _FakeClient()
|
|
mock_aiomqtt.MqttError = Exception
|
|
with pytest.raises(ConnectionError):
|
|
await _test_connection("bad", 1883)
|