1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-14 23:28:42 +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())
# Setup focus app
app_details: Product | None = None
if (current_state := self.data.get(console.id)) is not None:
app_details = 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 (
not current_state
or status.focus_app_aumid != current_state.status.focus_app_aumid
):
app_id = status.focus_app_aumid.split("!")[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]
catalog_result = (
await self.client.catalog.get_product_from_alternate_id(
app_id, id_type
)
if status.focus_app_aumid and (
not current_state
or status.focus_app_aumid != current_state.status.focus_app_aumid
):
catalog_result = (
await self.client.catalog.get_product_from_alternate_id(
*self._resolve_app_id(status.focus_app_aumid)
)
)
if catalog_result.products:
app_details = catalog_result.products[0]
else:
app_details = None
if catalog_result.products:
app_details = catalog_result.products[0]
data[console.id] = ConsoleData(status=status, app_details=app_details)
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]):
"""Update list of Xbox consoles."""

View File

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

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",
"isFavorite": true,
"isFavorite": false,
"isFollowingCaller": true,
"isFollowedByCaller": true,
"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**',
'is_broadcasting': False,
'is_cloaked': None,
'is_favorite': True,
'is_favorite': False,
'is_followed_by_caller': True,
'is_following_caller': True,
'is_friend': True,

View File

