|
| 1 | +"""Tests for the iCloud account.""" |
| 2 | + |
| 3 | +from unittest.mock import MagicMock, Mock, patch |
| 4 | + |
| 5 | +import pytest |
| 6 | + |
| 7 | +from homeassistant.components.icloud.account import IcloudAccount |
| 8 | +from homeassistant.components.icloud.const import ( |
| 9 | + CONF_GPS_ACCURACY_THRESHOLD, |
| 10 | + CONF_MAX_INTERVAL, |
| 11 | + CONF_WITH_FAMILY, |
| 12 | + DOMAIN, |
| 13 | +) |
| 14 | +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME |
| 15 | +from homeassistant.core import HomeAssistant |
| 16 | +from homeassistant.exceptions import ConfigEntryNotReady |
| 17 | +from homeassistant.helpers.storage import Store |
| 18 | + |
| 19 | +from .const import DEVICE, MOCK_CONFIG, USER_INFO, USERNAME |
| 20 | + |
| 21 | +from tests.common import MockConfigEntry |
| 22 | + |
| 23 | + |
| 24 | +@pytest.fixture(name="mock_store") |
| 25 | +def mock_store_fixture(): |
| 26 | + """Mock the storage.""" |
| 27 | + with patch("homeassistant.components.icloud.account.Store") as store_mock: |
| 28 | + store_instance = Mock(spec=Store) |
| 29 | + store_instance.path = "/mock/path" |
| 30 | + store_mock.return_value = store_instance |
| 31 | + yield store_instance |
| 32 | + |
| 33 | + |
| 34 | +@pytest.fixture(name="mock_icloud_service_no_userinfo") |
| 35 | +def mock_icloud_service_no_userinfo_fixture(): |
| 36 | + """Mock PyiCloudService with devices as dict but no userInfo.""" |
| 37 | + with patch( |
| 38 | + "homeassistant.components.icloud.account.PyiCloudService" |
| 39 | + ) as service_mock: |
| 40 | + service_instance = MagicMock() |
| 41 | + service_instance.requires_2fa = False |
| 42 | + mock_device = MagicMock() |
| 43 | + mock_device.status = iter(DEVICE) |
| 44 | + mock_device.user_info = None |
| 45 | + service_instance.devices = mock_device |
| 46 | + service_mock.return_value = service_instance |
| 47 | + yield service_instance |
| 48 | + |
| 49 | + |
| 50 | +async def test_setup_fails_when_userinfo_missing( |
| 51 | + hass: HomeAssistant, |
| 52 | + mock_store: Mock, |
| 53 | + mock_icloud_service_no_userinfo: MagicMock, |
| 54 | +) -> None: |
| 55 | + """Test setup fails when userInfo is missing from devices dict.""" |
| 56 | + |
| 57 | + assert mock_icloud_service_no_userinfo is not None |
| 58 | + |
| 59 | + config_entry = MockConfigEntry( |
| 60 | + domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=USERNAME |
| 61 | + ) |
| 62 | + config_entry.add_to_hass(hass) |
| 63 | + |
| 64 | + account = IcloudAccount( |
| 65 | + hass, |
| 66 | + MOCK_CONFIG[CONF_USERNAME], |
| 67 | + MOCK_CONFIG[CONF_PASSWORD], |
| 68 | + mock_store, |
| 69 | + MOCK_CONFIG[CONF_WITH_FAMILY], |
| 70 | + MOCK_CONFIG[CONF_MAX_INTERVAL], |
| 71 | + MOCK_CONFIG[CONF_GPS_ACCURACY_THRESHOLD], |
| 72 | + config_entry, |
| 73 | + ) |
| 74 | + |
| 75 | + with pytest.raises(ConfigEntryNotReady, match="No user info found"): |
| 76 | + account.setup() |
| 77 | + |
| 78 | + |
| 79 | +class MockAppleDevice: |
| 80 | + """Mock "Apple device" which implements the .status(...) method used by the account.""" |
| 81 | + |
| 82 | + def __init__(self, status_dict) -> None: |
| 83 | + """Set status.""" |
| 84 | + self._status = status_dict |
| 85 | + |
| 86 | + def status(self, key): |
| 87 | + """Return current status.""" |
| 88 | + return self._status |
| 89 | + |
| 90 | + def __getitem__(self, key): |
| 91 | + """Allow indexing the device itself (device[KEY]) to proxy into the raw status dict.""" |
| 92 | + return self._status.get(key) |
| 93 | + |
| 94 | + |
| 95 | +class MockDevicesContainer: |
| 96 | + """Mock devices container which is iterable and indexable returning device status dicts.""" |
| 97 | + |
| 98 | + def __init__(self, userinfo, devices) -> None: |
| 99 | + """Initialize with userinfo and list of device objects.""" |
| 100 | + self.user_info = userinfo |
| 101 | + self._devices = devices |
| 102 | + |
| 103 | + def __iter__(self): |
| 104 | + """Iterate returns device objects (each must have .status(...)).""" |
| 105 | + return iter(self._devices) |
| 106 | + |
| 107 | + def __len__(self): |
| 108 | + """Return number of devices.""" |
| 109 | + return len(self._devices) |
| 110 | + |
| 111 | + def __getitem__(self, idx): |
| 112 | + """Indexing returns device object (which must have .status(...)).""" |
| 113 | + dev = self._devices[idx] |
| 114 | + if hasattr(dev, "status"): |
| 115 | + return dev.status(None) |
| 116 | + return dev |
| 117 | + |
| 118 | + |
| 119 | +@pytest.fixture(name="mock_icloud_service") |
| 120 | +def mock_icloud_service_fixture(): |
| 121 | + """Mock PyiCloudService with devices container that is iterable and indexable returning status dict.""" |
| 122 | + with patch( |
| 123 | + "homeassistant.components.icloud.account.PyiCloudService", |
| 124 | + ) as service_mock: |
| 125 | + service_instance = MagicMock() |
| 126 | + device_obj = MockAppleDevice(DEVICE) |
| 127 | + devices_container = MockDevicesContainer(USER_INFO, [device_obj]) |
| 128 | + |
| 129 | + service_instance.devices = devices_container |
| 130 | + service_instance.requires_2fa = False |
| 131 | + |
| 132 | + service_mock.return_value = service_instance |
| 133 | + yield service_instance |
| 134 | + |
| 135 | + |
| 136 | +async def test_setup_success_with_devices( |
| 137 | + hass: HomeAssistant, |
| 138 | + mock_store: Mock, |
| 139 | + mock_icloud_service: MagicMock, |
| 140 | +) -> None: |
| 141 | + """Test successful setup with devices.""" |
| 142 | + |
| 143 | + assert mock_icloud_service is not None |
| 144 | + |
| 145 | + config_entry = MockConfigEntry( |
| 146 | + domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=USERNAME |
| 147 | + ) |
| 148 | + config_entry.add_to_hass(hass) |
| 149 | + |
| 150 | + account = IcloudAccount( |
| 151 | + hass, |
| 152 | + MOCK_CONFIG[CONF_USERNAME], |
| 153 | + MOCK_CONFIG[CONF_PASSWORD], |
| 154 | + mock_store, |
| 155 | + MOCK_CONFIG[CONF_WITH_FAMILY], |
| 156 | + MOCK_CONFIG[CONF_MAX_INTERVAL], |
| 157 | + MOCK_CONFIG[CONF_GPS_ACCURACY_THRESHOLD], |
| 158 | + config_entry, |
| 159 | + ) |
| 160 | + |
| 161 | + with patch.object(account, "_schedule_next_fetch"): |
| 162 | + account.setup() |
| 163 | + |
| 164 | + assert account.api is not None |
| 165 | + assert account.owner_fullname == "user name" |
| 166 | + assert "johntravolta" in account.family_members_fullname |
| 167 | + assert account.family_members_fullname["johntravolta"] == "John TRAVOLTA" |
0 commit comments