1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Increase test coverage in Xbox integration (#162876)

This commit is contained in:
Manu
2026-02-13 00:14:07 +01:00
committed by GitHub
parent 1667b3f16b
commit a6287731f7
14 changed files with 641 additions and 79 deletions

View File

@@ -168,36 +168,40 @@ class XboxConsoleStatusCoordinator(XboxBaseCoordinator[dict[str, ConsoleData]]):
_LOGGER.debug("%s status: %s", console.name, status.model_dump()) _LOGGER.debug("%s status: %s", console.name, status.model_dump())
# Setup focus app # Setup focus app
app_details: Product | None = None app_details = (
if (current_state := self.data.get(console.id)) is not None: current_state.app_details
app_details = current_state.app_details if (current_state := self.data.get(console.id)) is not None
and status.focus_app_aumid
else None
)
if status.focus_app_aumid: if status.focus_app_aumid and (
if ( not current_state
not current_state or status.focus_app_aumid != current_state.status.focus_app_aumid
or status.focus_app_aumid != current_state.status.focus_app_aumid ):
): catalog_result = (
app_id = status.focus_app_aumid.split("!")[0] await self.client.catalog.get_product_from_alternate_id(
id_type = AlternateIdType.PACKAGE_FAMILY_NAME *self._resolve_app_id(status.focus_app_aumid)
if app_id in SYSTEM_PFN_ID_MAP:
id_type = AlternateIdType.LEGACY_XBOX_PRODUCT_ID
app_id = SYSTEM_PFN_ID_MAP[app_id][id_type]
catalog_result = (
await self.client.catalog.get_product_from_alternate_id(
app_id, id_type
)
) )
)
if catalog_result.products: if catalog_result.products:
app_details = catalog_result.products[0] app_details = catalog_result.products[0]
else:
app_details = None
data[console.id] = ConsoleData(status=status, app_details=app_details) data[console.id] = ConsoleData(status=status, app_details=app_details)
return data return data
def _resolve_app_id(self, focus_app_aumid: str) -> tuple[str, AlternateIdType]:
app_id = focus_app_aumid.split("!", maxsplit=1)[0]
id_type = AlternateIdType.PACKAGE_FAMILY_NAME
if app_id in SYSTEM_PFN_ID_MAP:
id_type = AlternateIdType.LEGACY_XBOX_PRODUCT_ID
app_id = SYSTEM_PFN_ID_MAP[app_id][id_type]
return app_id, id_type
class XboxPresenceCoordinator(XboxBaseCoordinator[XboxData]): class XboxPresenceCoordinator(XboxBaseCoordinator[XboxData]):
"""Update list of Xbox consoles.""" """Update list of Xbox consoles."""

View File

@@ -148,10 +148,12 @@ class XboxMediaPlayer(XboxConsoleBaseEntity, MediaPlayerEntity):
@property @property
def media_content_type(self) -> MediaType: def media_content_type(self) -> MediaType:
"""Media content type.""" """Media content type."""
app_details = self.data.app_details
if app_details and app_details.product_family == "Games": return (
return MediaType.GAME MediaType.GAME
return MediaType.APP if self.data.app_details and self.data.app_details.product_family == "Games"
else MediaType.APP
)
@property @property
def media_content_id(self) -> str | None: def media_content_id(self) -> str | None:
@@ -161,11 +163,13 @@ class XboxMediaPlayer(XboxConsoleBaseEntity, MediaPlayerEntity):
@property @property
def media_title(self) -> str | None: def media_title(self) -> str | None:
"""Title of current playing media.""" """Title of current playing media."""
if not (app_details := self.data.app_details):
return None
return ( return (
app_details.localized_properties[0].product_title (
or app_details.localized_properties[0].short_title app_details.localized_properties[0].product_title
or app_details.localized_properties[0].short_title
)
if (app_details := self.data.app_details)
else None
) )
@property @property

View File

@@ -0,0 +1,37 @@
{
"BigIds": ["9WZDNCRFJ3TJ"],
"HasMorePages": false,
"Products": [
{
"LocalizedProperties": [
{
"Images": [
{
"FileId": "2000000000037288315",
"EISListingIdentifier": null,
"BackgroundColor": "",
"Caption": "",
"FileSizeInBytes": 2514491,
"ForegroundColor": "",
"Height": 1080,
"ImagePositionInfo": "",
"ImagePurpose": "FeaturePromotionalSquareArt",
"UnscaledImageSHA256Hash": "i7GhC2HdYXf69+X/mLHtm8B36zR1gEOfuG2g2bXAAnY=",
"Uri": "//store-images.s-microsoft.com/image/apps.45451.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.3abf2cc3-00cc-417d-a93d-97110cdfb261",
"Width": 1080
}
],
"ProductTitle": "Blue Dragon"
}
],
"MarketProperties": [],
"ProductBSchema": "ProductGame;1",
"ProductId": "C2HGK9J5367F",
"PartD": "",
"ProductFamily": "Games",
"ProductKind": "Game",
"DisplaySkuAvailabilities": []
}
],
"TotalResultCount": 1
}

View File

@@ -0,0 +1,68 @@
{
"BigIds": ["9VWGNH0VBZJX"],
"HasMorePages": false,
"Products": [
{
"LastModifiedDate": "2014-10-21T17:30:01.0000000+00:00",
"LocalizedProperties": [
{
"Images": [
{
"FileSizeInBytes": 0,
"Height": 1080,
"ImagePositionInfo": "Xbox",
"ImagePurpose": "FeaturePromotionalSquareArt",
"SortOrder": "0",
"Uri": "https://images-eds-ssl.xboxlive.com/image?url=8Oaj9Ryq1G1_p3lLnXlsaZgGzAie6Mnu24_PawYuDYIoH77pJ.X5Z.MqQPibUVTcbx57bBxf63xu2Ef8acP3S7Uz80NbHc5nza..4R00GT1V5G760cdfX7Hl0uIHdHCbkzTikdvNE0TedhKgQfQy.2gjOGbd8kXZXzy4VzeJiNPLhLq2QUQbo8q3sVoSPaw73J4BxM7gaNX8V8qLcWtO5sn6vgbTso51OaEIn4zeAiw-",
"Width": 1080
}
],
"Language": "en-US",
"PublisherName": "Microsoft",
"SearchTitles": [
{ "SearchTitleString": "TV", "SearchTitleType": "SearchHint" }
],
"ShortTitle": "TV",
"SortTitle": "TV",
"Videos": [],
"VoiceTitle": "TV",
"ProductDescription": "",
"ProductTitle": "TV",
"Markets": []
}
],
"MarketProperties": [],
"ProductASchema": "Product;3",
"ProductBSchema": "ProductUnifiedApp;3",
"ProductId": "9VWGNH0VBZJX",
"Properties": {
"AllowedUrls": [],
"Categories": ["Video"],
"PublisherId": "",
"SkuDisplayGroups": [],
"RevisionId": "2014-10-21T17:30:01.0000000+00:00"
},
"AlternateIds": [
{
"IdType": "LegacyXboxProductId",
"Value": "71e7df12-89e0-4dc7-a5ff-a182fc2df94f"
},
{ "IdType": "XboxTitleId", "Value": "371594669" }
],
"DomainDataVersion": "",
"IngestionSource": "Bingbox App",
"IsMicrosoftProduct": false,
"ProductType": "Application",
"ValidationData": null,
"MerchandizingTags": null,
"SandboxId": "RETAIL",
"ProductFamily": "Apps",
"SchemaVersion": "1",
"IsSandboxedProduct": true,
"ProductKind": "Application",
"ProductPolicies": {},
"DisplaySkuAvailabilities": []
}
],
"TotalResultCount": 2
}

View File

@@ -118,7 +118,7 @@
}, },
{ {
"xuid": "2533274913657542", "xuid": "2533274913657542",
"isFavorite": true, "isFavorite": false,
"isFollowingCaller": true, "isFollowingCaller": true,
"isFollowedByCaller": true, "isFollowedByCaller": true,
"isIdentityShared": false, "isIdentityShared": false,

View File

@@ -0,0 +1,14 @@
{
"status": {
"errorCode": "OK",
"errorMessage": null
},
"powerState": "On",
"playbackState": "Stopped",
"loginState": null,
"focusAppAumid": "",
"isTvConfigured": true,
"digitalAssistantRemoteControlEnabled": true,
"consoleStreamingEnabled": false,
"remoteManagementEnabled": true
}

View File

@@ -0,0 +1,14 @@
{
"status": {
"errorCode": "OK",
"errorMessage": null
},
"powerState": "On",
"playbackState": "Stopped",
"loginState": null,
"focusAppAumid": "Microsoft.Xbox.LiveTV_8wekyb3d8bbwe!Microsoft.Xbox.LiveTV.Application",
"isTvConfigured": true,
"digitalAssistantRemoteControlEnabled": true,
"consoleStreamingEnabled": false,
"remoteManagementEnabled": true
}

View File

@@ -9588,7 +9588,7 @@
'gamertag': '**REDACTED**', 'gamertag': '**REDACTED**',
'is_broadcasting': False, 'is_broadcasting': False,
'is_cloaked': None, 'is_cloaked': None,
'is_favorite': True, 'is_favorite': False,
'is_followed_by_caller': True, 'is_followed_by_caller': True,
'is_following_caller': True, 'is_following_caller': True,
'is_friend': True, 'is_friend': True,

View File

@@ -124,7 +124,7 @@
'title': 'Installed Applications', 'title': 'Installed Applications',
}) })
# --- # ---
# name: test_media_players[media_player.xone-entry] # name: test_media_players[app][media_player.xone-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@@ -161,7 +161,7 @@
'unit_of_measurement': None, 'unit_of_measurement': None,
}) })
# --- # ---
# name: test_media_players[media_player.xone-state] # name: test_media_players[app][media_player.xone-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'entity_picture': 'https://store-images.s-microsoft.com/image/apps.9815.9007199266246365.7dc5d343-fe4a-40c3-93dd-c78e77f97331.45eebdef-f725-4799-bbf8-9ad8391a8279', 'entity_picture': 'https://store-images.s-microsoft.com/image/apps.9815.9007199266246365.7dc5d343-fe4a-40c3-93dd-c78e77f97331.45eebdef-f725-4799-bbf8-9ad8391a8279',
@@ -180,7 +180,7 @@
'state': 'on', 'state': 'on',
}) })
# --- # ---
# name: test_media_players[media_player.xonex-entry] # name: test_media_players[app][media_player.xonex-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@@ -217,7 +217,7 @@
'unit_of_measurement': None, 'unit_of_measurement': None,
}) })
# --- # ---
# name: test_media_players[media_player.xonex-state] # name: test_media_players[app][media_player.xonex-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'entity_picture': 'https://store-images.s-microsoft.com/image/apps.9815.9007199266246365.7dc5d343-fe4a-40c3-93dd-c78e77f97331.45eebdef-f725-4799-bbf8-9ad8391a8279', 'entity_picture': 'https://store-images.s-microsoft.com/image/apps.9815.9007199266246365.7dc5d343-fe4a-40c3-93dd-c78e77f97331.45eebdef-f725-4799-bbf8-9ad8391a8279',
@@ -236,3 +236,221 @@
'state': 'on', 'state': 'on',
}) })
# --- # ---
# name: test_media_players[idle][media_player.xone-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.xone',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'xbox',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 149385>,
'translation_key': 'xbox',
'unique_id': 'HIJKLMN',
'unit_of_measurement': None,
})
# ---
# name: test_media_players[idle][media_player.xone-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'entity_picture_local': None,
'friendly_name': 'XONE',
'media_content_type': <MediaType.APP: 'app'>,
'supported_features': <MediaPlayerEntityFeature: 149385>,
}),
'context': <ANY>,
'entity_id': 'media_player.xone',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_media_players[idle][media_player.xonex-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.xonex',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'xbox',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 149385>,
'translation_key': 'xbox',
'unique_id': 'ABCDEFG',
'unit_of_measurement': None,
})
# ---
# name: test_media_players[idle][media_player.xonex-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'entity_picture_local': None,
'friendly_name': 'XONEX',
'media_content_type': <MediaType.APP: 'app'>,
'supported_features': <MediaPlayerEntityFeature: 149385>,
}),
'context': <ANY>,
'entity_id': 'media_player.xonex',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_media_players[livetvapp][media_player.xone-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.xone',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'xbox',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 149385>,
'translation_key': 'xbox',
'unique_id': 'HIJKLMN',
'unit_of_measurement': None,
})
# ---
# name: test_media_players[livetvapp][media_player.xone-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'entity_picture': 'https://images-eds-ssl.xboxlive.com/image?url=8Oaj9Ryq1G1_p3lLnXlsaZgGzAie6Mnu24_PawYuDYIoH77pJ.X5Z.MqQPibUVTcbx57bBxf63xu2Ef8acP3S7Uz80NbHc5nza..4R00GT1V5G760cdfX7Hl0uIHdHCbkzTikdvNE0TedhKgQfQy.2gjOGbd8kXZXzy4VzeJiNPLhLq2QUQbo8q3sVoSPaw73J4BxM7gaNX8V8qLcWtO5sn6vgbTso51OaEIn4zeAiw-',
'entity_picture_local': '/api/media_player_proxy/media_player.xone?token=mock_token&cache=cf419ddd9fb966d6',
'friendly_name': 'XONE',
'media_content_id': '9VWGNH0VBZJX',
'media_content_type': <MediaType.APP: 'app'>,
'media_title': 'TV',
'supported_features': <MediaPlayerEntityFeature: 149385>,
}),
'context': <ANY>,
'entity_id': 'media_player.xone',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_media_players[livetvapp][media_player.xonex-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.xonex',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'xbox',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 149385>,
'translation_key': 'xbox',
'unique_id': 'ABCDEFG',
'unit_of_measurement': None,
})
# ---
# name: test_media_players[livetvapp][media_player.xonex-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'entity_picture': 'https://images-eds-ssl.xboxlive.com/image?url=8Oaj9Ryq1G1_p3lLnXlsaZgGzAie6Mnu24_PawYuDYIoH77pJ.X5Z.MqQPibUVTcbx57bBxf63xu2Ef8acP3S7Uz80NbHc5nza..4R00GT1V5G760cdfX7Hl0uIHdHCbkzTikdvNE0TedhKgQfQy.2gjOGbd8kXZXzy4VzeJiNPLhLq2QUQbo8q3sVoSPaw73J4BxM7gaNX8V8qLcWtO5sn6vgbTso51OaEIn4zeAiw-',
'entity_picture_local': '/api/media_player_proxy/media_player.xonex?token=mock_token&cache=cf419ddd9fb966d6',
'friendly_name': 'XONEX',
'media_content_id': '9VWGNH0VBZJX',
'media_content_type': <MediaType.APP: 'app'>,
'media_title': 'TV',
'supported_features': <MediaPlayerEntityFeature: 149385>,
}),
'context': <ANY>,
'entity_id': 'media_player.xonex',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@@ -2,8 +2,9 @@
from http import HTTPStatus from http import HTTPStatus
from typing import Any from typing import Any
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, Mock, patch
from httpx import HTTPStatusError, RequestError, TimeoutException
import pytest import pytest
from pythonxbox.api.provider.people.models import PeopleResponse from pythonxbox.api.provider.people.models import PeopleResponse
@@ -22,7 +23,10 @@ from homeassistant.config_entries import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo
@@ -576,7 +580,10 @@ async def test_add_friend_flow_config_entry_not_loaded(
@pytest.mark.usefixtures("xbox_live_client", "authentication_manager") @pytest.mark.usefixtures("xbox_live_client", "authentication_manager")
async def test_unique_id_and_friends_migration(hass: HomeAssistant) -> None: async def test_unique_id_and_friends_migration(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test config entry unique_id migration and favorite to subentry migration.""" """Test config entry unique_id migration and favorite to subentry migration."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@@ -601,6 +608,17 @@ async def test_unique_id_and_friends_migration(hass: HomeAssistant) -> None:
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
device_own = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, "xbox_live")},
)
device_friend = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, "2533274838782903")},
)
assert device_friend.config_entries_subentries[config_entry.entry_id] == {None}
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@@ -611,14 +629,111 @@ async def test_unique_id_and_friends_migration(hass: HomeAssistant) -> None:
assert config_entry.title == "GSR Ae" assert config_entry.title == "GSR Ae"
# Assert favorite friends migrated to subentries # Assert favorite friends migrated to subentries
assert len(config_entry.subentries) == 2 assert len(config_entry.subentries) == 1
subentries = list(config_entry.subentries.values()) subentries = list(config_entry.subentries.values())
assert subentries[0].unique_id == "2533274838782903" assert subentries[0].unique_id == "2533274838782903"
assert subentries[0].title == "Ikken Hissatsuu" assert subentries[0].title == "Ikken Hissatsuu"
assert subentries[0].subentry_type == "friend" assert subentries[0].subentry_type == "friend"
assert subentries[1].unique_id == "2533274913657542"
assert subentries[1].title == "erics273" ## Assert devices have been migrated
assert subentries[1].subentry_type == "friend" assert (device_own := device_registry.async_get(device_own.id))
assert device_own.identifiers == {(DOMAIN, "271958441785640")}
assert (device_friend := device_registry.async_get(device_friend.id))
assert device_friend.config_entries_subentries[config_entry.entry_id] == {
subentries[0].subentry_id
}
@pytest.mark.parametrize(
("provider", "method"),
[
("people", "get_friends_by_xuid"),
("people", "get_friends_own"),
],
)
@pytest.mark.parametrize(
"exception",
[
TimeoutException(""),
RequestError("", request=Mock()),
HTTPStatusError("", request=Mock(), response=Mock()),
],
)
@pytest.mark.usefixtures("authentication_manager")
async def test_migration_exceptions(
hass: HomeAssistant,
xbox_live_client: AsyncMock,
provider: str,
method: str,
exception: Exception,
) -> None:
"""Test exceptions during 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,
)
provider = getattr(xbox_live_client, provider)
getattr(provider, method).side_effect = exception
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.MIGRATION_ERROR
@pytest.mark.usefixtures("xbox_live_client", "authentication_manager")
async def test_migration_implementation_unavailable(hass: HomeAssistant) -> None:
"""Test implementation unavailable exception during 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)
with patch(
"homeassistant.components.xbox.async_get_config_entry_implementation",
side_effect=ImplementationUnavailableError,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR
@pytest.mark.usefixtures( @pytest.mark.usefixtures(

View File

@@ -1,7 +1,7 @@
"""Tests for the Xbox integration.""" """Tests for the Xbox integration."""
from datetime import timedelta from datetime import timedelta
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, Mock, patch
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
from httpx import ConnectTimeout, HTTPStatusError, ProtocolError from httpx import ConnectTimeout, HTTPStatusError, ProtocolError
@@ -42,7 +42,11 @@ async def test_entry_setup_unload(
@pytest.mark.parametrize( @pytest.mark.parametrize(
"exception", "exception",
[ConnectTimeout, HTTPStatusError, ProtocolError], [
ConnectTimeout(""),
HTTPStatusError("", request=Mock(), response=Mock()),
ProtocolError(""),
],
) )
async def test_config_entry_not_ready( async def test_config_entry_not_ready(
hass: HomeAssistant, hass: HomeAssistant,
@@ -78,7 +82,14 @@ async def test_config_implementation_not_available(
assert config_entry.state is ConfigEntryState.SETUP_RETRY assert config_entry.state is ConfigEntryState.SETUP_RETRY
@pytest.mark.parametrize("exception", [ConnectTimeout, HTTPStatusError, ProtocolError]) @pytest.mark.parametrize(
"exception",
[
ConnectTimeout(""),
HTTPStatusError("", request=Mock(), response=Mock()),
ProtocolError(""),
],
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
("provider", "method"), ("provider", "method"),
[ [

View File

@@ -7,6 +7,7 @@ from unittest.mock import patch
from httpx import HTTPStatusError, RequestError, TimeoutException from httpx import HTTPStatusError, RequestError, TimeoutException
import pytest import pytest
from pythonxbox.api.provider.catalog.models import CatalogResponse
from pythonxbox.api.provider.smartglass.models import ( from pythonxbox.api.provider.smartglass.models import (
SmartglassConsoleStatus, SmartglassConsoleStatus,
VolumeDirection, VolumeDirection,
@@ -67,15 +68,37 @@ def mock_token() -> Generator[MagicMock]:
yield token yield token
@pytest.mark.usefixtures("xbox_live_client") @pytest.mark.parametrize(
("fixture_status", "fixture_catalog"),
[
("smartglass_console_status.json", "catalog_product_lookup.json"),
("smartglass_console_status_idle.json", "catalog_product_lookup.json"),
("smartglass_console_status_livetv.json", "catalog_product_lookup_livetv.json"),
],
ids=["app", "idle", "livetvapp"],
)
async def test_media_players( async def test_media_players(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
xbox_live_client: AsyncMock,
fixture_status: str,
fixture_catalog: str | None,
) -> None: ) -> None:
"""Test setup of the Xbox media player platform.""" """Test setup of the Xbox media player platform."""
xbox_live_client.smartglass.get_console_status.return_value = (
SmartglassConsoleStatus(
**await async_load_json_object_fixture(hass, fixture_status, DOMAIN) # pyright: ignore[reportArgumentType]
)
)
xbox_live_client.catalog.get_product_from_alternate_id.return_value = (
CatalogResponse(
**await async_load_json_object_fixture(hass, fixture_catalog, DOMAIN) # pyright: ignore[reportArgumentType]
)
)
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@@ -237,11 +260,11 @@ async def test_media_player_actions(
], ],
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
"exception", ("exception", "translation_key"),
[ [
TimeoutException(""), (TimeoutException(""), "timeout_exception"),
RequestError("", request=Mock()), (RequestError("", request=Mock()), "request_exception"),
HTTPStatusError("", request=Mock(), response=Mock()), (HTTPStatusError("", request=Mock(), response=Mock()), "request_exception"),
], ],
) )
async def test_media_player_action_exceptions( async def test_media_player_action_exceptions(
@@ -252,6 +275,7 @@ async def test_media_player_action_exceptions(
service_args: dict[str, Any], service_args: dict[str, Any],
call_method: str, call_method: str,
exception: Exception, exception: Exception,
translation_key: str,
) -> None: ) -> None:
"""Test media player action exceptions.""" """Test media player action exceptions."""
@@ -271,13 +295,14 @@ async def test_media_player_action_exceptions(
getattr(xbox_live_client.smartglass, call_method).side_effect = exception getattr(xbox_live_client.smartglass, call_method).side_effect = exception
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError) as e:
await hass.services.async_call( await hass.services.async_call(
MEDIA_PLAYER_DOMAIN, MEDIA_PLAYER_DOMAIN,
service, service,
target={ATTR_ENTITY_ID: "media_player.xone", **service_args}, target={ATTR_ENTITY_ID: "media_player.xone", **service_args},
blocking=True, blocking=True,
) )
assert e.value.translation_key == translation_key
async def test_media_player_turn_on_failed( async def test_media_player_turn_on_failed(
@@ -299,10 +324,11 @@ async def test_media_player_turn_on_failed(
), ),
) )
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError) as e:
await hass.services.async_call( await hass.services.async_call(
MEDIA_PLAYER_DOMAIN, MEDIA_PLAYER_DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
target={ATTR_ENTITY_ID: "media_player.xone"}, target={ATTR_ENTITY_ID: "media_player.xone"},
blocking=True, blocking=True,
) )
assert e.value.translation_key == "turn_on_failed"

View File

@@ -1,6 +1,6 @@
"""Tests for the Xbox media source platform.""" """Tests for the Xbox media source platform."""
import httpx from httpx import HTTPStatusError, RequestError, Response, TimeoutException
import pytest import pytest
from pythonxbox.api.provider.people.models import PeopleResponse from pythonxbox.api.provider.people.models import PeopleResponse
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
@@ -173,14 +173,30 @@ async def test_browse_media_accounts(
"titlehub", "titlehub",
"get_title_info", "get_title_info",
), ),
(
"/271958441785640/1297287135/community_gameclips",
"gameclips",
"get_recent_community_clips_by_title_id",
),
(
"/271958441785640/1297287135/community_screenshots",
"screenshots",
"get_recent_community_screenshots_by_title_id",
),
], ],
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
"exception", ("exception", "translation_key"),
[ [
httpx.HTTPStatusError("", request=MagicMock(), response=httpx.Response(500)), (
httpx.RequestError(""), HTTPStatusError("", request=MagicMock(), response=Response(500)),
httpx.TimeoutException(""), "request_exception",
),
(
RequestError(""),
"request_exception",
),
(TimeoutException(""), "timeout_exception"),
], ],
) )
async def test_browse_media_exceptions( async def test_browse_media_exceptions(
@@ -191,6 +207,7 @@ async def test_browse_media_exceptions(
provider: str, provider: str,
method: str, method: str,
exception: Exception, exception: Exception,
translation_key: str,
) -> None: ) -> None:
"""Test browsing media exceptions.""" """Test browsing media exceptions."""
@@ -203,8 +220,9 @@ async def test_browse_media_exceptions(
provider = getattr(xbox_live_client, provider) provider = getattr(xbox_live_client, provider)
getattr(provider, method).side_effect = exception getattr(provider, method).side_effect = exception
with pytest.raises(BrowseError): with pytest.raises(BrowseError) as e:
await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}{media_content_id}") await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}{media_content_id}")
assert e.value.translation_key == translation_key
@pytest.mark.usefixtures("xbox_live_client") @pytest.mark.usefixtures("xbox_live_client")
@@ -239,8 +257,9 @@ async def test_browse_media_not_configured_exception(
assert config_entry.state is ConfigEntryState.NOT_LOADED assert config_entry.state is ConfigEntryState.NOT_LOADED
with pytest.raises(BrowseError, match="The Xbox integration is not configured"): with pytest.raises(BrowseError) as e:
await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}")
assert e.value.translation_key == "xbox_not_configured"
@pytest.mark.usefixtures("xbox_live_client") @pytest.mark.usefixtures("xbox_live_client")
@@ -256,8 +275,9 @@ async def test_browse_media_account_not_configured_exception(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
with pytest.raises(BrowseError): with pytest.raises(BrowseError) as e:
await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/2533274838782903") await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/2533274838782903")
assert e.value.translation_key == "account_not_configured"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -278,8 +298,24 @@ async def test_browse_media_account_not_configured_exception(
"https://store-images.s-microsoft.com/image/apps.35725.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.c4bf34f8-ad40-4af3-914e-a85e75a76bed", "https://store-images.s-microsoft.com/image/apps.35725.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.c4bf34f8-ad40-4af3-914e-a85e75a76bed",
"image/png", "image/png",
), ),
(
"/271958441785640/1297287135/community_screenshots/504a78e5-be24-4020-a245-77cb528e91ea",
"https://screenshotscontent-d5002.media.xboxlive.com/xuid-2535422966774043-private/504a78e5-be24-4020-a245-77cb528e91ea.PNG?skoid=296fcea0-0bf0-4a22-abf7-16b3524eba1b&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-06T10%3A21%3A06Z&ske=2025-11-07T10%3A21%3A06Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-06T10%3A35%3A59Z&se=2125-11-06T10%3A50%3A59Z&sr=b&sp=r&sig=TqUUNeuAzHawaXBTFfSVuUzuXbGOMgrDu0Q2VBTFd5U%3D",
"image/png",
),
(
"/271958441785640/1297287135/community_gameclips/6fa2731a-8b58-4aa6-848c-4bf15734358b",
"https://gameclipscontent-d3021.media.xboxlive.com/xuid-2535458333395495-private/6fa2731a-8b58-4aa6-848c-4bf15734358b.MP4?skoid=2938738c-0e58-4f21-9b82-98081ade42e2&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-06T08%3A20%3A51Z&ske=2025-11-07T08%3A20%3A51Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-06T10%3A05%3A41Z&se=2125-11-06T10%3A20%3A41Z&sr=b&sp=r&sig=s%2FWDtmE2cnAwl9iJJFcch3knbRlkxkALoinHQwCnNP0%3D&__gda__=1762438393_eb8a56c3f482d00099045aa892a2aa05",
"video/mp4",
),
],
ids=[
"screenshot",
"gameclips",
"game_media",
"community_screenshots",
"community_gameclips",
], ],
ids=["screenshot", "gameclips", "game_media"],
) )
@pytest.mark.usefixtures("xbox_live_client") @pytest.mark.usefixtures("xbox_live_client")
async def test_resolve_media( async def test_resolve_media(
@@ -319,6 +355,16 @@ async def test_resolve_media(
"gameclips", "gameclips",
"get_recent_clips_by_xuid", "get_recent_clips_by_xuid",
), ),
(
"/271958441785640/1297287135/community_screenshots/504a78e5-be24-4020-a245-77cb528e91ea",
"screenshots",
"get_recent_community_screenshots_by_title_id",
),
(
"/271958441785640/1297287135/community_gameclips/6fa2731a-8b58-4aa6-848c-4bf15734358b",
"gameclips",
"get_recent_community_clips_by_title_id",
),
( (
"/271958441785640/1297287135/game_media/0", "/271958441785640/1297287135/game_media/0",
"titlehub", "titlehub",
@@ -327,11 +373,14 @@ async def test_resolve_media(
], ],
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
"exception", ("exception", "translation_key"),
[ [
httpx.HTTPStatusError("", request=MagicMock(), response=httpx.Response(500)), (
httpx.RequestError(""), HTTPStatusError("", request=MagicMock(), response=Response(500)),
httpx.TimeoutException(""), "request_exception",
),
(RequestError(""), "request_exception"),
(TimeoutException(""), "timeout_exception"),
], ],
) )
async def test_resolve_media_exceptions( async def test_resolve_media_exceptions(
@@ -342,6 +391,7 @@ async def test_resolve_media_exceptions(
provider: str, provider: str,
method: str, method: str,
exception: Exception, exception: Exception,
translation_key: str,
) -> None: ) -> None:
"""Test resolve media exceptions.""" """Test resolve media exceptions."""
@@ -354,12 +404,13 @@ async def test_resolve_media_exceptions(
provider = getattr(xbox_live_client, provider) provider = getattr(xbox_live_client, provider)
getattr(provider, method).side_effect = exception getattr(provider, method).side_effect = exception
with pytest.raises(Unresolvable): with pytest.raises(Unresolvable) as e:
await async_resolve_media( await async_resolve_media(
hass, hass,
f"{URI_SCHEME}{DOMAIN}{media_content_id}", f"{URI_SCHEME}{DOMAIN}{media_content_id}",
None, None,
) )
assert e.value.translation_key == translation_key
@pytest.mark.parametrize(("media_type"), ["screenshots", "gameclips", "game_media"]) @pytest.mark.parametrize(("media_type"), ["screenshots", "gameclips", "game_media"])
@@ -377,12 +428,13 @@ async def test_resolve_media_not_found_exceptions(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
with pytest.raises(Unresolvable, match="The requested media could not be found"): with pytest.raises(Unresolvable) as e:
await async_resolve_media( await async_resolve_media(
hass, hass,
f"{URI_SCHEME}{DOMAIN}/271958441785640/1297287135/{media_type}/12345", f"{URI_SCHEME}{DOMAIN}/271958441785640/1297287135/{media_type}/12345",
None, None,
) )
assert e.value.translation_key == "media_not_found"
@pytest.mark.usefixtures("xbox_live_client") @pytest.mark.usefixtures("xbox_live_client")
@@ -416,12 +468,13 @@ async def test_resolve_media_not_configured(
assert config_entry.state is ConfigEntryState.NOT_LOADED assert config_entry.state is ConfigEntryState.NOT_LOADED
with pytest.raises(Unresolvable, match="The Xbox integration is not configured"): with pytest.raises(Unresolvable) as e:
await async_resolve_media( await async_resolve_media(
hass, hass,
f"{URI_SCHEME}{DOMAIN}/2533274838782903", f"{URI_SCHEME}{DOMAIN}/2533274838782903",
None, None,
) )
assert e.value.translation_key == "xbox_not_configured"
@pytest.mark.usefixtures("xbox_live_client") @pytest.mark.usefixtures("xbox_live_client")
@@ -437,9 +490,10 @@ async def test_resolve_media_account_not_configured(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
with pytest.raises(Unresolvable, match="The Xbox account is not configured"): with pytest.raises(Unresolvable) as e:
await async_resolve_media( await async_resolve_media(
hass, hass,
f"{URI_SCHEME}{DOMAIN}/2533274838782903", f"{URI_SCHEME}{DOMAIN}/2533274838782903",
None, None,
) )
assert e.value.translation_key == "account_not_configured"

View File

@@ -248,9 +248,7 @@ async def test_send_command_exceptions(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
getattr(xbox_live_client.smartglass, call_method).side_effect = exception getattr(xbox_live_client.smartglass, call_method).side_effect = exception
with pytest.raises( with pytest.raises(HomeAssistantError) as e:
HomeAssistantError, check=lambda e: e.translation_key == translation_key
):
await hass.services.async_call( await hass.services.async_call(
REMOTE_DOMAIN, REMOTE_DOMAIN,
SERVICE_SEND_COMMAND, SERVICE_SEND_COMMAND,
@@ -258,6 +256,7 @@ async def test_send_command_exceptions(
target={ATTR_ENTITY_ID: "remote.xone"}, target={ATTR_ENTITY_ID: "remote.xone"},
blocking=True, blocking=True,
) )
assert e.value.translation_key == translation_key
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -290,15 +289,14 @@ async def test_turn_on_exceptions(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
xbox_live_client.smartglass.wake_up.side_effect = exception xbox_live_client.smartglass.wake_up.side_effect = exception
with pytest.raises( with pytest.raises(HomeAssistantError) as e:
HomeAssistantError, check=lambda e: e.translation_key == translation_key
):
await hass.services.async_call( await hass.services.async_call(
REMOTE_DOMAIN, REMOTE_DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
target={ATTR_ENTITY_ID: "remote.xone"}, target={ATTR_ENTITY_ID: "remote.xone"},
blocking=True, blocking=True,
) )
assert e.value.translation_key == translation_key
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -325,12 +323,11 @@ async def test_turn_off_exceptions(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
xbox_live_client.smartglass.turn_off.side_effect = exception xbox_live_client.smartglass.turn_off.side_effect = exception
with pytest.raises( with pytest.raises(HomeAssistantError) as e:
HomeAssistantError, check=lambda e: e.translation_key == translation_key
):
await hass.services.async_call( await hass.services.async_call(
REMOTE_DOMAIN, REMOTE_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
target={ATTR_ENTITY_ID: "remote.xone"}, target={ATTR_ENTITY_ID: "remote.xone"},
blocking=True, blocking=True,
) )
assert e.value.translation_key == translation_key