mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 02:48:57 +00:00
Bump pyiCloud to 2.2.0 (#156485)
This commit is contained in:
@@ -12,6 +12,7 @@ from pyicloud.exceptions import (
|
||||
PyiCloudFailedLoginException,
|
||||
PyiCloudNoDevicesException,
|
||||
PyiCloudServiceNotActivatedException,
|
||||
PyiCloudServiceUnavailable,
|
||||
)
|
||||
from pyicloud.services.findmyiphone import AppleDevice
|
||||
|
||||
@@ -130,15 +131,21 @@ class IcloudAccount:
|
||||
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']}"
|
||||
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')}"
|
||||
)
|
||||
|
||||
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']}"
|
||||
)
|
||||
|
||||
@@ -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.2.0"]
|
||||
}
|
||||
|
||||
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -2063,7 +2063,7 @@ pyhomeworks==1.1.2
|
||||
pyialarm==2.2.0
|
||||
|
||||
# homeassistant.components.icloud
|
||||
pyicloud==2.1.0
|
||||
pyicloud==2.2.0
|
||||
|
||||
# homeassistant.components.insteon
|
||||
pyinsteon==1.6.3
|
||||
|
||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -1722,7 +1722,7 @@ pyhomeworks==1.1.2
|
||||
pyialarm==2.2.0
|
||||
|
||||
# homeassistant.components.icloud
|
||||
pyicloud==2.1.0
|
||||
pyicloud==2.2.0
|
||||
|
||||
# homeassistant.components.insteon
|
||||
pyinsteon==1.6.3
|
||||
|
||||
@@ -10,6 +10,8 @@ from homeassistant.components.icloud.const import (
|
||||
)
|
||||
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 @@ WITH_FAMILY = True
|
||||
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,
|
||||
@@ -29,3 +55,17 @@ MOCK_CONFIG = {
|
||||
TRUSTED_DEVICES = [
|
||||
{"deviceType": "SMS", "areaCode": "", "phoneNumber": "*******58", "deviceId": "1"}
|
||||
]
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
167
tests/components/icloud/test_account.py
Normal file
167
tests/components/icloud/test_account.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""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 homeassistant.helpers.storage import Store
|
||||
|
||||
from .const import DEVICE, MOCK_CONFIG, USER_INFO, USERNAME
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_store")
|
||||
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")
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
assert mock_icloud_service_no_userinfo 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 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"
|
||||
Reference in New Issue
Block a user