* 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>
60 lines
2.0 KiB
Python
60 lines
2.0 KiB
Python
"""Tests for RelayTV media-player behavior."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from homeassistant.components.media_player.const import MediaPlayerEntityFeature
|
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
|
|
|
from custom_components.relaytv.media_player import RelayTVMediaPlayer, _parse_status
|
|
|
|
|
|
def test_status_parser_preserves_zero_values() -> None:
|
|
status = _parse_status(
|
|
{
|
|
"playing": True,
|
|
"volume": 0,
|
|
"position": 0,
|
|
"pos": 25,
|
|
"duration": 0,
|
|
"len": 120,
|
|
"mute": True,
|
|
}
|
|
)
|
|
|
|
assert status.volume == 0
|
|
assert status.position == 0
|
|
assert status.duration == 0
|
|
assert status.muted is True
|
|
|
|
|
|
def test_status_parser_reads_volume_on_percent_scale() -> None:
|
|
# RelayTV reports 0-100; a raw 1 is 1%, not full volume.
|
|
assert _parse_status({"volume": 1}).volume == 0.01
|
|
assert _parse_status({"volume": 100}).volume == 1.0
|
|
|
|
|
|
async def test_media_player_exposes_device_and_native_mute(hass) -> None:
|
|
entry = MockConfigEntry(
|
|
domain="relaytv",
|
|
title="Living Room",
|
|
data={"base_url": "http://relaytv.local:8787", "name": "Living Room"},
|
|
)
|
|
coordinator = MagicMock()
|
|
coordinator.data = {"playing": True, "volume": 25, "mute": False}
|
|
coordinator.last_update_success = True
|
|
coordinator.async_request_refresh = AsyncMock()
|
|
api = MagicMock(base_url="http://relaytv.local:8787")
|
|
api.mute = AsyncMock(return_value=True)
|
|
player = RelayTVMediaPlayer(entry, coordinator, api)
|
|
|
|
assert player.device_info["name"] == "Living Room"
|
|
assert player.device_info["manufacturer"] == "RelayTV"
|
|
assert player.volume_level == 0.25
|
|
assert player.is_volume_muted is False
|
|
assert player.supported_features & MediaPlayerEntityFeature.VOLUME_MUTE
|
|
|
|
await player.async_mute_volume(True)
|
|
|
|
api.mute.assert_awaited_once_with(True)
|
|
coordinator.async_request_refresh.assert_awaited_once_with()
|