Add config entry load/unload tests for LetPot (#136736)

This commit is contained in:
Joris Pelgröm 2025-01-28 17:42:26 +01:00 committed by GitHub
parent 9b598ed69c
commit 3eb1b182f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 252 additions and 101 deletions

View File

@ -1,12 +1,42 @@
"""Tests for the LetPot integration.""" """Tests for the LetPot integration."""
from letpot.models import AuthenticationInfo import datetime
from letpot.models import AuthenticationInfo, LetPotDeviceStatus
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Fixture for setting up the component."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
AUTHENTICATION = AuthenticationInfo( AUTHENTICATION = AuthenticationInfo(
access_token="access_token", access_token="access_token",
access_token_expires=0, access_token_expires=1738368000, # 2025-02-01 00:00:00 GMT
refresh_token="refresh_token", refresh_token="refresh_token",
refresh_token_expires=0, refresh_token_expires=1740441600, # 2025-02-25 00:00:00 GMT
user_id="a1b2c3d4e5f6a1b2c3d4e5f6", user_id="a1b2c3d4e5f6a1b2c3d4e5f6",
email="email@example.com", email="email@example.com",
) )
STATUS = LetPotDeviceStatus(
light_brightness=500,
light_mode=1,
light_schedule_end=datetime.time(12, 10),
light_schedule_start=datetime.time(12, 0),
online=True,
plant_days=1,
pump_mode=1,
pump_nutrient=None,
pump_status=0,
raw=[77, 0, 1, 18, 98, 1, 0, 0, 1, 1, 1, 0, 1, 12, 0, 12, 10, 1, 244, 0, 0, 0],
system_on=True,
system_sound=False,
system_state=0,
)

View File

@ -3,6 +3,7 @@
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from letpot.models import LetPotDevice
import pytest import pytest
from homeassistant.components.letpot.const import ( from homeassistant.components.letpot.const import (
@ -14,7 +15,7 @@ from homeassistant.components.letpot.const import (
) )
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL
from . import AUTHENTICATION from . import AUTHENTICATION, STATUS
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -28,6 +29,49 @@ def mock_setup_entry() -> Generator[AsyncMock]:
yield mock_setup_entry yield mock_setup_entry
@pytest.fixture
def mock_client() -> Generator[AsyncMock]:
"""Mock a LetPotClient."""
with (
patch(
"homeassistant.components.letpot.LetPotClient",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.letpot.config_flow.LetPotClient",
new=mock_client,
),
):
client = mock_client.return_value
client.login.return_value = AUTHENTICATION
client.refresh_token.return_value = AUTHENTICATION
client.get_devices.return_value = [
LetPotDevice(
serial_number="LPH21ABCD",
name="Garden",
device_type="LPH21",
is_online=True,
is_remote=False,
)
]
yield client
@pytest.fixture
def mock_device_client() -> Generator[AsyncMock]:
"""Mock a LetPotDeviceClient."""
with patch(
"homeassistant.components.letpot.coordinator.LetPotDeviceClient",
autospec=True,
) as mock_device_client:
device_client = mock_device_client.return_value
device_client.device_model_code = "LPH21"
device_client.device_model_name = "LetPot Air"
device_client.get_current_status.return_value = STATUS
device_client.last_status.return_value = STATUS
yield device_client
@pytest.fixture @pytest.fixture
def mock_config_entry() -> MockConfigEntry: def mock_config_entry() -> MockConfigEntry:
"""Mock a config entry.""" """Mock a config entry."""

View File

@ -2,7 +2,7 @@
import dataclasses import dataclasses
from typing import Any from typing import Any
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock
from letpot.exceptions import LetPotAuthenticationException, LetPotConnectionException from letpot.exceptions import LetPotAuthenticationException, LetPotConnectionException
import pytest import pytest
@ -39,7 +39,9 @@ def _assert_result_success(result: Any) -> None:
assert result["result"].unique_id == AUTHENTICATION.user_id assert result["result"].unique_id == AUTHENTICATION.user_id
async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: async def test_full_flow(
hass: HomeAssistant, mock_client: AsyncMock, mock_setup_entry: AsyncMock
) -> None:
"""Test full flow with success.""" """Test full flow with success."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
@ -47,18 +49,13 @@ async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
with patch( result = await hass.config_entries.flow.async_configure(
"homeassistant.components.letpot.config_flow.LetPotClient.login", result["flow_id"],
return_value=AUTHENTICATION, {
): CONF_EMAIL: "email@example.com",
result = await hass.config_entries.flow.async_configure( CONF_PASSWORD: "test-password",
result["flow_id"], },
{ )
CONF_EMAIL: "email@example.com",
CONF_PASSWORD: "test-password",
},
)
await hass.async_block_till_done()
_assert_result_success(result) _assert_result_success(result)
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
@ -74,6 +71,7 @@ async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No
) )
async def test_flow_exceptions( async def test_flow_exceptions(
hass: HomeAssistant, hass: HomeAssistant,
mock_client: AsyncMock,
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
exception: Exception, exception: Exception,
error: str, error: str,
@ -83,41 +81,37 @@ async def test_flow_exceptions(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
with patch( mock_client.login.side_effect = exception
"homeassistant.components.letpot.config_flow.LetPotClient.login", result = await hass.config_entries.flow.async_configure(
side_effect=exception, result["flow_id"],
): {
result = await hass.config_entries.flow.async_configure( CONF_EMAIL: "email@example.com",
result["flow_id"], CONF_PASSWORD: "test-password",
{ },
CONF_EMAIL: "email@example.com", )
CONF_PASSWORD: "test-password",
},
)
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error} assert result["errors"] == {"base": error}
# Retry to show recovery. # Retry to show recovery.
with patch( mock_client.login.side_effect = None
"homeassistant.components.letpot.config_flow.LetPotClient.login", result = await hass.config_entries.flow.async_configure(
return_value=AUTHENTICATION, result["flow_id"],
): {
result = await hass.config_entries.flow.async_configure( CONF_EMAIL: "email@example.com",
result["flow_id"], CONF_PASSWORD: "test-password",
{ },
CONF_EMAIL: "email@example.com", )
CONF_PASSWORD: "test-password",
},
)
await hass.async_block_till_done()
_assert_result_success(result) _assert_result_success(result)
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_flow_duplicate( async def test_flow_duplicate(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_config_entry: MockConfigEntry hass: HomeAssistant,
mock_client: AsyncMock,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Test flow aborts when trying to add a previously added account.""" """Test flow aborts when trying to add a previously added account."""
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
@ -130,18 +124,13 @@ async def test_flow_duplicate(
assert result["step_id"] == "user" assert result["step_id"] == "user"
assert result["errors"] == {} assert result["errors"] == {}
with patch( result = await hass.config_entries.flow.async_configure(
"homeassistant.components.letpot.config_flow.LetPotClient.login", result["flow_id"],
return_value=AUTHENTICATION, {
): CONF_EMAIL: "email@example.com",
result = await hass.config_entries.flow.async_configure( CONF_PASSWORD: "test-password",
result["flow_id"], },
{ )
CONF_EMAIL: "email@example.com",
CONF_PASSWORD: "test-password",
},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
@ -149,7 +138,10 @@ async def test_flow_duplicate(
async def test_reauth_flow( async def test_reauth_flow(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_config_entry: MockConfigEntry hass: HomeAssistant,
mock_client: AsyncMock,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Test reauth flow with success.""" """Test reauth flow with success."""
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
@ -163,15 +155,11 @@ async def test_reauth_flow(
access_token="new_access_token", access_token="new_access_token",
refresh_token="new_refresh_token", refresh_token="new_refresh_token",
) )
with patch( mock_client.login.return_value = updated_auth
"homeassistant.components.letpot.config_flow.LetPotClient.login", result = await hass.config_entries.flow.async_configure(
return_value=updated_auth, result["flow_id"],
): {CONF_PASSWORD: "new-password"},
result = await hass.config_entries.flow.async_configure( )
result["flow_id"],
{CONF_PASSWORD: "new-password"},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful" assert result["reason"] == "reauth_successful"
@ -196,6 +184,7 @@ async def test_reauth_flow(
) )
async def test_reauth_exceptions( async def test_reauth_exceptions(
hass: HomeAssistant, hass: HomeAssistant,
mock_client: AsyncMock,
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
exception: Exception, exception: Exception,
@ -208,14 +197,11 @@ async def test_reauth_exceptions(
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"
with patch( mock_client.login.side_effect = exception
"homeassistant.components.letpot.config_flow.LetPotClient.login", result = await hass.config_entries.flow.async_configure(
side_effect=exception, result["flow_id"],
): {CONF_PASSWORD: "new-password"},
result = await hass.config_entries.flow.async_configure( )
result["flow_id"],
{CONF_PASSWORD: "new-password"},
)
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error} assert result["errors"] == {"base": error}
@ -226,15 +212,12 @@ async def test_reauth_exceptions(
access_token="new_access_token", access_token="new_access_token",
refresh_token="new_refresh_token", refresh_token="new_refresh_token",
) )
with patch( mock_client.login.return_value = updated_auth
"homeassistant.components.letpot.config_flow.LetPotClient.login", mock_client.login.side_effect = None
return_value=updated_auth, result = await hass.config_entries.flow.async_configure(
): result["flow_id"],
result = await hass.config_entries.flow.async_configure( {CONF_PASSWORD: "new-password"},
result["flow_id"], )
{CONF_PASSWORD: "new-password"},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful" assert result["reason"] == "reauth_successful"
@ -250,7 +233,10 @@ async def test_reauth_exceptions(
async def test_reauth_different_user_id_new( async def test_reauth_different_user_id_new(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_config_entry: MockConfigEntry hass: HomeAssistant,
mock_client: AsyncMock,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Test reauth flow with different, new user ID updating the existing entry.""" """Test reauth flow with different, new user ID updating the existing entry."""
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
@ -263,15 +249,11 @@ async def test_reauth_different_user_id_new(
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"
updated_auth = dataclasses.replace(AUTHENTICATION, user_id="new_user_id") updated_auth = dataclasses.replace(AUTHENTICATION, user_id="new_user_id")
with patch( mock_client.login.return_value = updated_auth
"homeassistant.components.letpot.config_flow.LetPotClient.login", result = await hass.config_entries.flow.async_configure(
return_value=updated_auth, result["flow_id"],
): {CONF_PASSWORD: "new-password"},
result = await hass.config_entries.flow.async_configure( )
result["flow_id"],
{CONF_PASSWORD: "new-password"},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful" assert result["reason"] == "reauth_successful"
@ -289,7 +271,10 @@ async def test_reauth_different_user_id_new(
async def test_reauth_different_user_id_existing( async def test_reauth_different_user_id_existing(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_config_entry: MockConfigEntry hass: HomeAssistant,
mock_client: AsyncMock,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Test reauth flow with different, existing user ID aborting.""" """Test reauth flow with different, existing user ID aborting."""
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
@ -303,15 +288,11 @@ async def test_reauth_different_user_id_existing(
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"
updated_auth = dataclasses.replace(AUTHENTICATION, user_id="other_user_id") updated_auth = dataclasses.replace(AUTHENTICATION, user_id="other_user_id")
with patch( mock_client.login.return_value = updated_auth
"homeassistant.components.letpot.config_flow.LetPotClient.login", result = await hass.config_entries.flow.async_configure(
return_value=updated_auth, result["flow_id"],
): {CONF_PASSWORD: "new-password"},
result = await hass.config_entries.flow.async_configure( )
result["flow_id"],
{CONF_PASSWORD: "new-password"},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"

View File

@ -0,0 +1,96 @@
"""Test the LetPot integration initialization and setup."""
from unittest.mock import MagicMock
from letpot.exceptions import LetPotAuthenticationException, LetPotConnectionException
import pytest
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from . import setup_integration
from tests.common import MockConfigEntry
@pytest.mark.freeze_time("2025-01-31 00:00:00")
async def test_load_unload_config_entry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_client: MagicMock,
mock_device_client: MagicMock,
) -> None:
"""Test config entry loading/unloading."""
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
mock_client.refresh_token.assert_not_called() # Didn't refresh valid token
mock_client.get_devices.assert_called_once()
mock_device_client.subscribe.assert_called_once()
mock_device_client.get_current_status.assert_called_once()
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
mock_device_client.disconnect.assert_called_once()
@pytest.mark.freeze_time("2025-02-15 00:00:00")
async def test_refresh_authentication_on_load(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_client: MagicMock,
mock_device_client: MagicMock,
) -> None:
"""Test expired access token refreshed when needed to load config entry."""
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
mock_client.refresh_token.assert_called_once()
# Check loading continued as expected after refreshing token
mock_client.get_devices.assert_called_once()
mock_device_client.subscribe.assert_called_once()
mock_device_client.get_current_status.assert_called_once()
@pytest.mark.freeze_time("2025-03-01 00:00:00")
async def test_refresh_token_error_aborts(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_client: MagicMock,
) -> None:
"""Test expired refresh token aborting config entry loading."""
mock_client.refresh_token.side_effect = LetPotAuthenticationException
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
mock_client.refresh_token.assert_called_once()
mock_client.get_devices.assert_not_called()
@pytest.mark.parametrize(
("exception", "config_entry_state"),
[
(LetPotAuthenticationException, ConfigEntryState.SETUP_ERROR),
(LetPotConnectionException, ConfigEntryState.SETUP_RETRY),
],
)
async def test_get_devices_exceptions(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_client: MagicMock,
mock_device_client: MagicMock,
exception: Exception,
config_entry_state: ConfigEntryState,
) -> None:
"""Test config entry errors if an exception is raised when getting devices."""
mock_client.get_devices.side_effect = exception
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is config_entry_state
mock_client.get_devices.assert_called_once()
mock_device_client.subscribe.assert_not_called()