diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index 9649e0b9ddf..39b9f122f75 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -1,12 +1,20 @@ """Test fixtures for mqtt component.""" +import asyncio from random import getrandbits +from typing import Any from unittest.mock import patch import pytest -from typing_extensions import Generator +from typing_extensions import AsyncGenerator, Generator from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant, callback + +from tests.common import MockConfigEntry +from tests.typing import MqttMockPahoClient ENTRY_DEFAULT_BIRTH_MESSAGE = { mqtt.CONF_BROKER: "mock-broker", @@ -39,3 +47,35 @@ def mock_temp_dir(temp_dir_prefix: str) -> Generator[str]: f"home-assistant-mqtt-{temp_dir_prefix}-{getrandbits(10):03x}", ) as mocked_temp_dir: yield mocked_temp_dir + + +@pytest.fixture +async def setup_with_birth_msg_client_mock( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any] | None, + mqtt_client_mock: MqttMockPahoClient, +) -> AsyncGenerator[MqttMockPahoClient]: + """Test sending birth message.""" + birth = asyncio.Event() + with ( + patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0), + patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0), + patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0), + ): + entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} + ) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + + @callback + def wait_birth(msg: ReceiveMessage) -> None: + """Handle birth message.""" + birth.set() + + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + await hass.async_block_till_done() + await birth.wait() + yield mqtt_client_mock diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index e36971e386f..b9ef1a3c210 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1531,7 +1531,7 @@ async def test_mqtt_discovery_unsubscribe_once( async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: """Test mqtt step.""" - await asyncio.sleep(0.1) + await asyncio.sleep(0) return self.async_abort(reason="already_configured") mock_platform(hass, "comp.config_flow", None) @@ -1573,7 +1573,7 @@ async def test_mqtt_discovery_unsubscribe_once( async_fire_mqtt_message(hass, "comp/discovery/bla/config", "") async_fire_mqtt_message(hass, "comp/discovery/bla/config", "") await wait_unsub.wait() - await asyncio.sleep(0.2) + await asyncio.sleep(0) await hass.async_block_till_done(wait_background_tasks=True) mqtt_client_mock.unsubscribe.assert_called_once_with(["comp/discovery/#"]) await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 264f80f48f8..8a76c71f1f3 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -56,6 +56,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.dt import utcnow +from .conftest import ENTRY_DEFAULT_BIRTH_MESSAGE from .test_common import help_all_subscribe_calls from tests.common import ( @@ -149,21 +150,19 @@ def help_assert_message( async def test_mqtt_connects_on_home_assistant_mqtt_setup( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test if client is connected after mqtt init on bootstrap.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock assert mqtt_client_mock.connect.call_count == 1 async def test_mqtt_does_not_disconnect_on_home_assistant_stop( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test if client is not disconnected on HA stop.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock hass.bus.fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_block_till_done() @@ -226,12 +225,13 @@ async def test_mqtt_await_ack_at_disconnect( await hass.async_block_till_done(wait_background_tasks=True) +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) async def test_publish( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test the publish function.""" - mqtt_mock = await mqtt_mock_entry() - publish_mock: MagicMock = mqtt_mock._mqttc.publish + publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish await mqtt.async_publish(hass, "test-topic", "test-payload") await hass.async_block_till_done() assert publish_mock.called @@ -292,7 +292,7 @@ async def test_publish( 0, False, ) - mqtt_mock.reset_mock() + publish_mock.reset_mock() # test null payload mqtt.publish( @@ -1100,42 +1100,40 @@ async def test_subscribe_mqtt_config_entry_disabled( await mqtt.async_subscribe(hass, "test-topic", record_calls) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.2) async def test_subscribe_and_resubscribe( hass: HomeAssistant, client_debug_log: None, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, ) -> None: """Test resubscribing within the debounce time.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock + with ( + patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.4), + patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.4), + ): + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + # This unsub will be un-done with the following subscribe + # unsubscribe should not be called at the broker + unsub() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - # This unsub will be un-done with the following subscribe - # unsubscribe should not be called at the broker - unsub() - await asyncio.sleep(0.1) - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - await asyncio.sleep(0.1) - await hass.async_block_till_done() + async_fire_mqtt_message(hass, "test-topic", "test-payload") + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) - async_fire_mqtt_message(hass, "test-topic", "test-payload") - await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + # assert unsubscribe was not called + mqtt_client_mock.unsubscribe.assert_not_called() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - # assert unsubscribe was not called - mqtt_client_mock.unsubscribe.assert_not_called() + unsub() - unsub() - - await asyncio.sleep(0.2) - await hass.async_block_till_done() - mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) async def test_subscribe_topic_non_async( @@ -1442,25 +1440,26 @@ async def test_subscribe_special_characters( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_subscribe_same_topic( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test subscribing to same topic twice and simulate retained messages. When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again for it to resend any retained messages. """ - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + @callback def _callback_b(msg: ReceiveMessage) -> None: calls_b.append(msg) + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/state", _callback_a, qos=0) # Simulate a non retained message after the first subscription async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) @@ -1486,13 +1485,9 @@ async def test_subscribe_same_topic( mqtt_client_mock.subscribe.assert_called() -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_replaying_payload_same_topic( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying retained messages. @@ -1501,22 +1496,26 @@ async def test_replaying_payload_same_topic( Retained messages must only be replayed for new subscriptions, except when the MQTT client is reconnecting. """ - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + @callback def _callback_b(msg: ReceiveMessage) -> None: calls_b.append(msg) + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/state", _callback_a) + await hass.async_block_till_done() async_fire_mqtt_message( hass, "test/state", "online", qos=0, retain=True ) # Simulate a (retained) message played back - await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await asyncio.sleep(0) await hass.async_block_till_done() assert len(calls_a) == 1 @@ -1538,6 +1537,7 @@ async def test_replaying_payload_same_topic( # Make sure the debouncer delay was passed await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await asyncio.sleep(0) await hass.async_block_till_done() # The current subscription only received the message without retain flag @@ -1562,6 +1562,7 @@ async def test_replaying_payload_same_topic( async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await asyncio.sleep(0) await hass.async_block_till_done() assert len(calls_a) == 1 assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) @@ -1576,13 +1577,15 @@ async def test_replaying_payload_same_topic( mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await asyncio.sleep(0) await hass.async_block_till_done() mqtt_client_mock.subscribe.assert_called() # Simulate a (retained) message played back after reconnecting async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) # Both subscriptions now should replay the retained message assert len(calls_a) == 1 assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) @@ -1595,8 +1598,7 @@ async def test_replaying_payload_same_topic( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_replaying_payload_after_resubscribing( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying and filtering retained messages after resubscribing. @@ -1605,13 +1607,14 @@ async def test_replaying_payload_after_resubscribing( Retained messages must only be replayed for new subscriptions, except when the MQTT client is reconnection. """ - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + mqtt_client_mock.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) @@ -1655,8 +1658,7 @@ async def test_replaying_payload_after_resubscribing( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_replaying_payload_wildcard_topic( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying retained messages. @@ -1666,23 +1668,26 @@ async def test_replaying_payload_wildcard_topic( Retained messages should only be replayed for new subscriptions, except when the MQTT client is reconnection. """ - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + @callback def _callback_b(msg: ReceiveMessage) -> None: calls_b.append(msg) + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/#", _callback_a) # Simulate (retained) messages being played back on new subscriptions async_fire_mqtt_message(hass, "test/state1", "new_value_1", qos=0, retain=True) async_fire_mqtt_message(hass, "test/state2", "new_value_2", qos=0, retain=True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await asyncio.sleep(0) await hass.async_block_till_done() assert len(calls_a) == 2 mqtt_client_mock.subscribe.assert_called() @@ -1696,6 +1701,7 @@ async def test_replaying_payload_wildcard_topic( async_fire_mqtt_message(hass, "test/state2", "initial_value_2", qos=0, retain=True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await asyncio.sleep(0) await hass.async_block_till_done() # The retained messages playback should only be processed for the new subscriptions assert len(calls_a) == 0 @@ -1710,6 +1716,7 @@ async def test_replaying_payload_wildcard_topic( async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=False) async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=False) await hass.async_block_till_done() + await asyncio.sleep(0) assert len(calls_a) == 2 assert len(calls_b) == 2 @@ -1721,6 +1728,7 @@ async def test_replaying_payload_wildcard_topic( mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await asyncio.sleep(0) await hass.async_block_till_done() mqtt_client_mock.subscribe.assert_called() # Simulate the (retained) messages are played back after reconnecting @@ -1729,40 +1737,38 @@ async def test_replaying_payload_wildcard_topic( async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) # Both subscriptions should replay assert len(calls_a) == 2 assert len(calls_b) == 2 -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_not_calling_unsubscribe_with_active_subscribers( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) await mqtt.async_subscribe(hass, "test/state", record_calls, 1) await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() + await asyncio.sleep(0) await hass.async_block_till_done() assert mqtt_client_mock.subscribe.called unsub() await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await hass.async_block_till_done(wait_background_tasks=True) assert not mqtt_client_mock.unsubscribe.called async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test not calling subscribe() when it is unsubscribed. @@ -1770,7 +1776,7 @@ async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( Make sure subscriptions are cleared if unsubscribed before the subscribe cool down period has ended. """ - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.subscribe.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) @@ -1780,26 +1786,20 @@ async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( assert not mqtt_client_mock.subscribe.called -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_unsubscribe_race( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" - await mqtt_mock_entry() - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + @callback def _callback_b(msg: ReceiveMessage) -> None: calls_b.append(msg) @@ -1807,10 +1807,11 @@ async def test_unsubscribe_race( unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) unsub() await mqtt.async_subscribe(hass, "test/state", _callback_b) - await hass.async_block_till_done() + await asyncio.sleep(0) await hass.async_block_till_done() async_fire_mqtt_message(hass, "test/state", "online") + await asyncio.sleep(0) await hass.async_block_till_done() assert not calls_a assert calls_b @@ -1840,54 +1841,44 @@ async def test_unsubscribe_race( "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) async def test_restore_subscriptions_on_reconnect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test subscriptions are restored on reconnect.""" - await mqtt_mock_entry() - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() - mqtt_client_mock.subscribe.reset_mock() + mqtt_client_mock = setup_with_birth_msg_client_mock + + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/state", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await asyncio.sleep(0) await hass.async_block_till_done() - assert mqtt_client_mock.subscribe.call_count == 1 + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + mqtt_client_mock.reset_mock() mqtt_client_mock.on_disconnect(None, None, 0) mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() - assert mqtt_client_mock.subscribe.call_count == 2 + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) @pytest.mark.parametrize( "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 1.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 1.0) async def test_restore_all_active_subscriptions_on_reconnect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, freezer: FrozenDateTimeFactory, ) -> None: """Test active subscriptions are restored correctly on reconnect.""" - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) @@ -1914,14 +1905,15 @@ async def test_restore_all_active_subscriptions_on_reconnect( await hass.async_block_till_done() expected.append(call([("test/state", 1)])) - assert mqtt_client_mock.subscribe.mock_calls == expected + for expected_call in expected: + assert mqtt_client_mock.subscribe.hass_call(expected_call) freezer.tick(3) async_fire_time_changed(hass) # cooldown await hass.async_block_till_done() freezer.tick(3) async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) @pytest.mark.parametrize( @@ -1933,14 +1925,13 @@ async def test_restore_all_active_subscriptions_on_reconnect( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 1.0) async def test_subscribed_at_highest_qos( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, freezer: FrozenDateTimeFactory, ) -> None: """Test the highest qos as assigned when subscribing to the same topic.""" - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) await hass.async_block_till_done() freezer.tick(5) @@ -1974,7 +1965,6 @@ async def test_reload_entry_with_restored_subscriptions( entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) - mqtt_client_mock.connect.return_value = 0 with patch("homeassistant.config.load_yaml_config_file", return_value={}): await hass.config_entries.async_setup(entry.entry_id) @@ -2026,49 +2016,42 @@ async def test_reload_entry_with_restored_subscriptions( assert recorded_calls[1].payload == "wild-card-payload3" -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 2) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 2) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 2) async def test_canceling_debouncer_on_shutdown( hass: HomeAssistant, record_calls: MessageCallbackType, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test canceling the debouncer when HA shuts down.""" + mqtt_client_mock = setup_with_birth_msg_client_mock - await mqtt_mock_entry() - mqtt_client_mock.subscribe.reset_mock() + with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 2): + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + await mqtt.async_subscribe(hass, "test/state1", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + # Stop HA so the scheduled debouncer task will be canceled + mqtt_client_mock.subscribe.reset_mock() + hass.bus.fire(EVENT_HOMEASSISTANT_STOP) + await mqtt.async_subscribe(hass, "test/state2", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state3", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state4", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state5", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await hass.async_block_till_done() - await mqtt.async_subscribe(hass, "test/state1", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) - await hass.async_block_till_done() + mqtt_client_mock.subscribe.assert_not_called() - await mqtt.async_subscribe(hass, "test/state2", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) - await hass.async_block_till_done() - - await mqtt.async_subscribe(hass, "test/state3", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) - await hass.async_block_till_done() - - await mqtt.async_subscribe(hass, "test/state4", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) - await hass.async_block_till_done() - - await mqtt.async_subscribe(hass, "test/state5", record_calls) - - mqtt_client_mock.subscribe.assert_not_called() - - # Stop HA so the scheduled task will be canceled - hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - # mock disconnect status - mqtt_client_mock.on_disconnect(None, None, 0) - await hass.async_block_till_done() - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() - mqtt_client_mock.subscribe.assert_not_called() + # Note thet the broker connection will not be disconnected gracefully + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.subscribe.assert_not_called() + mqtt_client_mock.disconnect.assert_not_called() async def test_canceling_debouncer_normal( @@ -2130,13 +2113,13 @@ async def test_initial_setup_logs_error( async def test_logs_error_if_no_connect_broker( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test for setup failure if connection to broker is missing.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock # test with rc = 3 -> broker unavailable - mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 3) + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, 3) await hass.async_block_till_done() assert ( "Unable to connect to the MQTT broker: Connection Refused: broker unavailable." @@ -2148,14 +2131,14 @@ async def test_logs_error_if_no_connect_broker( async def test_triggers_reauth_flow_if_auth_fails( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, return_code: int, ) -> None: """Test re-auth is triggered if authentication is failing.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock # test with rc = 4 -> CONNACK_REFUSED_NOT_AUTHORIZED and 5 -> CONNACK_REFUSED_BAD_USERNAME_PASSWORD - mqtt_client_mock.on_connect(mqtt_client_mock, None, None, return_code) + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, return_code) await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2166,11 +2149,10 @@ async def test_triggers_reauth_flow_if_auth_fails( async def test_handle_mqtt_on_callback( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test receiving an ACK callback before waiting for it.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock with patch.object(mqtt_client_mock, "get_mid", return_value=100): # Simulate an ACK for mid == 100, this will call mqtt_mock._async_get_mid_future(mid) mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) @@ -2222,51 +2204,47 @@ async def test_publish_error( assert "Failed to connect to MQTT server: Out of memory." in caplog.text -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) async def test_subscribe_error( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, caplog: pytest.LogCaptureFixture, ) -> None: """Test publish error.""" - await mqtt_mock_entry() - mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0) - await hass.async_block_till_done() - await hass.async_block_till_done() + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.reset_mock() # simulate client is not connected error before subscribing mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) - await mqtt.async_subscribe(hass, "some-topic", record_calls) - while mqtt_client_mock.subscribe.call_count == 0: + with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0): + await mqtt.async_subscribe(hass, "some-topic", record_calls) + while mqtt_client_mock.subscribe.call_count == 0: + await hass.async_block_till_done() await hass.async_block_till_done() - await hass.async_block_till_done() - await hass.async_block_till_done() - assert ( - "Error talking to MQTT: The client is not currently connected." in caplog.text - ) + await hass.async_block_till_done() + assert ( + "Error talking to MQTT: The client is not currently connected." + in caplog.text + ) async def test_handle_message_callback( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test for handling an incoming message callback.""" + mqtt_client_mock = setup_with_birth_msg_client_mock callbacks = [] @callback def _callback(args) -> None: callbacks.append(args) - mock_mqtt = await mqtt_mock_entry() msg = ReceiveMessage( "some-topic", b"test-payload", 1, False, "some-topic", datetime.now() ) - mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0) await mqtt.async_subscribe(hass, "some-topic", _callback) - mqtt_client_mock.on_message(mock_mqtt, None, msg) + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_message(None, None, msg) await hass.async_block_till_done() await hass.async_block_till_done() @@ -2395,8 +2373,6 @@ async def test_handle_mqtt_timeout_on_callback( ) entry.add_to_hass(hass) - # Make sure we are connected correctly - mock_client.on_connect(mock_client, None, None, 0) # Set up the integration assert await hass.config_entries.async_setup(entry.entry_id) @@ -2506,62 +2482,48 @@ async def test_tls_version( } ], ) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_custom_birth_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test sending birth message.""" - await mqtt_mock_entry() - birth = asyncio.Event() - async def wait_birth(msg: ReceiveMessage) -> None: + birth = asyncio.Event() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + + @callback + def wait_birth(msg: ReceiveMessage) -> None: """Handle birth message.""" birth.set() - with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): - await mqtt.async_subscribe(hass, "birth", wait_birth) - mqtt_client_mock.on_connect(None, None, 0, 0) - await hass.async_block_till_done() - await birth.wait() - mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) + await mqtt.async_subscribe(hass, "birth", wait_birth) + await hass.async_block_till_done() + await birth.wait() + mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) @pytest.mark.parametrize( "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "homeassistant/status", - mqtt.ATTR_PAYLOAD: "online", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], + [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_default_birth_message( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test sending birth message.""" - await mqtt_mock_entry() - birth = asyncio.Event() - - async def wait_birth(msg: ReceiveMessage) -> None: - """Handle birth message.""" - birth.set() - - with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): - await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - mqtt_client_mock.on_connect(None, None, 0, 0) - await hass.async_block_till_done() - await birth.wait() - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) + mqtt_client_mock = setup_with_birth_msg_client_mock + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) @pytest.mark.parametrize( @@ -2573,28 +2535,30 @@ async def test_default_birth_message( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_no_birth_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test disabling birth message.""" - await mqtt_mock_entry() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) await hass.async_block_till_done() mqtt_client_mock.reset_mock() - # Assert no birth message was sent - mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) await hass.async_block_till_done() mqtt_client_mock.publish.assert_not_called() - async def callback(msg: ReceiveMessage) -> None: - """Handle birth message.""" + @callback + def msg_callback(msg: ReceiveMessage) -> None: + """Handle callback.""" mqtt_client_mock.reset_mock() - await mqtt.async_subscribe(hass, "homeassistant/some-topic", callback) + await mqtt.async_subscribe(hass, "homeassistant/some-topic", msg_callback) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) await hass.async_block_till_done() @@ -2603,130 +2567,61 @@ async def test_no_birth_message( @pytest.mark.parametrize( "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "homeassistant/status", - mqtt.ATTR_PAYLOAD: "online", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], + [ENTRY_DEFAULT_BIRTH_MESSAGE], ) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.2) async def test_delayed_birth_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_config_entry_data, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test sending birth message does not happen until Home Assistant starts.""" - mqtt_mock = await mqtt_mock_entry() - hass.set_state(CoreState.starting) - birth = asyncio.Event() - await hass.async_block_till_done() - + birth = asyncio.Event() entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - mqtt_component_mock = MagicMock( - return_value=hass.data["mqtt"].client, - wraps=hass.data["mqtt"].client, - ) - mqtt_component_mock._mqttc = mqtt_client_mock - - hass.data["mqtt"].client = mqtt_component_mock - mqtt_mock = hass.data["mqtt"].client - mqtt_mock.reset_mock() - - async def wait_birth(msg: ReceiveMessage) -> None: - """Handle birth message.""" - birth.set() - - with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): - await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - mqtt_client_mock.on_connect(None, None, 0, 0) - await hass.async_block_till_done() - with pytest.raises(TimeoutError): - await asyncio.wait_for(birth.wait(), 0.2) - assert not mqtt_client_mock.publish.called - assert not birth.is_set() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await birth.wait() - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "homeassistant/status", - mqtt.ATTR_PAYLOAD: "online", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], -) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -async def test_subscription_done_when_birth_message_is_sent( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, - mqtt_config_entry_data, -) -> None: - """Test sending birth message until initial subscription has been completed.""" - hass.set_state(CoreState.starting) - birth = asyncio.Event() - - await hass.async_block_till_done() - - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - mqtt_client_mock.on_disconnect(None, None, 0, 0) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - @callback def wait_birth(msg: ReceiveMessage) -> None: """Handle birth message.""" birth.set() - await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) await hass.async_block_till_done() - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_connect(None, None, 0, 0) - # We wait until we receive a birth message - await asyncio.wait_for(birth.wait(), 1) + with pytest.raises(TimeoutError): + await asyncio.wait_for(birth.wait(), 0.05) + assert not mqtt_client_mock.publish.called + assert not birth.is_set() - # Assert we already have subscribed at the client - # for new config payloads at the time we the birth message is received + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await birth.wait() + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_subscription_done_when_birth_message_is_sent( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test sending birth message until initial subscription has been completed.""" + mqtt_client_mock = setup_with_birth_msg_client_mock subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) assert ("homeassistant/+/+/config", 0) in subscribe_calls assert ("homeassistant/+/+/+/config", 0) in subscribe_calls mqtt_client_mock.publish.assert_called_with( "homeassistant/status", "online", 0, False ) - assert ("topic/test", 0) in subscribe_calls @pytest.mark.parametrize( @@ -2745,11 +2640,15 @@ async def test_subscription_done_when_birth_message_is_sent( ) async def test_custom_will_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test will message.""" - await mqtt_mock_entry() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mqtt_client_mock.will_set.assert_called_with( topic="death", payload="death", qos=0, retain=False @@ -2758,12 +2657,10 @@ async def test_custom_will_message( async def test_default_will_message( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test will message.""" - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.will_set.assert_called_with( topic="homeassistant/status", payload="offline", qos=0, retain=False ) @@ -2775,56 +2672,47 @@ async def test_default_will_message( ) async def test_no_will_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test will message.""" - await mqtt_mock_entry() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mqtt_client_mock.will_set.assert_not_called() @pytest.mark.parametrize( "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: {}, - mqtt.CONF_DISCOVERY: False, - } - ], + [ENTRY_DEFAULT_BIRTH_MESSAGE | {mqtt.CONF_DISCOVERY: False}], ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_subscribes_topics_on_connect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test subscription to topic on connect.""" - await mqtt_mock_entry() - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - mqtt_client_mock.reset_mock() + mqtt_client_mock = setup_with_birth_msg_client_mock await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) await mqtt.async_subscribe(hass, "still/pending", record_calls) await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) - mqtt_client_mock.on_connect(None, None, 0, 0) + mqtt_client_mock.on_disconnect(Mock(), None, 0) + + mqtt_client_mock.reset_mock() + + mqtt_client_mock.on_connect(Mock(), None, 0, 0) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - - assert mqtt_client_mock.disconnect.call_count == 0 + await hass.async_block_till_done(wait_background_tasks=True) subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) - assert len(subscribe_calls) == 3 assert ("topic/test", 0) in subscribe_calls assert ("home/sensor", 2) in subscribe_calls assert ("still/pending", 1) in subscribe_calls @@ -2832,31 +2720,21 @@ async def test_mqtt_subscribes_topics_on_connect( @pytest.mark.parametrize( "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: {}, - mqtt.CONF_DISCOVERY: False, - } - ], + [ENTRY_DEFAULT_BIRTH_MESSAGE], ) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_subscribes_in_single_call( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test bundled client subscription to topic.""" - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.subscribe.reset_mock() await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "home/sensor", record_calls) - await hass.async_block_till_done() # Make sure the debouncer finishes - await asyncio.sleep(0.2) + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) assert mqtt_client_mock.subscribe.call_count == 1 # Assert we have a single subscription call with both subscriptions @@ -2866,28 +2744,16 @@ async def test_mqtt_subscribes_in_single_call( ] -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: {}, - mqtt.CONF_DISCOVERY: False, - } - ], -) +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) @patch("homeassistant.components.mqtt.client.MAX_SUBSCRIBES_PER_CALL", 2) @patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_subscribes_and_unsubscribes_in_chunks( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test chunked client subscriptions.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.subscribe.reset_mock() unsub_tasks: list[CALLBACK_TYPE] = [] @@ -2895,9 +2761,9 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor1", record_calls)) unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test2", record_calls)) unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor2", record_calls)) - await hass.async_block_till_done() # Make sure the debouncer finishes - await asyncio.sleep(0.2) + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) assert mqtt_client_mock.subscribe.call_count == 2 # Assert we have a 2 subscription calls with both 2 subscriptions @@ -2909,7 +2775,8 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( task() await hass.async_block_till_done() # Make sure the debouncer finishes - await asyncio.sleep(0.2) + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) assert mqtt_client_mock.unsubscribe.call_count == 2 # Assert we have a 2 unsubscribe calls with both 2 topic @@ -2920,7 +2787,6 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( async def test_default_entry_setting_are_applied( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - mqtt_mock_entry: MqttMockHAClientGenerator, mqtt_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: @@ -2932,11 +2798,12 @@ async def test_default_entry_setting_are_applied( ) # Config entry data is incomplete but valid according the schema - entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - hass.config_entries.async_update_entry( - entry, data={"broker": "test-broker", "port": 1234} + entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={"broker": "test-broker", "port": 1234} ) - await mqtt_mock_entry() + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() # Discover a device to verify the entry was setup correctly @@ -2977,10 +2844,9 @@ async def test_message_callback_exception_gets_logged( async def test_message_partial_callback_exception_gets_logged( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test exception raised by message handler.""" - await mqtt_mock_entry() @callback def bad_handler(msg: ReceiveMessage) -> None: @@ -2998,8 +2864,12 @@ async def test_message_partial_callback_exception_gets_logged( await mqtt.async_subscribe( hass, "test-topic", partial(parial_handler, bad_handler, {"some_attr"}) ) + await hass.async_block_till_done(wait_background_tasks=True) async_fire_mqtt_message(hass, "test-topic", "test") await hass.async_block_till_done() + await hass.async_block_till_done() + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) assert ( "Exception in bad_handler when handling msg on 'test-topic':" @@ -3726,11 +3596,10 @@ async def test_publish_json_from_template( async def test_subscribe_connection_status( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test connextion status subscription.""" - mqtt_mock = await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_connected_calls_callback: list[bool] = [] mqtt_connected_calls_async: list[bool] = [] @@ -3743,7 +3612,13 @@ async def test_subscribe_connection_status( """Update state on connection/disconnection to MQTT broker.""" mqtt_connected_calls_async.append(status) - mqtt_mock.connected = True + # Check connection status + assert mqtt.is_connected(hass) is True + + # Mock disconnect status + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + assert mqtt.is_connected(hass) is False unsub_callback = mqtt.async_subscribe_connection_status( hass, async_mqtt_connected_callback @@ -3753,7 +3628,7 @@ async def test_subscribe_connection_status( ) await hass.async_block_till_done() - # Mock connection status + # Mock connect status mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() assert mqtt.is_connected(hass) is True @@ -3761,13 +3636,17 @@ async def test_subscribe_connection_status( # Mock disconnect status mqtt_client_mock.on_disconnect(None, None, 0) await hass.async_block_till_done() + assert mqtt.is_connected(hass) is False # Unsubscribe unsub_callback() unsub_async() + # Mock connect status mqtt_client_mock.on_connect(None, None, 0, 0) + await asyncio.sleep(0) await hass.async_block_till_done() + assert mqtt.is_connected(hass) is True # Check calls assert len(mqtt_connected_calls_callback) == 2 @@ -3781,11 +3660,11 @@ async def test_subscribe_connection_status( async def test_unload_config_entry( hass: HomeAssistant, - mqtt_mock: MqttMockHAClient, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test unloading the MQTT entry.""" + mqtt_client_mock = setup_with_birth_msg_client_mock assert hass.services.has_service(mqtt.DOMAIN, "dump") assert hass.services.has_service(mqtt.DOMAIN, "publish") @@ -4015,6 +3894,7 @@ async def test_link_config_entry( mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] mqtt_platforms = async_get_platforms(hass, mqtt.DOMAIN) + @callback def _check_entities() -> int: entities: list[Entity] = [] for mqtt_platform in mqtt_platforms: @@ -4096,6 +3976,7 @@ async def test_reload_config_entry( entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + @callback def _check_entities() -> int: entities: list[Entity] = [] mqtt_platforms = async_get_platforms(hass, mqtt.DOMAIN) @@ -4406,19 +4287,14 @@ async def test_multi_platform_discovery( ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_auto_reconnect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test reconnection is automatically done.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.reconnect.reset_mock() mqtt_client_mock.disconnect() @@ -4454,20 +4330,15 @@ async def test_auto_reconnect( assert len(mqtt_client_mock.reconnect.mock_calls) == 2 -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_server_sock_connect_and_disconnect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, ) -> None: """Test handling the socket connected and disconnected.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4495,19 +4366,14 @@ async def test_server_sock_connect_and_disconnect( assert len(recorded_calls) == 0 -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_server_sock_buffer_size( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test handling the socket buffer size fails.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4523,19 +4389,14 @@ async def test_server_sock_buffer_size( assert "Unable to increase the socket buffer size" in caplog.text -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_server_sock_buffer_size_with_websocket( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test handling the socket buffer size fails.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4560,20 +4421,15 @@ async def test_server_sock_buffer_size_with_websocket( assert "Unable to increase the socket buffer size" in caplog.text -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_client_sock_failure_after_connect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, ) -> None: """Test handling the socket connected and disconnected.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4589,7 +4445,7 @@ async def test_client_sock_failure_after_connect( mqtt_client_mock.loop_write.side_effect = OSError("foo") client.close() # close the client socket out from under the client - assert mqtt_mock.connected is True + assert mqtt_client_mock.connect.call_count == 1 unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) await hass.async_block_till_done() @@ -4599,19 +4455,14 @@ async def test_client_sock_failure_after_connect( assert len(recorded_calls) == 0 -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_loop_write_failure( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test handling the socket connected and disconnected.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4642,7 +4493,7 @@ async def test_loop_write_failure( # Final for the disconnect callback await hass.async_block_till_done() - assert "Disconnected from MQTT server mock-broker:1883" in caplog.text + assert "Disconnected from MQTT server test-broker:1883" in caplog.text @pytest.mark.parametrize(