@@ -124,7 +124,7 @@
'title': 'Installed Applications',
})
# ---
# name: test_media_players[media_player.xone-entry]
# name: test_media_players[app][media_player.xone-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -161,7 +161,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_media_players[media_player.xone-state]
# name: test_media_players[app][media_player.xone-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'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',
})
# ---
# name: test_media_players[media_player.xonex-entry]
# name: test_media_players[app][media_player.xonex-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -217,7 +217,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_media_players[media_player.xonex-state]
# name: test_media_players[app][media_player.xonex-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'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',
})
# ---
# 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 typing import Any
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock, Mock, patch
from httpx import HTTPStatusError, RequestError, TimeoutException
import pytest
from pythonxbox.api.provider.people.models import PeopleResponse
@@ -22,7 +23,10 @@ from homeassistant.config_entries import (
)
from homeassistant.core import HomeAssistant
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.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")
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."""
config_entry = MockConfigEntry(
domain=DOMAIN,
@@ -601,6 +608,17 @@ async def test_unique_id_and_friends_migration(hass: HomeAssistant) -> None:
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.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 favorite friends migrated to subentries
assert len(config_entry.subentries) == 2
assert len(config_entry.subentries) == 1
subentries = list(config_entry.subentries.values())
assert subentries[0].unique_id == "2533274838782903"
assert subentries[0].title == "Ikken Hissatsuu"
assert subentries[0].subentry_type == "friend"
assert subentries[1].unique_id == "2533274913657542"
assert subentries[1].title == "erics273"
assert subentries[1].subentry_type == "friend"
## Assert devices have been migrated
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(

View File

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

View File

@@ -7,6 +7,7 @@ from unittest.mock import patch
from httpx import HTTPStatusError, RequestError, TimeoutException
import pytest
from pythonxbox.api.provider.catalog.models import CatalogResponse
from pythonxbox.api.provider.smartglass.models import (
SmartglassConsoleStatus,
VolumeDirection,
@@ -67,15 +68,37 @@ def mock_token() -> Generator[MagicMock]:
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(
hass: HomeAssistant,
config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
xbox_live_client: AsyncMock,
fixture_status: str,
fixture_catalog: str | None,
) -> None:
"""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)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
@@ -237,11 +260,11 @@ async def test_media_player_actions(
],
)
@pytest.mark.parametrize(
"exception",
("exception", "translation_key"),
[
TimeoutException(""),
RequestError("", request=Mock()),
HTTPStatusError("", request=Mock(), response=Mock()),
(TimeoutException(""), "timeout_exception"),
(RequestError("", request=Mock()), "request_exception"),
(HTTPStatusError("", request=Mock(), response=Mock()), "request_exception"),
],
)
async def test_media_player_action_exceptions(
@@ -252,6 +275,7 @@ async def test_media_player_action_exceptions(
service_args: dict[str, Any],
call_method: str,
exception: Exception,
translation_key: str,
) -> None:
"""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
with pytest.raises(HomeAssistantError):
with pytest.raises(HomeAssistantError) as e:
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
service,
target={ATTR_ENTITY_ID: "media_player.xone", **service_args},
blocking=True,
)
assert e.value.translation_key == translation_key
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(
MEDIA_PLAYER_DOMAIN,
SERVICE_TURN_ON,
target={ATTR_ENTITY_ID: "media_player.xone"},
blocking=True,
)
assert e.value.translation_key == "turn_on_failed"

View File

@@ -1,6 +1,6 @@
"""Tests for the Xbox media source platform."""
import httpx
from httpx import HTTPStatusError, RequestError, Response, TimeoutException
import pytest
from pythonxbox.api.provider.people.models import PeopleResponse
from syrupy.assertion import SnapshotAssertion
@@ -173,14 +173,30 @@ async def test_browse_media_accounts(
"titlehub",
"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(
"exception",
("exception", "translation_key"),
[
httpx.HTTPStatusError("", request=MagicMock(), response=httpx.Response(500)),
httpx.RequestError(""),
httpx.TimeoutException(""),
(
HTTPStatusError("", request=MagicMock(), response=Response(500)),
"request_exception",
),
(
RequestError(""),
"request_exception",
),
(TimeoutException(""), "timeout_exception"),
],
)
async def test_browse_media_exceptions(
@@ -191,6 +207,7 @@ async def test_browse_media_exceptions(
provider: str,
method: str,
exception: Exception,
translation_key: str,
) -> None:
"""Test browsing media exceptions."""
@@ -203,8 +220,9 @@ async def test_browse_media_exceptions(
provider = getattr(xbox_live_client, provider)
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}")
assert e.value.translation_key == translation_key
@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
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}")
assert e.value.translation_key == "xbox_not_configured"
@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
with pytest.raises(BrowseError):
with pytest.raises(BrowseError) as e:
await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/2533274838782903")
assert e.value.translation_key == "account_not_configured"
@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",
"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")
async def test_resolve_media(
@@ -319,6 +355,16 @@ async def test_resolve_media(
"gameclips",
"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",
"titlehub",
@@ -327,11 +373,14 @@ async def test_resolve_media(
],
)
@pytest.mark.parametrize(
"exception",
("exception", "translation_key"),
[
httpx.HTTPStatusError("", request=MagicMock(), response=httpx.Response(500)),
httpx.RequestError(""),
httpx.TimeoutException(""),
(
HTTPStatusError("", request=MagicMock(), response=Response(500)),
"request_exception",
),
(RequestError(""), "request_exception"),
(TimeoutException(""), "timeout_exception"),
],
)
async def test_resolve_media_exceptions(
@@ -342,6 +391,7 @@ async def test_resolve_media_exceptions(
provider: str,
method: str,
exception: Exception,
translation_key: str,
) -> None:
"""Test resolve media exceptions."""
@@ -354,12 +404,13 @@ async def test_resolve_media_exceptions(
provider = getattr(xbox_live_client, provider)
getattr(provider, method).side_effect = exception
with pytest.raises(Unresolvable):
with pytest.raises(Unresolvable) as e:
await async_resolve_media(
hass,
f"{URI_SCHEME}{DOMAIN}{media_content_id}",
None,
)
assert e.value.translation_key == translation_key
@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
with pytest.raises(Unresolvable, match="The requested media could not be found"):
with pytest.raises(Unresolvable) as e:
await async_resolve_media(
hass,
f"{URI_SCHEME}{DOMAIN}/271958441785640/1297287135/{media_type}/12345",
None,
)
assert e.value.translation_key == "media_not_found"
@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
with pytest.raises(Unresolvable, match="The Xbox integration is not configured"):
with pytest.raises(Unresolvable) as e:
await async_resolve_media(
hass,
f"{URI_SCHEME}{DOMAIN}/2533274838782903",
None,
)
assert e.value.translation_key == "xbox_not_configured"
@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
with pytest.raises(Unresolvable, match="The Xbox account is not configured"):
with pytest.raises(Unresolvable) as e:
await async_resolve_media(
hass,
f"{URI_SCHEME}{DOMAIN}/2533274838782903",
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
getattr(xbox_live_client.smartglass, call_method).side_effect = exception
with pytest.raises(
HomeAssistantError, check=lambda e: e.translation_key == translation_key
):
with pytest.raises(HomeAssistantError) as e:
await hass.services.async_call(
REMOTE_DOMAIN,
SERVICE_SEND_COMMAND,
@@ -258,6 +256,7 @@ async def test_send_command_exceptions(
target={ATTR_ENTITY_ID: "remote.xone"},
blocking=True,
)
assert e.value.translation_key == translation_key
@pytest.mark.parametrize(
@@ -290,15 +289,14 @@ async def test_turn_on_exceptions(
assert config_entry.state is ConfigEntryState.LOADED
xbox_live_client.smartglass.wake_up.side_effect = exception
with pytest.raises(
HomeAssistantError, check=lambda e: e.translation_key == translation_key
):
with pytest.raises(HomeAssistantError) as e:
await hass.services.async_call(
REMOTE_DOMAIN,
SERVICE_TURN_ON,
target={ATTR_ENTITY_ID: "remote.xone"},
blocking=True,
)
assert e.value.translation_key == translation_key
@pytest.mark.parametrize(
@@ -325,12 +323,11 @@ async def test_turn_off_exceptions(
assert config_entry.state is ConfigEntryState.LOADED
xbox_live_client.smartglass.turn_off.side_effect = exception
with pytest.raises(
HomeAssistantError, check=lambda e: e.translation_key == translation_key
):
with pytest.raises(HomeAssistantError) as e:
await hass.services.async_call(
REMOTE_DOMAIN,
SERVICE_TURN_OFF,
target={ATTR_ENTITY_ID: "remote.xone"},
blocking=True,
)
assert e.value.translation_key == translation_key