Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 58 additions & 9 deletions homeassistant/components/icloud/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from pyicloud import PyiCloudService
from pyicloud.exceptions import (
PyiCloudAuthRequiredException,
PyiCloudFailedLoginException,
PyiCloudNoDevicesException,
PyiCloudServiceNotActivatedException,
Expand Down Expand Up @@ -111,23 +112,71 @@ def setup(self) -> None:
raise PyiCloudFailedLoginException("2FA Required") # noqa: TRY301

except PyiCloudFailedLoginException:
requires_2fa = self.api is not None and self.api.requires_2fa
self.api = None
Comment thread
TeroPihlaja marked this conversation as resolved.
Outdated
# Login failed which means credentials need to be updated.
_LOGGER.error(
(
"Your password for '%s' is no longer working; Go to the "
"Integrations menu and click on Configure on the discovered Apple "
"iCloud card to login again"
),
self._config_entry.data[CONF_USERNAME],
)
# Login failed, which can mean 2FA reauthentication is required or
# that credentials need to be updated.
if requires_2fa:
_LOGGER.warning(
(
"2FA authentication required for '%s'; Go to the "
"Integrations menu and click on Configure on the iCloud "
"card to enter your verification code"
),
self._config_entry.data[CONF_USERNAME],
)
else:
_LOGGER.error(
(
"Your password for '%s' is no longer working; Go to the "
"Integrations menu and click on Configure on the discovered Apple "
"iCloud card to login again"
),
self._config_entry.data[CONF_USERNAME],
)

Comment thread
TeroPihlaja marked this conversation as resolved.
Outdated
self._require_reauth()
return

except PyiCloudAuthRequiredException:
requires_2fa = self.api is not None and self.api.requires_2fa
self.api = None
Comment thread
TeroPihlaja marked this conversation as resolved.
Outdated
if requires_2fa:
_LOGGER.warning(
(
"2FA authentication required for '%s'; Go to the "
Comment thread
TeroPihlaja marked this conversation as resolved.
Outdated
"Integrations menu and click on Configure on the iCloud "
"card to enter your verification code"
),
self._config_entry.data[CONF_USERNAME],
)
else:
_LOGGER.error(
(
"Re-authentication required for '%s'; Go to the "
"Integrations menu and click on Configure on the iCloud "
"card to login again"
),
self._config_entry.data[CONF_USERNAME],
)
self._require_reauth()
return
Comment thread
TeroPihlaja marked this conversation as resolved.
Comment thread
TeroPihlaja marked this conversation as resolved.

try:
# Gets device owners infos
user_info = self.api.devices.user_info
except PyiCloudAuthRequiredException:
self.api = None
_LOGGER.warning(
(
"Re-authentication required for '%s'; Go to the "
"Integrations menu and click on Configure on the iCloud "
"card to enter your verification code"
),
self._config_entry.data[CONF_USERNAME],
)
Comment thread
TeroPihlaja marked this conversation as resolved.
Outdated
self._require_reauth()
return
except (
PyiCloudServiceNotActivatedException,
PyiCloudNoDevicesException,
Expand Down
110 changes: 109 additions & 1 deletion tests/components/icloud/test_account.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Tests for the iCloud account."""

from unittest.mock import MagicMock, Mock, patch
from unittest.mock import MagicMock, Mock, PropertyMock, patch

from pyicloud.exceptions import PyiCloudAuthRequiredException
import pytest

from homeassistant.components.icloud.account import IcloudAccount
Expand Down Expand Up @@ -165,3 +166,110 @@ async def test_setup_success_with_devices(
assert account.owner_fullname == "user name"
assert "johntravolta" in account.family_members_fullname
assert account.family_members_fullname["johntravolta"] == "John TRAVOLTA"


def _make_account(hass: HomeAssistant, mock_store: Mock) -> IcloudAccount:
"""Build an IcloudAccount with mocked config entry."""
config_entry = MockConfigEntry(
domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=USERNAME
)
config_entry.add_to_hass(hass)
return IcloudAccount(
hass,
MOCK_CONFIG[CONF_USERNAME],
MOCK_CONFIG[CONF_PASSWORD],
mock_store,
MOCK_CONFIG[CONF_WITH_FAMILY],
MOCK_CONFIG[CONF_MAX_INTERVAL],
MOCK_CONFIG[CONF_GPS_ACCURACY_THRESHOLD],
config_entry,
)


async def test_setup_failed_login_with_2fa_logs_warning(
hass: HomeAssistant,
mock_store: Mock,
) -> None:
"""Test setup logs a warning (not error) when the login failure is due to 2FA.

When requires_2fa is True, the code internally raises PyiCloudFailedLoginException.
The handler should log a warning directing the user to enter a code, not an error
telling them their password no longer works.
"""
account = _make_account(hass, mock_store)

service_instance = MagicMock()
service_instance.requires_2fa = True

with (
patch(
"homeassistant.components.icloud.account.PyiCloudService",
return_value=service_instance,
),
patch.object(account, "_require_reauth") as mock_reauth,
patch("homeassistant.components.icloud.account._LOGGER") as mock_logger,
):
account.setup()

mock_reauth.assert_called_once()
assert account.api is None
mock_logger.warning.assert_called_once()
mock_logger.error.assert_not_called()


async def test_setup_auth_required_exception_calls_reauth(
hass: HomeAssistant,
mock_store: Mock,
) -> None:
"""Test setup handles PyiCloudAuthRequiredException by calling reauth.

This covers the case where FMIP requires re-authentication even after the
main iCloud login succeeded (e.g. MFA required specifically for Find My).
Before this fix, the exception was unhandled and crashed setup.
"""
account = _make_account(hass, mock_store)

with (
patch(
"homeassistant.components.icloud.account.PyiCloudService",
side_effect=PyiCloudAuthRequiredException("test@example.com", MagicMock()),
),
patch.object(account, "_require_reauth") as mock_reauth,
):
account.setup()
Comment thread
TeroPihlaja marked this conversation as resolved.

mock_reauth.assert_called_once()
assert account.api is None


async def test_setup_auth_required_exception_from_devices_calls_reauth(
hass: HomeAssistant,
mock_store: Mock,
) -> None:
"""Test setup handles PyiCloudAuthRequiredException raised when reading devices.
Comment thread
TeroPihlaja marked this conversation as resolved.

This covers the case where auth is required when accessing device data
(e.g. api.devices.user_info) after service construction succeeded.
Before this fix, the exception was unhandled and crashed setup.
"""
account = _make_account(hass, mock_store)

service_instance = MagicMock()
service_instance.requires_2fa = False
devices_mock = MagicMock()
type(devices_mock).user_info = PropertyMock(
side_effect=PyiCloudAuthRequiredException("test@example.com", MagicMock())
)
Comment thread
TeroPihlaja marked this conversation as resolved.
Outdated
service_instance.devices = devices_mock

with (
patch(
"homeassistant.components.icloud.account.PyiCloudService",
return_value=service_instance,
),
patch.object(account, "_require_reauth") as mock_reauth,
):
account.setup()

mock_reauth.assert_called_once()
assert account.api is None
Loading