From 2e802c88f88f3602ea8cd9bab784509dcbd192d8 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 10 Mar 2020 11:42:04 +0100 Subject: [PATCH] Add devices check to iCloud config flow (#31950) * Add devices check to iCloud config flow * Some test rename * Bump pyicloud to catch KeyError --- .../components/icloud/.translations/en.json | 3 +- homeassistant/components/icloud/account.py | 8 +++- .../components/icloud/config_flow.py | 18 +++++++- homeassistant/components/icloud/manifest.json | 2 +- homeassistant/components/icloud/strings.json | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/icloud/test_config_flow.py | 41 +++++++++++++++---- 8 files changed, 64 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/icloud/.translations/en.json b/homeassistant/components/icloud/.translations/en.json index 3b7da70bcaf..73ca1b31256 100644 --- a/homeassistant/components/icloud/.translations/en.json +++ b/homeassistant/components/icloud/.translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Account already configured" + "already_configured": "Account already configured", + "no_device": "None of your devices have \"Find my iPhone\" activated" }, "error": { "login": "Login error: please check your email & password", diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 789ae563482..bb3742174d7 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -5,7 +5,11 @@ import operator from typing import Dict from pyicloud import PyiCloudService -from pyicloud.exceptions import PyiCloudFailedLoginException, PyiCloudNoDevicesException +from pyicloud.exceptions import ( + PyiCloudFailedLoginException, + PyiCloudNoDevicesException, + PyiCloudServiceNotActivatedException, +) from pyicloud.services.findmyiphone import AppleDevice from homeassistant.components.zone import async_active_zone @@ -109,7 +113,7 @@ class IcloudAccount: api_devices = self.api.devices # Gets device owners infos user_info = api_devices.response["userInfo"] - except (KeyError, PyiCloudNoDevicesException): + except (PyiCloudServiceNotActivatedException, PyiCloudNoDevicesException): _LOGGER.error("No iCloud device found") raise ConfigEntryNotReady diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index b3cb9c28181..72ff6e6481d 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -3,7 +3,12 @@ import logging import os from pyicloud import PyiCloudService -from pyicloud.exceptions import PyiCloudException, PyiCloudFailedLoginException +from pyicloud.exceptions import ( + PyiCloudException, + PyiCloudFailedLoginException, + PyiCloudNoDevicesException, + PyiCloudServiceNotActivatedException, +) import voluptuous as vol from homeassistant import config_entries @@ -101,6 +106,17 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self.api.requires_2sa: return await self.async_step_trusted_device() + try: + devices = await self.hass.async_add_executor_job( + getattr, self.api, "devices" + ) + if not devices: + raise PyiCloudNoDevicesException() + except (PyiCloudServiceNotActivatedException, PyiCloudNoDevicesException): + _LOGGER.error("No device found in the iCloud account: %s", self._username) + self.api = None + return self.async_abort(reason="no_device") + return self.async_create_entry( title=self._username, data={ diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 5b232cf1e62..76b6b9b39ae 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -3,7 +3,7 @@ "name": "Apple iCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/icloud", - "requirements": ["pyicloud==0.9.3"], + "requirements": ["pyicloud==0.9.4"], "dependencies": [], "codeowners": ["@Quentame"] } diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json index e0a7b7a32ce..f1931f7cb5c 100644 --- a/homeassistant/components/icloud/strings.json +++ b/homeassistant/components/icloud/strings.json @@ -31,7 +31,8 @@ "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again" }, "abort": { - "already_configured": "Account already configured" + "already_configured": "Account already configured", + "no_device": "None of your devices have \"Find my iPhone\" activated" } } } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index acfebe8c4d0..022d81aff49 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1312,7 +1312,7 @@ pyhomeworks==0.0.6 pyialarm==0.3 # homeassistant.components.icloud -pyicloud==0.9.3 +pyicloud==0.9.4 # homeassistant.components.intesishome pyintesishome==1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54ecf61d031..9bb76076b4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -480,7 +480,7 @@ pyheos==0.6.0 pyhomematic==0.1.65 # homeassistant.components.icloud -pyicloud==0.9.3 +pyicloud==0.9.4 # homeassistant.components.ipma pyipma==2.0.5 diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index 6091d1cf1da..646d62a09b8 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -46,8 +46,8 @@ def mock_controller_service(): yield service_mock -@pytest.fixture(name="service_with_cookie") -def mock_controller_service_with_cookie(): +@pytest.fixture(name="service_authenticated") +def mock_controller_service_authenticated(): """Mock a successful service while already authenticate.""" with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" @@ -59,6 +59,20 @@ def mock_controller_service_with_cookie(): yield service_mock +@pytest.fixture(name="service_authenticated_no_device") +def mock_controller_service_authenticated_no_device(): + """Mock a successful service while already authenticate, but without device.""" + with patch( + "homeassistant.components.icloud.config_flow.PyiCloudService" + ) as service_mock: + service_mock.return_value.requires_2sa = False + service_mock.return_value.trusted_devices = TRUSTED_DEVICES + service_mock.return_value.send_verification_code = Mock(return_value=True) + service_mock.return_value.validate_verification_code = Mock(return_value=True) + service_mock.return_value.devices = {} + yield service_mock + + @pytest.fixture(name="service_send_verification_code_failed") def mock_controller_service_send_verification_code_failed(): """Mock a failed service during sending verification code step.""" @@ -103,7 +117,7 @@ async def test_user(hass: HomeAssistantType, service: MagicMock): async def test_user_with_cookie( - hass: HomeAssistantType, service_with_cookie: MagicMock + hass: HomeAssistantType, service_authenticated: MagicMock ): """Test user config with presence of a cookie.""" # test with all provided @@ -148,7 +162,7 @@ async def test_import(hass: HomeAssistantType, service: MagicMock): async def test_import_with_cookie( - hass: HomeAssistantType, service_with_cookie: MagicMock + hass: HomeAssistantType, service_authenticated: MagicMock ): """Test import step with presence of a cookie.""" # import with username and password @@ -186,7 +200,7 @@ async def test_import_with_cookie( async def test_two_accounts_setup( - hass: HomeAssistantType, service_with_cookie: MagicMock + hass: HomeAssistantType, service_authenticated: MagicMock ): """Test to setup two accounts.""" MockConfigEntry( @@ -210,7 +224,7 @@ async def test_two_accounts_setup( assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD -async def test_abort_if_already_setup(hass: HomeAssistantType): +async def test_already_setup(hass: HomeAssistantType): """Test we abort if the account is already setup.""" MockConfigEntry( domain=DOMAIN, @@ -240,7 +254,7 @@ async def test_abort_if_already_setup(hass: HomeAssistantType): async def test_login_failed(hass: HomeAssistantType): """Test when we have errors during login.""" with patch( - "pyicloud.base.PyiCloudService.authenticate", + "homeassistant.components.icloud.config_flow.PyiCloudService.authenticate", side_effect=PyiCloudFailedLoginException(), ): result = await hass.config_entries.flow.async_init( @@ -252,6 +266,19 @@ async def test_login_failed(hass: HomeAssistantType): assert result["errors"] == {CONF_USERNAME: "login"} +async def test_no_device( + hass: HomeAssistantType, service_authenticated_no_device: MagicMock +): + """Test when we have no devices.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_device" + + async def test_trusted_device(hass: HomeAssistantType, service: MagicMock): """Test trusted_device step.""" result = await hass.config_entries.flow.async_init(