|
|
@@ -860,6 +860,117 @@ class TestNozzleRackData:
|
|
|
assert mqtt_client.state.nozzles[1].nozzle_diameter == "0.6"
|
|
|
|
|
|
|
|
|
+class TestRequestTopicFailSafe:
|
|
|
+ """Tests for graceful degradation when broker rejects request topic subscription."""
|
|
|
+
|
|
|
+ @pytest.fixture
|
|
|
+ def mqtt_client(self):
|
|
|
+ from backend.app.services.bambu_mqtt import BambuMQTTClient
|
|
|
+
|
|
|
+ client = BambuMQTTClient(
|
|
|
+ ip_address="192.168.1.100",
|
|
|
+ serial_number="TEST123",
|
|
|
+ access_code="12345678",
|
|
|
+ )
|
|
|
+ return client
|
|
|
+
|
|
|
+ def test_request_topic_supported_by_default(self, mqtt_client):
|
|
|
+ """Request topic subscription is attempted by default."""
|
|
|
+ assert mqtt_client._request_topic_supported is True
|
|
|
+ assert mqtt_client._request_topic_confirmed is False
|
|
|
+
|
|
|
+ def test_on_subscribe_confirms_success(self, mqtt_client):
|
|
|
+ """Successful SUBACK marks request topic as confirmed."""
|
|
|
+ from paho.mqtt.reasoncodes import ReasonCode
|
|
|
+
|
|
|
+ mqtt_client._request_topic_sub_mid = 42
|
|
|
+ rc = ReasonCode(9, identifier=0) # SUBACK packetType=9, QoS 0 = success
|
|
|
+ mqtt_client._on_subscribe(None, None, 42, [rc], None)
|
|
|
+
|
|
|
+ assert mqtt_client._request_topic_confirmed is True
|
|
|
+ assert mqtt_client._request_topic_supported is True
|
|
|
+ assert mqtt_client._request_topic_sub_mid is None
|
|
|
+ assert mqtt_client._request_topic_sub_time == 0.0
|
|
|
+
|
|
|
+ def test_on_subscribe_detects_rejection(self, mqtt_client):
|
|
|
+ """SUBACK with failure code disables request topic."""
|
|
|
+ from paho.mqtt.reasoncodes import ReasonCode
|
|
|
+
|
|
|
+ mqtt_client._request_topic_sub_mid = 42
|
|
|
+ rc = ReasonCode(9, identifier=0x80) # SUBACK packetType=9, 0x80 = failure
|
|
|
+ mqtt_client._on_subscribe(None, None, 42, [rc], None)
|
|
|
+
|
|
|
+ assert mqtt_client._request_topic_supported is False
|
|
|
+ assert mqtt_client._request_topic_confirmed is False
|
|
|
+
|
|
|
+ def test_on_subscribe_ignores_other_mids(self, mqtt_client):
|
|
|
+ """SUBACK for other subscriptions (e.g. report topic) is ignored."""
|
|
|
+ from paho.mqtt.reasoncodes import ReasonCode
|
|
|
+
|
|
|
+ mqtt_client._request_topic_sub_mid = 42
|
|
|
+ rc = ReasonCode(9, identifier=0x80)
|
|
|
+ mqtt_client._on_subscribe(None, None, 99, [rc], None)
|
|
|
+
|
|
|
+ # Not affected — mid doesn't match
|
|
|
+ assert mqtt_client._request_topic_supported is True
|
|
|
+
|
|
|
+ def test_disconnect_after_subscription_disables_topic(self, mqtt_client):
|
|
|
+ """Disconnect within 10s of subscription attempt disables request topic."""
|
|
|
+ import time
|
|
|
+
|
|
|
+ mqtt_client._request_topic_sub_time = time.time()
|
|
|
+ mqtt_client._request_topic_confirmed = False
|
|
|
+ mqtt_client._last_message_time = 0.0
|
|
|
+
|
|
|
+ mqtt_client._on_disconnect(None, None)
|
|
|
+
|
|
|
+ assert mqtt_client._request_topic_supported is False
|
|
|
+ assert mqtt_client._request_topic_sub_time == 0.0
|
|
|
+
|
|
|
+ def test_disconnect_after_confirmation_does_not_disable(self, mqtt_client):
|
|
|
+ """Disconnect after SUBACK confirmation keeps request topic enabled."""
|
|
|
+ import time
|
|
|
+
|
|
|
+ mqtt_client._request_topic_sub_time = time.time()
|
|
|
+ mqtt_client._request_topic_confirmed = True
|
|
|
+ mqtt_client._last_message_time = 0.0
|
|
|
+
|
|
|
+ mqtt_client._on_disconnect(None, None)
|
|
|
+
|
|
|
+ assert mqtt_client._request_topic_supported is True
|
|
|
+
|
|
|
+ def test_late_disconnect_does_not_disable(self, mqtt_client):
|
|
|
+ """Disconnect long after subscription (>10s) doesn't blame request topic."""
|
|
|
+ import time
|
|
|
+
|
|
|
+ mqtt_client._request_topic_sub_time = time.time() - 30.0
|
|
|
+ mqtt_client._request_topic_confirmed = False
|
|
|
+ mqtt_client._last_message_time = 0.0
|
|
|
+
|
|
|
+ mqtt_client._on_disconnect(None, None)
|
|
|
+
|
|
|
+ assert mqtt_client._request_topic_supported is True
|
|
|
+
|
|
|
+ def test_on_connect_skips_request_topic_when_unsupported(self, mqtt_client):
|
|
|
+ """After marking unsupported, reconnect skips request topic subscription."""
|
|
|
+ mqtt_client._request_topic_supported = False
|
|
|
+
|
|
|
+ subscribe_calls = []
|
|
|
+ mock_client = type(
|
|
|
+ "MockClient",
|
|
|
+ (),
|
|
|
+ {
|
|
|
+ "subscribe": lambda self, topic: subscribe_calls.append(topic) or (0, 1),
|
|
|
+ },
|
|
|
+ )()
|
|
|
+
|
|
|
+ mqtt_client._on_connect(mock_client, None, None, 0)
|
|
|
+
|
|
|
+ # Only report topic subscribed, not request topic
|
|
|
+ assert len(subscribe_calls) == 1
|
|
|
+ assert subscribe_calls[0] == mqtt_client.topic_subscribe
|
|
|
+
|
|
|
+
|
|
|
class TestRequestTopicAmsMapping:
|
|
|
"""Tests for capturing ams_mapping from the MQTT request topic."""
|
|
|
|