Files
RelayTV-HA/tests/test_targeting.py
markandClaude Fable 5 f2bf565623 Harden RelayTV integration and prepare HACS 0.4.0 (#2)
* docs: add Home Assistant 0.4.0 hardening roadmap

* fix: protect media credentials and resume state

* feat: authenticate RelayTV API operations

* fix: make targeting and media state reliable

* test: add Home Assistant integration CI

* fix: validate API credentials during setup

* release: prepare HACS 0.4.0

* fix: align pytest dependency pin

* fix: close URL sanitizer gaps and correct player state reporting

Sync the sensitive-query-key list with the RelayTV server (adds auth,
exp, jwt, X-Emby-Token, X-Jellyfin-Token), filter query credentials
from relative URLs instead of returning them verbatim, and preserve
brackets around IPv6 literal hosts.

Also report volume on RelayTV's 0-100 scale unconditionally (a raw 1
is 1%, not full volume) and give the coordinator its own
position_updated_at stamp — the base DataUpdateCoordinator has no
last_update_success_time, so media_position_updated_at silently fell
back to now() on every read and the seek bar never extrapolated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 21:08:54 -05:00

102 lines
3.5 KiB
Python

"""Tests for RelayTV service and sidebar targeting."""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from homeassistant.core import ServiceCall
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.relaytv import (
_async_set_default_sidebar_target,
_async_update_panel,
_resolve_entry_ids_for_call,
)
from custom_components.relaytv.const import (
CONF_PANEL_ENABLED,
CONF_PANEL_TARGET_ENTRY_ID,
DATA_API,
DATA_PANEL_SETTINGS,
DATA_STORE,
DOMAIN,
)
def _add_entry(hass, title: str) -> MockConfigEntry:
entry = MockConfigEntry(domain=DOMAIN, title=title, data={"base_url": f"http://{title.lower()}:8787"})
entry.add_to_hass(hass)
return entry
async def test_explicit_non_relaytv_target_never_uses_fallback(hass) -> None:
relay_entry = _add_entry(hass, "RelayTV")
other_entry = MockConfigEntry(domain="test", title="Other", data={})
other_entry.add_to_hass(hass)
registry = er.async_get(hass)
other_entity = registry.async_get_or_create(
"media_player",
"test",
"other-player",
config_entry=other_entry,
suggested_object_id="other_player",
)
hass.data[DOMAIN] = {relay_entry.entry_id: {DATA_API: MagicMock()}}
call = ServiceCall(hass, DOMAIN, "play_now", {"entity_id": other_entity.entity_id})
with pytest.raises(ServiceValidationError, match="does not contain a loaded RelayTV"):
_resolve_entry_ids_for_call(hass, call)
async def test_no_target_uses_saved_panel_target(hass) -> None:
first = _add_entry(hass, "First")
second = _add_entry(hass, "Second")
hass.data[DOMAIN] = {
first.entry_id: {DATA_API: MagicMock()},
second.entry_id: {DATA_API: MagicMock()},
DATA_PANEL_SETTINGS: {CONF_PANEL_TARGET_ENTRY_ID: second.entry_id},
}
call = ServiceCall(hass, DOMAIN, "play_now", {})
assert _resolve_entry_ids_for_call(hass, call) == [second.entry_id]
async def test_setup_does_not_overwrite_saved_panel_target(hass) -> None:
first = _add_entry(hass, "First")
second = _add_entry(hass, "Second")
store = MagicMock()
store.async_save = AsyncMock()
settings = {CONF_PANEL_ENABLED: True, CONF_PANEL_TARGET_ENTRY_ID: first.entry_id}
hass.data[DOMAIN] = {DATA_STORE: store, DATA_PANEL_SETTINGS: settings}
await _async_set_default_sidebar_target(hass, second)
assert settings[CONF_PANEL_TARGET_ENTRY_ID] == first.entry_id
store.async_save.assert_not_awaited()
async def test_panel_waits_for_saved_target_to_finish_loading(hass) -> None:
selected = _add_entry(hass, "Selected")
fallback = _add_entry(hass, "Fallback")
store = MagicMock()
store.async_save = AsyncMock()
settings = {CONF_PANEL_ENABLED: True, CONF_PANEL_TARGET_ENTRY_ID: selected.entry_id}
hass.data[DOMAIN] = {
DATA_STORE: store,
DATA_PANEL_SETTINGS: settings,
fallback.entry_id: {DATA_API: MagicMock(base_url="http://fallback:8787")},
}
with (
patch("custom_components.relaytv._async_unregister_panel") as unregister,
patch("custom_components.relaytv._register_panel") as register,
):
await _async_update_panel(hass)
assert settings[CONF_PANEL_TARGET_ENTRY_ID] == selected.entry_id
store.async_save.assert_not_awaited()
unregister.assert_not_called()
register.assert_not_called()