Files
core/tests/components/vizio/test_init.py
T
2026-08-11 15:46:01 +02:00

313 lines
10 KiB
Python

"""Tests for Vizio init."""
from datetime import timedelta
from typing import Any
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from vizaio import VizioConnectionError
from homeassistant.components.media_player import (
DOMAIN as MEDIA_PLAYER_DOMAIN,
MediaPlayerDeviceClass,
)
from homeassistant.components.vizio import DATA_APPS
from homeassistant.components.vizio.const import (
CONF_ADDITIONAL_CONFIGS,
CONF_APPS,
CONF_VOLUME_STEP,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_DEVICE_CLASS,
CONF_HOST,
CONF_INCLUDE,
CONF_NAME,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .conftest import setup_integration
from .const import (
ADDITIONAL_APP_CONFIG,
APP_RECORDS,
CURRENT_APP,
HOST,
HOST2,
MOCK_USER_VALID_TV_CONFIG,
MODEL,
NAME2,
PORTLESS_HOST,
UNIQUE_ID,
VERSION,
VOLUME_STEP,
)
from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_tv_load_and_unload(
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
) -> None:
"""Test loading and unloading TV entry."""
await setup_integration(hass, mock_tv_config_entry)
assert len(hass.states.async_entity_ids(MEDIA_PLAYER_DOMAIN)) == 1
assert DATA_APPS in hass.data
assert await hass.config_entries.async_unload(mock_tv_config_entry.entry_id)
await hass.async_block_till_done()
entities = hass.states.async_entity_ids(MEDIA_PLAYER_DOMAIN)
assert len(entities) == 1
for entity in entities:
assert hass.states.get(entity).state == STATE_UNAVAILABLE
assert DATA_APPS not in hass.data
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_speaker_load_and_unload(
hass: HomeAssistant, mock_speaker_config_entry: MockConfigEntry
) -> None:
"""Test loading and unloading speaker entry."""
await setup_integration(hass, mock_speaker_config_entry)
assert len(hass.states.async_entity_ids(MEDIA_PLAYER_DOMAIN)) == 1
assert await hass.config_entries.async_unload(mock_speaker_config_entry.entry_id)
await hass.async_block_till_done()
entities = hass.states.async_entity_ids(MEDIA_PLAYER_DOMAIN)
assert len(entities) == 1
for entity in entities:
assert hass.states.get(entity).state == STATE_UNAVAILABLE
@pytest.mark.usefixtures(
"vizio_connect", "vizio_bypass_update", "vizio_data_coordinator_update_failure"
)
async def test_coordinator_update_failure(
hass: HomeAssistant,
mock_tv_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test coordinator update failure after 10 days."""
await setup_integration(hass, mock_tv_config_entry)
assert len(hass.states.async_entity_ids(MEDIA_PLAYER_DOMAIN)) == 1
assert DATA_APPS in hass.data
# Failing 25 days in a row should result in a single log message
# (first one after 10 days, next one would be at 30 days)
for days in range(1, 25):
freezer.tick(timedelta(days=days))
async_fire_time_changed(hass)
await hass.async_block_till_done()
err_msg = "Unable to retrieve the apps list from the external server"
assert len([record for record in caplog.records if err_msg in record.msg]) == 1
@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_update")
async def test_apps_coordinator_persists_until_last_tv_unloads(
hass: HomeAssistant,
mock_tv_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test shared apps coordinator is not shut down until the last TV entry unloads."""
config_entry_2 = MockConfigEntry(
domain=DOMAIN,
data={
CONF_NAME: NAME2,
CONF_HOST: HOST2,
CONF_DEVICE_CLASS: MediaPlayerDeviceClass.TV,
CONF_ACCESS_TOKEN: "deadbeef2",
},
unique_id="testid2",
)
await setup_integration(hass, mock_tv_config_entry)
config_entry_2.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry_2.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(MEDIA_PLAYER_DOMAIN)) == 2
# Unload first TV — coordinator should still be fetching apps
assert await hass.config_entries.async_unload(mock_tv_config_entry.entry_id)
await hass.async_block_till_done()
with patch(
"homeassistant.components.vizio.coordinator.fetch_remote_app_catalog",
return_value=APP_RECORDS,
) as mock_fetch:
freezer.tick(timedelta(days=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_fetch.call_count == 1
# Unload second (last) TV — coordinator should stop fetching apps
assert await hass.config_entries.async_unload(config_entry_2.entry_id)
await hass.async_block_till_done()
with patch(
"homeassistant.components.vizio.coordinator.fetch_remote_app_catalog",
return_value=APP_RECORDS,
) as mock_fetch:
freezer.tick(timedelta(days=2))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_fetch.call_count == 0
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_device_registry_model_and_version(
hass: HomeAssistant,
mock_tv_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test that coordinator populates device registry with model and version."""
await setup_integration(hass, mock_tv_config_entry)
device = device_registry.async_get_device_by_identifier(
(DOMAIN, UNIQUE_ID), mock_tv_config_entry.entry_id
)
assert device is not None
assert device.model == MODEL
assert device.sw_version == VERSION
assert device.manufacturer == "VIZIO"
@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_update")
async def test_device_registry_without_model_or_version(
hass: HomeAssistant,
mock_tv_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test device registry when model and version are unavailable."""
await setup_integration(hass, mock_tv_config_entry)
device = device_registry.async_get_device_by_identifier(
(DOMAIN, UNIQUE_ID), mock_tv_config_entry.entry_id
)
assert device is not None
assert device.model is None
assert device.sw_version is None
assert device.manufacturer == "VIZIO"
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_portless_host_is_resolved_and_persisted(hass: HomeAssistant) -> None:
"""Test a config entry storing a host without a port is repaired on setup."""
entry = MockConfigEntry(
domain=DOMAIN,
data={**MOCK_USER_VALID_TV_CONFIG, CONF_HOST: PORTLESS_HOST},
unique_id=UNIQUE_ID,
)
with patch(
"homeassistant.components.vizio.async_resolve_host",
AsyncMock(return_value=HOST),
) as mock_resolve:
await setup_integration(hass, entry)
assert mock_resolve.call_args[0][0] == PORTLESS_HOST
assert entry.state is ConfigEntryState.LOADED
assert entry.data[CONF_HOST] == HOST
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_portless_host_resolve_failure_retries(hass: HomeAssistant) -> None:
"""Test setup is retried when the port cannot be determined."""
entry = MockConfigEntry(
domain=DOMAIN,
data={**MOCK_USER_VALID_TV_CONFIG, CONF_HOST: PORTLESS_HOST},
unique_id=UNIQUE_ID,
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.vizio.async_resolve_host",
AsyncMock(side_effect=VizioConnectionError("no SmartCast API")),
):
assert not await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
assert entry.data[CONF_HOST] == PORTLESS_HOST
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_host_with_port_is_left_alone(
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
) -> None:
"""Test a host that already has a port is not rewritten.
async_resolve_host is idempotent, so it is called unconditionally and
returns the host untouched without any probing of its own.
"""
await setup_integration(hass, mock_tv_config_entry)
assert mock_tv_config_entry.data[CONF_HOST] == HOST
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
@pytest.mark.parametrize(
("data", "options", "expected_data", "expected_options"),
[
pytest.param(
{
**MOCK_USER_VALID_TV_CONFIG,
CONF_VOLUME_STEP: VOLUME_STEP,
CONF_APPS: {
CONF_INCLUDE: [CURRENT_APP],
CONF_ADDITIONAL_CONFIGS: [ADDITIONAL_APP_CONFIG],
},
},
{},
{
**MOCK_USER_VALID_TV_CONFIG,
CONF_APPS: {CONF_ADDITIONAL_CONFIGS: [ADDITIONAL_APP_CONFIG]},
},
{
CONF_VOLUME_STEP: VOLUME_STEP,
CONF_APPS: {CONF_INCLUDE: [CURRENT_APP]},
},
id="moves_settings_to_options",
),
pytest.param(
{**MOCK_USER_VALID_TV_CONFIG, CONF_VOLUME_STEP: VOLUME_STEP},
{CONF_VOLUME_STEP: VOLUME_STEP + 1},
MOCK_USER_VALID_TV_CONFIG,
{CONF_VOLUME_STEP: VOLUME_STEP + 1},
id="existing_options_win",
),
pytest.param(
MOCK_USER_VALID_TV_CONFIG,
{},
MOCK_USER_VALID_TV_CONFIG,
{},
id="nothing_to_migrate",
),
],
)
async def test_migrate_entry_to_minor_version_2(
hass: HomeAssistant,
data: dict[str, Any],
options: dict[str, Any],
expected_data: dict[str, Any],
expected_options: dict[str, Any],
) -> None:
"""Test migrating a 1.1 entry moves settings from data to options."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data=data,
options=options,
unique_id=UNIQUE_ID,
minor_version=1,
)
await setup_integration(hass, config_entry)
assert config_entry.version == 1
assert config_entry.minor_version == 2
assert dict(config_entry.data) == expected_data
assert dict(config_entry.options) == expected_options