diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 62e1c63129a..7889fafcc9e 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -54,6 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: XboxConfigEntry) -> bool await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await async_migrate_unique_id(hass, entry) return True @@ -61,3 +62,28 @@ async def async_unload_entry(hass: HomeAssistant, entry: XboxConfigEntry) -> boo """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_migrate_unique_id(hass: HomeAssistant, entry: XboxConfigEntry) -> bool: + """Migrate config entry. + + Migration requires runtime data + """ + + if entry.version == 1 and entry.minor_version < 2: + # Migrate unique_id from `xbox` to account xuid and + # change generic entry name to user's gamertag + return hass.config_entries.async_update_entry( + entry, + unique_id=entry.runtime_data.client.xuid, + title=( + entry.runtime_data.data.presence[ + entry.runtime_data.client.xuid + ].gamertag + if entry.title == "Home Assistant Cloud" + else entry.title + ), + minor_version=2, + ) + + return True diff --git a/homeassistant/components/xbox/config_flow.py b/homeassistant/components/xbox/config_flow.py index 86157be5d7f..f50be700a3b 100644 --- a/homeassistant/components/xbox/config_flow.py +++ b/homeassistant/components/xbox/config_flow.py @@ -3,6 +3,11 @@ import logging from typing import Any +from xbox.webapi.api.client import XboxLiveClient +from xbox.webapi.authentication.manager import AuthenticationManager +from xbox.webapi.authentication.models import OAuth2TokenResponse +from xbox.webapi.common.signed_session import SignedSession + from homeassistant.config_entries import ConfigFlowResult from homeassistant.helpers import config_entry_oauth2_flow @@ -16,6 +21,8 @@ class OAuth2FlowHandler( DOMAIN = DOMAIN + MINOR_VERSION = 2 + @property def logger(self) -> logging.Logger: """Return logger.""" @@ -31,9 +38,23 @@ class OAuth2FlowHandler( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a flow start.""" - await self.async_set_unique_id(DOMAIN) if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") return await super().async_step_user(user_input) + + async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: + """Create an entry for the flow.""" + + async with SignedSession() as session: + auth = AuthenticationManager(session, "", "", "") + auth.oauth = OAuth2TokenResponse(**data["token"]) + await auth.refresh_tokens() + + client = XboxLiveClient(auth) + + me = await client.people.get_friends_own_batch([client.xuid]) + + await self.async_set_unique_id(client.xuid) + return self.async_create_entry(title=me.people[0].gamertag, data=data) diff --git a/tests/components/xbox/conftest.py b/tests/components/xbox/conftest.py index 090d8d49f2f..e6ad579e0c2 100644 --- a/tests/components/xbox/conftest.py +++ b/tests/components/xbox/conftest.py @@ -65,17 +65,36 @@ def mock_config_entry() -> MockConfigEntry: "user_id": "AAAAAAAAAAAAAAAAAAAAA", }, }, - unique_id="xbox", ) +@pytest.fixture(name="authentication_manager") +def mock_authentication_manager() -> Generator[AsyncMock]: + """Mock xbox-webapi AuthenticationManager.""" + + with ( + patch( + "homeassistant.components.xbox.config_flow.AuthenticationManager", + autospec=True, + ) as mock_client, + ): + client = mock_client.return_value + + yield client + + @pytest.fixture(name="signed_session") def mock_signed_session() -> Generator[AsyncMock]: """Mock xbox-webapi SignedSession.""" - with patch( - "homeassistant.components.xbox.SignedSession", autospec=True - ) as mock_client: + with ( + patch( + "homeassistant.components.xbox.SignedSession", autospec=True + ) as mock_client, + patch( + "homeassistant.components.xbox.config_flow.SignedSession", new=mock_client + ), + ): client = mock_client.return_value yield client @@ -85,9 +104,14 @@ def mock_signed_session() -> Generator[AsyncMock]: def mock_xbox_live_client(signed_session) -> Generator[AsyncMock]: """Mock xbox-webapi XboxLiveClient.""" - with patch( - "homeassistant.components.xbox.XboxLiveClient", autospec=True - ) as mock_client: + with ( + patch( + "homeassistant.components.xbox.XboxLiveClient", autospec=True + ) as mock_client, + patch( + "homeassistant.components.xbox.config_flow.XboxLiveClient", new=mock_client + ), + ): client = mock_client.return_value client.smartglass = AsyncMock() @@ -110,4 +134,7 @@ def mock_xbox_live_client(signed_session) -> Generator[AsyncMock]: client.people.get_friends_own.return_value = PeopleResponse( **load_json_object_fixture("people_friends_own.json", DOMAIN) ) + + client.xuid = "271958441785640" + yield client diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index 533b2359ad3..66c92d7e807 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -29,7 +29,11 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: assert result["reason"] == "single_instance_allowed" -@pytest.mark.usefixtures("current_request_with_host") +@pytest.mark.usefixtures( + "current_request_with_host", + "xbox_live_client", + "authentication_manager", +) async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, @@ -68,13 +72,57 @@ async def test_full_flow( "access_token": "mock-access-token", "type": "Bearer", "expires_in": 60, + "scope": "XboxLive.signin XboxLive.offline_access", + "service": "xbox", + "token_type": "bearer", + "user_id": "AAAAAAAAAAAAAAAAAAAAA", }, ) with patch( "homeassistant.components.xbox.async_setup_entry", return_value=True ) as mock_setup: - await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["result"].unique_id == "271958441785640" + assert result["result"].title == "GSR Ae" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 + + +@pytest.mark.usefixtures("xbox_live_client") +async def test_unique_id_migration( + hass: HomeAssistant, +) -> None: + """Test config entry unique_id migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Home Assistant Cloud", + data={ + "auth_implementation": "cloud", + "token": { + "access_token": "1234567890", + "expires_at": 1760697327.7298331, + "expires_in": 3600, + "refresh_token": "0987654321", + "scope": "XboxLive.signin XboxLive.offline_access", + "service": "xbox", + "token_type": "bearer", + "user_id": "AAAAAAAAAAAAAAAAAAAAA", + }, + }, + unique_id=DOMAIN, + version=1, + minor_version=1, + ) + + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is config_entries.ConfigEntryState.LOADED + assert config_entry.version == 1 + assert config_entry.minor_version == 2 + assert config_entry.unique_id == "271958441785640" + assert config_entry.title == "GSR Ae"