From 397864c4979c873e42efa36c06c734965a8cf1d5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 15 May 2023 21:15:10 +0200 Subject: [PATCH] Fix last imap message is not reset on empty search (#93119) --- homeassistant/components/imap/coordinator.py | 4 +- tests/components/imap/test_init.py | 99 ++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 666a82c73d4..31d028c0519 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -201,7 +201,9 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): raise UpdateFailed( f"Invalid response for search '{self.config_entry.data[CONF_SEARCH]}': {result} / {lines[0]}" ) - count: int = len(message_ids := lines[0].split()) + if not (count := len(message_ids := lines[0].split())): + self._last_message_id = None + return 0 last_message_id = ( str(message_ids[-1:][0], encoding=self.config_entry.data[CONF_CHARSET]) if count diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index 58bb084296a..8f00cf395d2 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -15,6 +15,7 @@ from homeassistant.util.dt import utcnow from .const import ( BAD_RESPONSE, + EMPTY_SEARCH_RESPONSE, TEST_FETCH_RESPONSE_BINARY, TEST_FETCH_RESPONSE_HTML, TEST_FETCH_RESPONSE_INVALID_DATE, @@ -347,3 +348,101 @@ async def test_fetch_number_of_messages( # we should have an entity with an unavailable state assert state is not None assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE]) +@pytest.mark.parametrize( + ("imap_fetch", "valid_date"), + [(TEST_FETCH_RESPONSE_TEXT_PLAIN, True)], + ids=["plain"], +) +@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) +async def test_reset_last_message( + hass: HomeAssistant, mock_imap_protocol: MagicMock, valid_date: bool +) -> None: + """Test receiving a message successfully.""" + event = asyncio.Event() # needed for pushed coordinator to make a new loop + + async def _sleep_till_event() -> None: + """Simulate imap server waiting for pushes message and keep the push loop going. + + Needed for pushed coordinator only. + """ + nonlocal event + await event.wait() + event.clear() + mock_imap_protocol.idle_start.return_value = AsyncMock()() + + # Make sure we make another cycle (needed for pushed coordinator) + mock_imap_protocol.idle_start.return_value = AsyncMock()() + # Mock we wait till we push an update (needed for pushed coordinator) + mock_imap_protocol.wait_server_push.side_effect = _sleep_till_event + + event_called = async_capture_events(hass, "imap_content") + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # Make sure we have had one update (when polling) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + state = hass.states.get("sensor.imap_email_email_com") + # We should have received one message + assert state is not None + assert state.state == "1" + + # We should have received one event + assert len(event_called) == 1 + data: dict[str, Any] = event_called[0].data + assert data["server"] == "imap.server.com" + assert data["username"] == "email@email.com" + assert data["search"] == "UnSeen UnDeleted" + assert data["folder"] == "INBOX" + assert data["sender"] == "john.doe@example.com" + assert data["subject"] == "Test subject" + assert data["text"] + assert ( + valid_date + and isinstance(data["date"], datetime) + or not valid_date + and data["date"] is None + ) + + # Simulate an update where no messages are found (needed for pushed coordinator) + mock_imap_protocol.search.return_value = Response(*EMPTY_SEARCH_RESPONSE) + + # Make sure we have an update + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + + # Awake loop (needed for pushed coordinator) + event.set() + + await hass.async_block_till_done() + + state = hass.states.get("sensor.imap_email_email_com") + # We should have message + assert state is not None + assert state.state == "0" + # No new events should be called + assert len(event_called) == 1 + + # Simulate an update where with the original message + mock_imap_protocol.search.return_value = Response(*TEST_SEARCH_RESPONSE) + # Make sure we have an update again with the same UID + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + + # Awake loop (needed for pushed coordinator) + event.set() + + await hass.async_block_till_done() + + state = hass.states.get("sensor.imap_email_email_com") + # We should have received one message + assert state is not None + assert state.state == "1" + await hass.async_block_till_done() + await hass.async_block_till_done() + + # One new event + assert len(event_called) == 2