Strip trailing slash from Jellyfin server URL (#173049)

This commit is contained in:
Evan Severson
2026-06-08 06:43:58 -07:00
committed by GitHub
parent df4fbc91f9
commit 828ec639dd
4 changed files with 71 additions and 0 deletions
@@ -2,6 +2,7 @@
from typing import Any from typing import Any
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers import config_validation as cv, device_registry as dr
@@ -65,6 +66,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) ->
return True return True
async def async_migrate_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) -> bool:
"""Migrate an old config entry."""
if entry.version == 1 and entry.minor_version < 2:
new_data = {**entry.data, CONF_URL: entry.data[CONF_URL].rstrip("/")}
hass.config_entries.async_update_entry(entry, data=new_data, minor_version=2)
return True
async def async_unload_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -46,6 +46,7 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Jellyfin.""" """Handle a config flow for Jellyfin."""
VERSION = 1 VERSION = 1
MINOR_VERSION = 2
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the Jellyfin config flow.""" """Initialize the Jellyfin config flow."""
@@ -58,6 +59,8 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
user_input[CONF_URL] = user_input[CONF_URL].rstrip("/")
if self.client_device_id is None: if self.client_device_id is None:
self.client_device_id = _generate_client_device_id() self.client_device_id = _generate_client_device_id()
@@ -59,6 +59,35 @@ async def test_form(
assert len(mock_client.jellyfin.get_user_settings.mock_calls) == 1 assert len(mock_client.jellyfin.get_user_settings.mock_calls) == 1
async def test_form_strips_trailing_slash_from_url(
hass: HomeAssistant,
mock_jellyfin: MagicMock,
mock_client: MagicMock,
mock_client_device_id: MagicMock,
mock_setup_entry: MagicMock,
) -> None:
"""Test a trailing slash is stripped from the configured URL.
A trailing slash would otherwise be joined into a double-slashed request
path (e.g. //system/info/public) that some Jellyfin versions reject.
"""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={**USER_INPUT, CONF_URL: f"{TEST_URL}/"},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.CREATE_ENTRY
# The persisted URL has no trailing slash...
assert result2["data"][CONF_URL] == TEST_URL
# ...and the connection was attempted against the normalized URL.
mock_client.auth.connect_to_address.assert_called_once_with(TEST_URL)
async def test_form_cannot_connect( async def test_form_cannot_connect(
hass: HomeAssistant, hass: HomeAssistant,
mock_jellyfin: MagicMock, mock_jellyfin: MagicMock,
+29
View File
@@ -4,11 +4,13 @@ from unittest.mock import MagicMock
from homeassistant.components.jellyfin.const import DOMAIN from homeassistant.components.jellyfin.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from . import async_load_json_fixture from . import async_load_json_fixture
from .const import TEST_PASSWORD, TEST_URL, TEST_USERNAME
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
from tests.typing import WebSocketGenerator from tests.typing import WebSocketGenerator
@@ -75,6 +77,33 @@ async def test_load_unload_config_entry(
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
async def test_migrate_strips_trailing_slash_from_url(
hass: HomeAssistant,
mock_jellyfin: MagicMock,
mock_client: MagicMock,
) -> None:
"""Test migrating an entry strips a trailing slash from the stored URL."""
config_entry = MockConfigEntry(
title="Jellyfin",
domain=DOMAIN,
data={
CONF_URL: f"{TEST_URL}/",
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
},
unique_id="USER-UUID",
version=1,
minor_version=1,
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.minor_version == 2
assert config_entry.data[CONF_URL] == TEST_URL
async def test_device_remove_devices( async def test_device_remove_devices(
hass: HomeAssistant, hass: HomeAssistant,
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,