From b9ef776fb41b880d1927078d75af75833cf18fab Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 12 Nov 2025 18:54:46 +1300 Subject: [PATCH 01/13] Bump pyiCloud version to 2.1.1 due to "self.api.devices.user_info" missing from pyiCloud 2.1.0 Fixes #155652, #155933 --- homeassistant/components/icloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 60a4063a079fa..f2afd19674a29 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/icloud", "iot_class": "cloud_polling", "loggers": ["keyrings.alt", "pyicloud"], - "requirements": ["pyicloud==2.1.0"] + "requirements": ["pyicloud==2.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index bcbf36669f676..e9a819443fe7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2062,7 +2062,7 @@ pyhomeworks==1.1.2 pyialarm==2.2.0 # homeassistant.components.icloud -pyicloud==2.1.0 +pyicloud==2.1.1 # homeassistant.components.insteon pyinsteon==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49608e835a1ca..1e8276fc0a95b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1721,7 +1721,7 @@ pyhomeworks==1.1.2 pyialarm==2.2.0 # homeassistant.components.icloud -pyicloud==2.1.0 +pyicloud==2.1.1 # homeassistant.components.insteon pyinsteon==1.6.3 From 75a954566bfc319406d2a97f80092603a3a9e1e9 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 13 Nov 2025 12:45:48 +1300 Subject: [PATCH 02/13] Bump pyiCloud to 2.2.0 Read properties correctly --- homeassistant/components/icloud/account.py | 6 ++++-- homeassistant/components/icloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 35e04d20ecd15..6a76cce047cd4 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -12,6 +12,7 @@ PyiCloudFailedLoginException, PyiCloudNoDevicesException, PyiCloudServiceNotActivatedException, + PyiCloudServiceUnavailable ) from pyicloud.services.findmyiphone import AppleDevice @@ -130,15 +131,16 @@ def setup(self) -> None: except ( PyiCloudServiceNotActivatedException, PyiCloudNoDevicesException, + PyiCloudServiceUnavailable, ) as err: _LOGGER.error("No iCloud device found") raise ConfigEntryNotReady from err - self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" + self._owner_fullname = f"{user_info.get('firstName')} {user_info.get('lastName')}" self._family_members_fullname = {} if user_info.get("membersInfo") is not None: - for prs_id, member in user_info["membersInfo"].items(): + for prs_id, member in user_info.get("membersInfo").items(): self._family_members_fullname[prs_id] = ( f"{member['firstName']} {member['lastName']}" ) diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index f2afd19674a29..0cf6b89d20cda 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/icloud", "iot_class": "cloud_polling", "loggers": ["keyrings.alt", "pyicloud"], - "requirements": ["pyicloud==2.1.1"] + "requirements": ["pyicloud==2.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index e9a819443fe7c..b199e8cb1979a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2062,7 +2062,7 @@ pyhomeworks==1.1.2 pyialarm==2.2.0 # homeassistant.components.icloud -pyicloud==2.1.1 +pyicloud==2.2.0 # homeassistant.components.insteon pyinsteon==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e8276fc0a95b..d1ecbde3b1784 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1721,7 +1721,7 @@ pyhomeworks==1.1.2 pyialarm==2.2.0 # homeassistant.components.icloud -pyicloud==2.1.1 +pyicloud==2.2.0 # homeassistant.components.insteon pyinsteon==1.6.3 From 5fb67cca51a33b8f2f83eb29444907350b46239b Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 13 Nov 2025 13:12:51 +1300 Subject: [PATCH 03/13] Fix ruff errors --- homeassistant/components/icloud/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 6a76cce047cd4..9042acc6851d9 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -12,7 +12,7 @@ PyiCloudFailedLoginException, PyiCloudNoDevicesException, PyiCloudServiceNotActivatedException, - PyiCloudServiceUnavailable + PyiCloudServiceUnavailable, ) from pyicloud.services.findmyiphone import AppleDevice From 49da0bf20e0b85d02f31bd92ab773b254aecd8ee Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 13 Nov 2025 13:45:49 +1300 Subject: [PATCH 04/13] Missed a ruff error --- homeassistant/components/icloud/account.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 9042acc6851d9..150a1cb798d22 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -136,7 +136,9 @@ def setup(self) -> None: _LOGGER.error("No iCloud device found") raise ConfigEntryNotReady from err - self._owner_fullname = f"{user_info.get('firstName')} {user_info.get('lastName')}" + self._owner_fullname = ( + f"{user_info.get('firstName')} {user_info.get('lastName')}" + ) self._family_members_fullname = {} if user_info.get("membersInfo") is not None: From 3e4f577dede62d2a4e91cfe2c6844fc09bc53aed Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 15 Nov 2025 00:58:17 +1300 Subject: [PATCH 05/13] Add test --- tests/components/icloud/const.py | 28 ++++++++ tests/components/icloud/test_account.py | 91 +++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 tests/components/icloud/test_account.py diff --git a/tests/components/icloud/const.py b/tests/components/icloud/const.py index 463ae6a7da23c..7abed44c8b82d 100644 --- a/tests/components/icloud/const.py +++ b/tests/components/icloud/const.py @@ -18,6 +18,16 @@ MAX_INTERVAL = 15 GPS_ACCURACY_THRESHOLD = 250 +# Fakers +FIRST_NAME = "user" +LAST_NAME = "name" + +MEMBER_1_FIRST_NAME = "John" +MEMBER_1_LAST_NAME = "TRAVOLTA" +MEMBER_1_FULL_NAME = MEMBER_1_FIRST_NAME + " " + MEMBER_1_LAST_NAME +MEMBER_1_PERSON_ID = (MEMBER_1_FIRST_NAME + MEMBER_1_LAST_NAME).lower() +MEMBER_1_APPLE_ID = MEMBER_1_PERSON_ID + "@icloud.com" + MOCK_CONFIG = { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, @@ -29,3 +39,21 @@ TRUSTED_DEVICES = [ {"deviceType": "SMS", "areaCode": "", "phoneNumber": "*******58", "deviceId": "1"} ] + +USER_INFO = { + "accountFormatter": 0, + "firstName": FIRST_NAME, + "lastName": LAST_NAME, + "membersInfo": { + MEMBER_1_PERSON_ID: { + "accountFormatter": 0, + "firstName": MEMBER_1_FIRST_NAME, + "lastName": MEMBER_1_LAST_NAME, + "deviceFetchStatus": "DONE", + "useAuthWidget": True, + "isHSA": True, + "appleId": MEMBER_1_APPLE_ID, + } + }, + "hasMembers": True, +} diff --git a/tests/components/icloud/test_account.py b/tests/components/icloud/test_account.py new file mode 100644 index 0000000000000..6a5d15939dacf --- /dev/null +++ b/tests/components/icloud/test_account.py @@ -0,0 +1,91 @@ +"""Tests for the iCloud account.""" + +from unittest.mock import MagicMock, Mock, patch + +import pytest + +from homeassistant.components.icloud.account import IcloudAccount +from homeassistant.components.icloud.const import ( + CONF_GPS_ACCURACY_THRESHOLD, + CONF_MAX_INTERVAL, + CONF_WITH_FAMILY, + DOMAIN, +) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import MOCK_CONFIG, USER_INFO, USERNAME + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mock_icloud_service") +def mock_icloud_service_fixture(): + """Mock PyiCloudService with devices as object.""" + with patch( + "homeassistant.components.icloud.account.PyiCloudService" + ) as service_mock: + service_instance = MagicMock() + service_instance.requires_2fa = False + service_instance.devices = Mock() + service_instance.devices.user_info = USER_INFO + service_mock.return_value = service_instance + yield service_instance + + +async def test_setup_success_with_dict_devices( + hass: HomeAssistant, + mock_store: Mock, + mock_icloud_service: MagicMock, +) -> None: + """Test successful setup with devices as dict.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=USERNAME + ) + config_entry.add_to_hass(hass) + + account = 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, + ) + + with patch.object(account, "_schedule_next_fetch"): + account.setup() + + assert account.api is not None + assert account.owner_fullname == "user name" + assert "person1" in account.family_members_fullname + assert account.family_members_fullname["person1"] == "John TRAVOLTA" + + +async def test_setup_fails_when_userinfo_missing( + hass: HomeAssistant, + mock_store: Mock, + mock_icloud_service_no_userinfo: MagicMock, +) -> None: + """Test setup fails when userInfo is missing from devices dict.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=USERNAME + ) + config_entry.add_to_hass(hass) + + account = 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, + ) + + with pytest.raises(ConfigEntryNotReady, match="No user info found"): + account.setup() From 026d3d808a8484671cafd7eed486c278e449ea0a Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 15 Nov 2025 01:35:25 +1300 Subject: [PATCH 06/13] . --- tests/components/icloud/test_account.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/components/icloud/test_account.py b/tests/components/icloud/test_account.py index 6a5d15939dacf..8f04645f574d0 100644 --- a/tests/components/icloud/test_account.py +++ b/tests/components/icloud/test_account.py @@ -14,6 +14,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.storage import Store from .const import MOCK_CONFIG, USER_INFO, USERNAME @@ -33,6 +34,10 @@ def mock_icloud_service_fixture(): service_mock.return_value = service_instance yield service_instance +@pytest.fixture(name="mock_store") +def mock_store_fixture(hass) -> Store: + """Return a Store instance for tests that writes to the test config dir.""" + return Store(hass, version=1, key="icloud_account") async def test_setup_success_with_dict_devices( hass: HomeAssistant, From 3bb66160163fd60785cb6096fdb3473957e92bb4 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 17 Nov 2025 00:23:13 +0000 Subject: [PATCH 07/13] Fix tests --- tests/components/icloud/test_account.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/icloud/test_account.py b/tests/components/icloud/test_account.py index 8f04645f574d0..26344e6b89424 100644 --- a/tests/components/icloud/test_account.py +++ b/tests/components/icloud/test_account.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, Mock, patch +from const import MOCK_CONFIG, USER_INFO, USERNAME import pytest from homeassistant.components.icloud.account import IcloudAccount @@ -16,8 +17,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.storage import Store -from .const import MOCK_CONFIG, USER_INFO, USERNAME - from tests.common import MockConfigEntry @@ -34,11 +33,13 @@ def mock_icloud_service_fixture(): service_mock.return_value = service_instance yield service_instance + @pytest.fixture(name="mock_store") -def mock_store_fixture(hass) -> Store: +def mock_store_fixture(hass: HomeAssistant) -> Store: """Return a Store instance for tests that writes to the test config dir.""" return Store(hass, version=1, key="icloud_account") + async def test_setup_success_with_dict_devices( hass: HomeAssistant, mock_store: Mock, From da1602b5c45702bb0cab53705b7de25aa66b2bba Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 17 Nov 2025 00:39:38 +0000 Subject: [PATCH 08/13] Fix tests --- tests/components/icloud/test_account.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/icloud/test_account.py b/tests/components/icloud/test_account.py index 26344e6b89424..50b752070f59c 100644 --- a/tests/components/icloud/test_account.py +++ b/tests/components/icloud/test_account.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock, Mock, patch -from const import MOCK_CONFIG, USER_INFO, USERNAME import pytest from homeassistant.components.icloud.account import IcloudAccount @@ -17,6 +16,8 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.storage import Store +from .const import MOCK_CONFIG, USER_INFO, USERNAME + from tests.common import MockConfigEntry From ff7abfcab79a5222016f7b13cb1a6063d27ef90d Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 17 Nov 2025 01:19:42 +0000 Subject: [PATCH 09/13] Fix tests --- tests/components/icloud/test_account.py | 33 +++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/tests/components/icloud/test_account.py b/tests/components/icloud/test_account.py index 50b752070f59c..8b53afe661f6f 100644 --- a/tests/components/icloud/test_account.py +++ b/tests/components/icloud/test_account.py @@ -21,6 +21,25 @@ from tests.common import MockConfigEntry +@pytest.fixture(name="mock_store") +def mock_store_fixture(hass: HomeAssistant) -> Store: + """Return a Store instance for tests that writes to the test config dir.""" + return Store(hass, version=1, key="icloud_account") + + +@pytest.fixture(name="mock_icloud_service_no_userinfo") +def mock_icloud_service_no_userinfo_fixture(): + """Mock PyiCloudService with devices as dict but no userInfo.""" + with patch( + "homeassistant.components.icloud.account.PyiCloudService" + ) as service_mock: + service_instance = MagicMock() + service_instance.requires_2fa = False + service_instance.devices = {} + service_mock.return_value = service_instance + yield service_instance + + @pytest.fixture(name="mock_icloud_service") def mock_icloud_service_fixture(): """Mock PyiCloudService with devices as object.""" @@ -35,18 +54,15 @@ def mock_icloud_service_fixture(): yield service_instance -@pytest.fixture(name="mock_store") -def mock_store_fixture(hass: HomeAssistant) -> Store: - """Return a Store instance for tests that writes to the test config dir.""" - return Store(hass, version=1, key="icloud_account") - - async def test_setup_success_with_dict_devices( hass: HomeAssistant, mock_store: Mock, mock_icloud_service: MagicMock, ) -> None: """Test successful setup with devices as dict.""" + + assert mock_icloud_service is not None + config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=USERNAME ) @@ -75,9 +91,12 @@ async def test_setup_success_with_dict_devices( async def test_setup_fails_when_userinfo_missing( hass: HomeAssistant, mock_store: Mock, - mock_icloud_service_no_userinfo: MagicMock, + mock_icloud_service_no_userinfo: Mock, ) -> None: """Test setup fails when userInfo is missing from devices dict.""" + + assert mock_icloud_service_no_userinfo is not None + config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=USERNAME ) From 7947d1842dca772a8add4ede8a36146aa4f01862 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 17 Nov 2025 04:41:47 +0000 Subject: [PATCH 10/13] Fix tests --- homeassistant/components/icloud/account.py | 3 + tests/components/icloud/const.py | 28 ++++----- tests/components/icloud/test_account.py | 67 +++++----------------- 3 files changed, 28 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 150a1cb798d22..d1d35def76bd2 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -136,6 +136,9 @@ def setup(self) -> None: _LOGGER.error("No iCloud device found") raise ConfigEntryNotReady from err + if user_info is None: + raise ConfigEntryNotReady("No user info found in iCloud devices response") + self._owner_fullname = ( f"{user_info.get('firstName')} {user_info.get('lastName')}" ) diff --git a/tests/components/icloud/const.py b/tests/components/icloud/const.py index 7abed44c8b82d..2540f05d18130 100644 --- a/tests/components/icloud/const.py +++ b/tests/components/icloud/const.py @@ -40,20 +40,16 @@ {"deviceType": "SMS", "areaCode": "", "phoneNumber": "*******58", "deviceId": "1"} ] -USER_INFO = { - "accountFormatter": 0, - "firstName": FIRST_NAME, - "lastName": LAST_NAME, - "membersInfo": { - MEMBER_1_PERSON_ID: { - "accountFormatter": 0, - "firstName": MEMBER_1_FIRST_NAME, - "lastName": MEMBER_1_LAST_NAME, - "deviceFetchStatus": "DONE", - "useAuthWidget": True, - "isHSA": True, - "appleId": MEMBER_1_APPLE_ID, - } - }, - "hasMembers": True, +DEVICE = { + "id": "device1", + "name": "iPhone", + "deviceStatus": "200", + "batteryStatus": "NotCharging", + "batteryLevel": 0.8, + "rawDeviceModel": "iPhone14,2", + "deviceClass": "iPhone", + "deviceDisplayName": "iPhone", + "prsId": None, + "lowPowerMode": False, + "location": None, } diff --git a/tests/components/icloud/test_account.py b/tests/components/icloud/test_account.py index 8b53afe661f6f..4474c793439b7 100644 --- a/tests/components/icloud/test_account.py +++ b/tests/components/icloud/test_account.py @@ -16,15 +16,19 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.storage import Store -from .const import MOCK_CONFIG, USER_INFO, USERNAME +from .const import DEVICE, MOCK_CONFIG, USERNAME from tests.common import MockConfigEntry @pytest.fixture(name="mock_store") -def mock_store_fixture(hass: HomeAssistant) -> Store: - """Return a Store instance for tests that writes to the test config dir.""" - return Store(hass, version=1, key="icloud_account") +def mock_store_fixture(): + """Mock the storage.""" + with patch("homeassistant.components.icloud.account.Store") as store_mock: + store_instance = Mock(spec=Store) + store_instance.path = "/mock/path" + store_mock.return_value = store_instance + yield store_instance @pytest.fixture(name="mock_icloud_service_no_userinfo") @@ -35,63 +39,18 @@ def mock_icloud_service_no_userinfo_fixture(): ) as service_mock: service_instance = MagicMock() service_instance.requires_2fa = False - service_instance.devices = {} + mock_device = MagicMock() + mock_device.status = iter(DEVICE) + mock_device.user_info = None + service_instance.devices = mock_device service_mock.return_value = service_instance yield service_instance -@pytest.fixture(name="mock_icloud_service") -def mock_icloud_service_fixture(): - """Mock PyiCloudService with devices as object.""" - with patch( - "homeassistant.components.icloud.account.PyiCloudService" - ) as service_mock: - service_instance = MagicMock() - service_instance.requires_2fa = False - service_instance.devices = Mock() - service_instance.devices.user_info = USER_INFO - service_mock.return_value = service_instance - yield service_instance - - -async def test_setup_success_with_dict_devices( - hass: HomeAssistant, - mock_store: Mock, - mock_icloud_service: MagicMock, -) -> None: - """Test successful setup with devices as dict.""" - - assert mock_icloud_service is not None - - config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=USERNAME - ) - config_entry.add_to_hass(hass) - - account = 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, - ) - - with patch.object(account, "_schedule_next_fetch"): - account.setup() - - assert account.api is not None - assert account.owner_fullname == "user name" - assert "person1" in account.family_members_fullname - assert account.family_members_fullname["person1"] == "John TRAVOLTA" - - async def test_setup_fails_when_userinfo_missing( hass: HomeAssistant, mock_store: Mock, - mock_icloud_service_no_userinfo: Mock, + mock_icloud_service_no_userinfo: MagicMock, ) -> None: """Test setup fails when userInfo is missing from devices dict.""" From 460882e21c73727831bb20495b9f7a73b8ec0e47 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 17 Nov 2025 04:43:37 +0000 Subject: [PATCH 11/13] Fix tests --- tests/components/icloud/const.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/components/icloud/const.py b/tests/components/icloud/const.py index 2540f05d18130..6b1203798a322 100644 --- a/tests/components/icloud/const.py +++ b/tests/components/icloud/const.py @@ -22,12 +22,6 @@ FIRST_NAME = "user" LAST_NAME = "name" -MEMBER_1_FIRST_NAME = "John" -MEMBER_1_LAST_NAME = "TRAVOLTA" -MEMBER_1_FULL_NAME = MEMBER_1_FIRST_NAME + " " + MEMBER_1_LAST_NAME -MEMBER_1_PERSON_ID = (MEMBER_1_FIRST_NAME + MEMBER_1_LAST_NAME).lower() -MEMBER_1_APPLE_ID = MEMBER_1_PERSON_ID + "@icloud.com" - MOCK_CONFIG = { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, From 0346df622f518da3a751d74f6da1fa5cc8b56ba7 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 17 Nov 2025 04:45:34 +0000 Subject: [PATCH 12/13] remove unused items --- tests/components/icloud/const.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/components/icloud/const.py b/tests/components/icloud/const.py index 6b1203798a322..14c61fa7ef094 100644 --- a/tests/components/icloud/const.py +++ b/tests/components/icloud/const.py @@ -18,10 +18,6 @@ MAX_INTERVAL = 15 GPS_ACCURACY_THRESHOLD = 250 -# Fakers -FIRST_NAME = "user" -LAST_NAME = "name" - MOCK_CONFIG = { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, From 7067913fb718f95214552801b00d0925b9a7ab36 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 17 Nov 2025 10:47:51 +0000 Subject: [PATCH 13/13] Add Test --- tests/components/icloud/const.py | 26 +++++++ tests/components/icloud/test_account.py | 93 ++++++++++++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/tests/components/icloud/const.py b/tests/components/icloud/const.py index 14c61fa7ef094..d2bbfeb717c38 100644 --- a/tests/components/icloud/const.py +++ b/tests/components/icloud/const.py @@ -10,6 +10,8 @@ ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +FIRST_NAME = "user" +LAST_NAME = "name" USERNAME = "username@me.com" USERNAME_2 = "second_username@icloud.com" PASSWORD = "password" @@ -18,6 +20,30 @@ MAX_INTERVAL = 15 GPS_ACCURACY_THRESHOLD = 250 +MEMBER_1_FIRST_NAME = "John" +MEMBER_1_LAST_NAME = "TRAVOLTA" +MEMBER_1_FULL_NAME = MEMBER_1_FIRST_NAME + " " + MEMBER_1_LAST_NAME +MEMBER_1_PERSON_ID = (MEMBER_1_FIRST_NAME + MEMBER_1_LAST_NAME).lower() +MEMBER_1_APPLE_ID = MEMBER_1_PERSON_ID + "@icloud.com" + +USER_INFO = { + "accountFormatter": 0, + "firstName": FIRST_NAME, + "lastName": LAST_NAME, + "membersInfo": { + MEMBER_1_PERSON_ID: { + "accountFormatter": 0, + "firstName": MEMBER_1_FIRST_NAME, + "lastName": MEMBER_1_LAST_NAME, + "deviceFetchStatus": "DONE", + "useAuthWidget": True, + "isHSA": True, + "appleId": MEMBER_1_APPLE_ID, + } + }, + "hasMembers": True, +} + MOCK_CONFIG = { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, diff --git a/tests/components/icloud/test_account.py b/tests/components/icloud/test_account.py index 4474c793439b7..52e6799f41749 100644 --- a/tests/components/icloud/test_account.py +++ b/tests/components/icloud/test_account.py @@ -16,7 +16,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.storage import Store -from .const import DEVICE, MOCK_CONFIG, USERNAME +from .const import DEVICE, MOCK_CONFIG, USER_INFO, USERNAME from tests.common import MockConfigEntry @@ -74,3 +74,94 @@ async def test_setup_fails_when_userinfo_missing( with pytest.raises(ConfigEntryNotReady, match="No user info found"): account.setup() + + +class MockAppleDevice: + """Mock "Apple device" which implements the .status(...) method used by the account.""" + + def __init__(self, status_dict) -> None: + """Set status.""" + self._status = status_dict + + def status(self, key): + """Return current status.""" + return self._status + + def __getitem__(self, key): + """Allow indexing the device itself (device[KEY]) to proxy into the raw status dict.""" + return self._status.get(key) + + +class MockDevicesContainer: + """Mock devices container which is iterable and indexable returning device status dicts.""" + + def __init__(self, userinfo, devices) -> None: + """Initialize with userinfo and list of device objects.""" + self.user_info = userinfo + self._devices = devices + + def __iter__(self): + """Iterate returns device objects (each must have .status(...)).""" + return iter(self._devices) + + def __len__(self): + """Return number of devices.""" + return len(self._devices) + + def __getitem__(self, idx): + """Indexing returns device object (which must have .status(...)).""" + dev = self._devices[idx] + if hasattr(dev, "status"): + return dev.status(None) + return dev + + +@pytest.fixture(name="mock_icloud_service") +def mock_icloud_service_fixture(): + """Mock PyiCloudService with devices container that is iterable and indexable returning status dict.""" + with patch( + "homeassistant.components.icloud.account.PyiCloudService", + ) as service_mock: + service_instance = MagicMock() + device_obj = MockAppleDevice(DEVICE) + devices_container = MockDevicesContainer(USER_INFO, [device_obj]) + + service_instance.devices = devices_container + service_instance.requires_2fa = False + + service_mock.return_value = service_instance + yield service_instance + + +async def test_setup_success_with_devices( + hass: HomeAssistant, + mock_store: Mock, + mock_icloud_service: MagicMock, +) -> None: + """Test successful setup with devices.""" + + assert mock_icloud_service is not None + + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=USERNAME + ) + config_entry.add_to_hass(hass) + + account = 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, + ) + + with patch.object(account, "_schedule_next_fetch"): + account.setup() + + assert account.api is not None + assert account.owner_fullname == "user name" + assert "johntravolta" in account.family_members_fullname + assert account.family_members_fullname["johntravolta"] == "John TRAVOLTA"