diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 763f6214185..6d9b70051f5 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -4,7 +4,6 @@ import logging from hass_nabucasa import Cloud import voluptuous as vol -from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.alexa import const as alexa_const from homeassistant.components.google_assistant import const as ga_c from homeassistant.const import ( @@ -186,19 +185,6 @@ async def async_setup(hass, config): prefs = CloudPreferences(hass) await prefs.async_initialize() - # Cloud user - user = None - if prefs.cloud_user: - # Fetch the user. It can happen that the user no longer exists if - # an image was restored without restoring the cloud prefs. - user = await hass.auth.async_get_user(prefs.cloud_user) - - if user is None: - user = await hass.auth.async_create_system_user( - "Home Assistant Cloud", [GROUP_ID_ADMIN] - ) - await prefs.async_update(cloud_user=user.id) - # Initialize Cloud websession = hass.helpers.aiohttp_client.async_get_clientsession() client = CloudClient(hass, prefs, websession, alexa_conf, google_conf) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index c7626777943..00acf930f86 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -7,7 +7,7 @@ import logging import aiohttp from hass_nabucasa.client import CloudClient as Interface -from homeassistant.core import callback +from homeassistant.core import callback, Context from homeassistant.components.google_assistant import smart_home as ga from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -44,7 +44,6 @@ class CloudClient(Interface): self.alexa_user_config = alexa_user_config self._alexa_config = None self._google_config = None - self.cloud = None @property def base_path(self) -> Path: @@ -92,23 +91,22 @@ class CloudClient(Interface): return self._alexa_config - @property - def google_config(self) -> google_config.CloudGoogleConfig: + async def get_google_config(self) -> google_config.CloudGoogleConfig: """Return Google config.""" if not self._google_config: assert self.cloud is not None + + cloud_user = await self._prefs.get_cloud_user() + self._google_config = google_config.CloudGoogleConfig( - self._hass, self.google_user_config, self._prefs, self.cloud + self._hass, self.google_user_config, cloud_user, self._prefs, self.cloud ) return self._google_config - async def async_initialize(self, cloud) -> None: - """Initialize the client.""" - self.cloud = cloud - - if not self.cloud.is_logged_in: - return + async def logged_in(self) -> None: + """When user logs in.""" + await self.prefs.async_set_username(self.cloud.username) if self.alexa_config.enabled and self.alexa_config.should_report_state: try: @@ -116,14 +114,18 @@ class CloudClient(Interface): except alexa_errors.NoTokenAvailable: pass - if self.google_config.enabled: - self.google_config.async_enable_local_sdk() + if self._prefs.google_enabled: + gconf = await self.get_google_config() - if self.google_config.should_report_state: - self.google_config.async_enable_report_state() + gconf.async_enable_local_sdk() + + if gconf.should_report_state: + gconf.async_enable_report_state() async def cleanups(self) -> None: """Cleanup some stuff after logout.""" + await self.prefs.async_set_username(None) + self._google_config = None @callback @@ -141,8 +143,13 @@ class CloudClient(Interface): async def async_alexa_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: """Process cloud alexa message to client.""" + cloud_user = await self._prefs.get_cloud_user() return await alexa_sh.async_handle_message( - self._hass, self.alexa_config, payload, enabled=self._prefs.alexa_enabled + self._hass, + self.alexa_config, + payload, + context=Context(user_id=cloud_user), + enabled=self._prefs.alexa_enabled, ) async def async_google_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: @@ -150,8 +157,10 @@ class CloudClient(Interface): if not self._prefs.google_enabled: return ga.turned_off_response(payload) + gconf = await self.get_google_config() + return await ga.async_handle_message( - self._hass, self.google_config, self.prefs.cloud_user, payload + self._hass, gconf, gconf.cloud_user, payload ) async def async_webhook_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 9a2dccf8d7c..406263c85f8 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -17,6 +17,7 @@ PREF_DISABLE_2FA = "disable_2fa" PREF_ALIASES = "aliases" PREF_SHOULD_EXPOSE = "should_expose" PREF_GOOGLE_LOCAL_WEBHOOK_ID = "google_local_webhook_id" +PREF_USERNAME = "username" DEFAULT_SHOULD_EXPOSE = True DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = False diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 80d3fedba5b..3d6511ffc3d 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -23,10 +23,11 @@ _LOGGER = logging.getLogger(__name__) class CloudGoogleConfig(AbstractConfig): """HA Cloud Configuration for Google Assistant.""" - def __init__(self, hass, config, prefs, cloud): + def __init__(self, hass, config, cloud_user, prefs, cloud): """Initialize the Google config.""" super().__init__(hass) self._config = config + self._user = cloud_user self._prefs = prefs self._cloud = cloud self._cur_entity_prefs = self._prefs.google_entity_configs @@ -46,7 +47,7 @@ class CloudGoogleConfig(AbstractConfig): @property def agent_user_id(self): """Return Agent User Id to use for query responses.""" - return self._cloud.claims["cognito:username"] + return self._cloud.username @property def entity_config(self): @@ -74,7 +75,12 @@ class CloudGoogleConfig(AbstractConfig): @property def local_sdk_user_id(self): """Return the user ID to be used for actions received via the local SDK.""" - return self._prefs.cloud_user + return self._user + + @property + def cloud_user(self): + """Return Cloud User account.""" + return self._user def should_expose(self, state): """If a state object should be exposed.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index f795bc2b68c..d808fe72d39 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -174,9 +174,8 @@ class GoogleActionsSyncView(HomeAssistantView): """Trigger a Google Actions sync.""" hass = request.app["hass"] cloud: Cloud = hass.data[DOMAIN] - status = await cloud.client.google_config.async_sync_entities( - cloud.client.google_config.agent_user_id - ) + gconf = await cloud.client.get_google_config() + status = await gconf.async_sync_entities(gconf.agent_user_id) return self.json({}, status_code=status) @@ -194,11 +193,7 @@ class CloudLoginView(HomeAssistantView): """Handle login request.""" hass = request.app["hass"] cloud = hass.data[DOMAIN] - - with async_timeout.timeout(REQUEST_TIMEOUT): - await hass.async_add_job(cloud.auth.login, data["email"], data["password"]) - - hass.async_add_job(cloud.iot.connect) + await cloud.login(data["email"], data["password"]) return self.json({"success": True}) @@ -479,7 +474,8 @@ async def websocket_remote_disconnect(hass, connection, msg): async def google_assistant_list(hass, connection, msg): """List all google assistant entities.""" cloud = hass.data[DOMAIN] - entities = google_helpers.async_get_entities(hass, cloud.client.google_config) + gconf = await cloud.client.get_google_config() + entities = google_helpers.async_get_entities(hass, gconf) result = [] diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 2feef55835e..ec9a556af0a 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.29"], + "requirements": ["hass-nabucasa==0.30"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 0599b00a8bd..e96ee9527fb 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -1,7 +1,10 @@ """Preference management for cloud.""" from ipaddress import ip_address +from typing import Optional from homeassistant.core import callback +from homeassistant.auth.models import User +from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.util.logging import async_create_catching_coro from .const import ( @@ -19,6 +22,7 @@ from .const import ( PREF_SHOULD_EXPOSE, PREF_ALEXA_ENTITY_CONFIGS, PREF_ALEXA_REPORT_STATE, + PREF_USERNAME, DEFAULT_ALEXA_REPORT_STATE, PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_LOCAL_WEBHOOK_ID, @@ -47,16 +51,7 @@ class CloudPreferences: prefs = await self._store.async_load() if prefs is None: - prefs = { - PREF_ENABLE_ALEXA: True, - PREF_ENABLE_GOOGLE: True, - PREF_ENABLE_REMOTE: False, - PREF_GOOGLE_SECURE_DEVICES_PIN: None, - PREF_GOOGLE_ENTITY_CONFIGS: {}, - PREF_ALEXA_ENTITY_CONFIGS: {}, - PREF_CLOUDHOOKS: {}, - PREF_CLOUD_USER: None, - } + prefs = self._empty_config("") self._prefs = prefs @@ -166,6 +161,27 @@ class CloudPreferences: updated_entities = {**entities, entity_id: updated_entity} await self.async_update(alexa_entity_configs=updated_entities) + async def async_set_username(self, username): + """Set the username that is logged in.""" + # Logging out. + if username is None: + user = await self._load_cloud_user() + + if user is not None: + await self._hass.auth.async_remove_user(user) + await self._save_prefs({**self._prefs, PREF_CLOUD_USER: None}) + return + + cur_username = self._prefs.get(PREF_USERNAME) + + if cur_username == username: + return + + if cur_username is None: + await self._save_prefs({**self._prefs, PREF_USERNAME: username}) + else: + await self._save_prefs(self._empty_config(username)) + def as_dict(self): """Return dictionary version.""" return { @@ -178,7 +194,6 @@ class CloudPreferences: PREF_ALEXA_REPORT_STATE: self.alexa_report_state, PREF_GOOGLE_REPORT_STATE: self.google_report_state, PREF_CLOUDHOOKS: self.cloudhooks, - PREF_CLOUD_USER: self.cloud_user, } @property @@ -239,10 +254,29 @@ class CloudPreferences: """Return the published cloud webhooks.""" return self._prefs.get(PREF_CLOUDHOOKS, {}) - @property - def cloud_user(self) -> str: + async def get_cloud_user(self) -> str: """Return ID from Home Assistant Cloud system user.""" - return self._prefs.get(PREF_CLOUD_USER) + user = await self._load_cloud_user() + + if user: + return user.id + + user = await self._hass.auth.async_create_system_user( + "Home Assistant Cloud", [GROUP_ID_ADMIN] + ) + await self.async_update(cloud_user=user.id) + return user.id + + async def _load_cloud_user(self) -> Optional[User]: + """Load cloud user if available.""" + user_id = self._prefs.get(PREF_CLOUD_USER) + + if user_id is None: + return None + + # Fetch the user. It can happen that the user no longer exists if + # an image was restored without restoring the cloud prefs. + return await self._hass.auth.async_get_user(user_id) @property def _has_local_trusted_network(self) -> bool: @@ -283,3 +317,19 @@ class CloudPreferences: for listener in self._listeners: self._hass.async_create_task(async_create_catching_coro(listener(self))) + + @callback + def _empty_config(self, username): + """Return an empty config.""" + return { + PREF_ENABLE_ALEXA: True, + PREF_ENABLE_GOOGLE: True, + PREF_ENABLE_REMOTE: False, + PREF_GOOGLE_SECURE_DEVICES_PIN: None, + PREF_GOOGLE_ENTITY_CONFIGS: {}, + PREF_ALEXA_ENTITY_CONFIGS: {}, + PREF_CLOUDHOOKS: {}, + PREF_CLOUD_USER: None, + PREF_USERNAME: username, + PREF_GOOGLE_LOCAL_WEBHOOK_ID: self._hass.components.webhook.async_generate_id(), + } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1edc5184c02..0e336835c4d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 -hass-nabucasa==0.29 +hass-nabucasa==0.30 home-assistant-frontend==20191119.6 importlib-metadata==0.23 jinja2>=2.10.3 diff --git a/requirements_all.txt b/requirements_all.txt index 7d0c08e6a28..20eb48e3bdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -629,7 +629,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.29 +hass-nabucasa==0.30 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d8d13ab8908..3b6b65d7e4d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -208,7 +208,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.29 +hass-nabucasa==0.30 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 054b38daffc..a9c4ade668d 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -7,6 +7,7 @@ import pytest from homeassistant.core import State from homeassistant.setup import async_setup_component from homeassistant.components.cloud import DOMAIN +from homeassistant.components.cloud.client import CloudClient from homeassistant.components.cloud.const import PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE from tests.components.alexa import test_smart_home as test_alexa from tests.common import mock_coro @@ -187,25 +188,42 @@ async def test_google_config_expose_entity(hass, mock_cloud_setup, mock_cloud_lo """Test Google config exposing entity method uses latest config.""" cloud_client = hass.data[DOMAIN].client state = State("light.kitchen", "on") + gconf = await cloud_client.get_google_config() - assert cloud_client.google_config.should_expose(state) + assert gconf.should_expose(state) await cloud_client.prefs.async_update_google_entity_config( entity_id="light.kitchen", should_expose=False ) - assert not cloud_client.google_config.should_expose(state) + assert not gconf.should_expose(state) async def test_google_config_should_2fa(hass, mock_cloud_setup, mock_cloud_login): """Test Google config disabling 2FA method uses latest config.""" cloud_client = hass.data[DOMAIN].client + gconf = await cloud_client.get_google_config() state = State("light.kitchen", "on") - assert cloud_client.google_config.should_2fa(state) + assert gconf.should_2fa(state) await cloud_client.prefs.async_update_google_entity_config( entity_id="light.kitchen", disable_2fa=True ) - assert not cloud_client.google_config.should_2fa(state) + assert not gconf.should_2fa(state) + + +async def test_set_username(hass): + """Test we set username during loggin.""" + prefs = MagicMock( + alexa_enabled=False, + google_enabled=False, + async_set_username=MagicMock(return_value=mock_coro()), + ) + client = CloudClient(hass, prefs, None, {}, {}) + client.cloud = MagicMock(is_logged_in=True, username="mock-username") + await client.logged_in() + + assert len(prefs.async_set_username.mock_calls) == 1 + assert prefs.async_set_username.mock_calls[0][1][0] == "mock-username" diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 7ccb6a33336..0284a2c3851 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -15,6 +15,7 @@ async def test_google_update_report_state(hass, cloud_prefs): config = CloudGoogleConfig( hass, GACTIONS_SCHEMA({}), + "mock-user-id", cloud_prefs, Mock(claims={"cognito:username": "abcdefghjkl"}), ) @@ -37,6 +38,7 @@ async def test_sync_entities(aioclient_mock, hass, cloud_prefs): config = CloudGoogleConfig( hass, GACTIONS_SCHEMA({}), + "mock-user-id", cloud_prefs, Mock( google_actions_sync_url="http://example.com", @@ -52,6 +54,7 @@ async def test_google_update_expose_trigger_sync(hass, cloud_prefs): config = CloudGoogleConfig( hass, GACTIONS_SCHEMA({}), + "mock-user-id", cloud_prefs, Mock(claims={"cognito:username": "abcdefghjkl"}), ) @@ -90,7 +93,7 @@ async def test_google_update_expose_trigger_sync(hass, cloud_prefs): async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): """Test Google config responds to entity registry.""" config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] ) with patch.object( diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 22d4ddd172e..440ad7a9c89 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -103,32 +103,18 @@ async def test_google_actions_sync_fails( assert req.status == 403 -async def test_login_view(hass, cloud_client, mock_cognito): +async def test_login_view(hass, cloud_client): """Test logging in.""" - mock_cognito.id_token = jwt.encode( - {"email": "hello@home-assistant.io", "custom:sub-exp": "2018-01-03"}, "test" - ) - mock_cognito.access_token = "access_token" - mock_cognito.refresh_token = "refresh_token" + hass.data["cloud"] = MagicMock(login=MagicMock(return_value=mock_coro())) - with patch("hass_nabucasa.iot.CloudIoT.connect") as mock_connect, patch( - "hass_nabucasa.auth.CognitoAuth._authenticate", return_value=mock_cognito - ) as mock_auth: - req = await cloud_client.post( - "/api/cloud/login", json={"email": "my_username", "password": "my_password"} - ) + req = await cloud_client.post( + "/api/cloud/login", json={"email": "my_username", "password": "my_password"} + ) assert req.status == 200 result = await req.json() assert result == {"success": True} - assert len(mock_connect.mock_calls) == 1 - - assert len(mock_auth.mock_calls) == 1 - result_user, result_pass = mock_auth.mock_calls[0][1] - assert result_user == "my_username" - assert result_pass == "my_password" - async def test_login_view_random_exception(cloud_client): """Try logging in with invalid JSON.""" @@ -351,7 +337,6 @@ async def test_websocket_status( "cloud": "connected", "prefs": { "alexa_enabled": True, - "cloud_user": None, "cloudhooks": {}, "google_enabled": True, "google_entity_configs": {}, diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index e160ea8826a..d039cdd1b0b 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -5,7 +5,6 @@ import pytest from homeassistant.core import Context from homeassistant.exceptions import Unauthorized -from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import cloud from homeassistant.components.cloud.const import DOMAIN from homeassistant.components.cloud.prefs import STORAGE_KEY @@ -142,68 +141,11 @@ async def test_setup_existing_cloud_user(hass, hass_storage): assert hass_storage[STORAGE_KEY]["data"]["cloud_user"] == user.id -async def test_setup_invalid_cloud_user(hass, hass_storage): - """Test setup with API push default data.""" - hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": "non-existing"}} - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): - result = await async_setup_component( - hass, - "cloud", - { - "http": {}, - "cloud": { - cloud.CONF_MODE: cloud.MODE_DEV, - "cognito_client_id": "test-cognito_client_id", - "user_pool_id": "test-user_pool_id", - "region": "test-region", - "relayer": "test-relayer", - }, - }, - ) - assert result - - assert hass_storage[STORAGE_KEY]["data"]["cloud_user"] != "non-existing" - cloud_user = await hass.auth.async_get_user( - hass_storage[STORAGE_KEY]["data"]["cloud_user"] - ) - - assert cloud_user - assert cloud_user.groups[0].id == GROUP_ID_ADMIN - - -async def test_setup_setup_cloud_user(hass, hass_storage): - """Test setup with API push default data.""" - hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": None}} - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): - result = await async_setup_component( - hass, - "cloud", - { - "http": {}, - "cloud": { - cloud.CONF_MODE: cloud.MODE_DEV, - "cognito_client_id": "test-cognito_client_id", - "user_pool_id": "test-user_pool_id", - "region": "test-region", - "relayer": "test-relayer", - }, - }, - ) - assert result - - cloud_user = await hass.auth.async_get_user( - hass_storage[STORAGE_KEY]["data"]["cloud_user"] - ) - - assert cloud_user - assert cloud_user.groups[0].id == GROUP_ID_ADMIN - - async def test_on_connect(hass, mock_cloud_fixture): """Test cloud on connect triggers.""" cl = hass.data["cloud"] - assert len(cl.iot._on_connect) == 4 + assert len(cl.iot._on_connect) == 3 assert len(hass.states.async_entity_ids("binary_sensor")) == 0 diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py new file mode 100644 index 00000000000..1678757e52c --- /dev/null +++ b/tests/components/cloud/test_prefs.py @@ -0,0 +1,80 @@ +"""Test Cloud preferences.""" +from unittest.mock import patch + +from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.components.cloud.prefs import CloudPreferences, STORAGE_KEY + + +async def test_set_username(hass): + """Test we clear config if we set different username.""" + prefs = CloudPreferences(hass) + await prefs.async_initialize() + + assert prefs.google_enabled + + await prefs.async_update(google_enabled=False) + + assert not prefs.google_enabled + + await prefs.async_set_username("new-username") + + assert prefs.google_enabled + + +async def test_set_username_migration(hass): + """Test we not clear config if we had no username.""" + prefs = CloudPreferences(hass) + + with patch.object(prefs, "_empty_config", return_value=prefs._empty_config(None)): + await prefs.async_initialize() + + assert prefs.google_enabled + + await prefs.async_update(google_enabled=False) + + assert not prefs.google_enabled + + await prefs.async_set_username("new-username") + + assert not prefs.google_enabled + + +async def test_load_invalid_cloud_user(hass, hass_storage): + """Test loading cloud user with invalid storage.""" + hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": "non-existing"}} + + prefs = CloudPreferences(hass) + await prefs.async_initialize() + + cloud_user_id = await prefs.get_cloud_user() + + assert cloud_user_id != "non-existing" + + cloud_user = await hass.auth.async_get_user( + hass_storage[STORAGE_KEY]["data"]["cloud_user"] + ) + + assert cloud_user + assert cloud_user.groups[0].id == GROUP_ID_ADMIN + + +async def test_setup_remove_cloud_user(hass, hass_storage): + """Test creating and removing cloud user.""" + hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": None}} + + prefs = CloudPreferences(hass) + await prefs.async_initialize() + await prefs.async_set_username("user1") + + cloud_user = await hass.auth.async_get_user(await prefs.get_cloud_user()) + + assert cloud_user + assert cloud_user.groups[0].id == GROUP_ID_ADMIN + + await prefs.async_set_username("user2") + + cloud_user2 = await hass.auth.async_get_user(await prefs.get_cloud_user()) + + assert cloud_user2 + assert cloud_user2.groups[0].id == GROUP_ID_ADMIN + assert cloud_user2.id != cloud_user.id