Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion homeassistant/components/music_assistant/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from music_assistant_models.errors import (
ActionUnavailable,
AuthenticationFailed,
AuthenticationRequired,
InvalidToken,
MusicAssistantError,
)
Expand Down Expand Up @@ -99,7 +100,7 @@ async def async_setup_entry( # noqa: C901
translation_key="invalid_server_version",
)
raise ConfigEntryNotReady(f"Invalid server version: {err}") from err
except (AuthenticationFailed, InvalidToken) as err:
except (AuthenticationRequired, AuthenticationFailed, InvalidToken) as err:
raise ConfigEntryAuthFailed(
f"Authentication failed for {mass_url}: {err}"
) from err
Expand Down
28 changes: 23 additions & 5 deletions homeassistant/components/music_assistant/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
from music_assistant_models.errors import AuthenticationFailed, InvalidToken
import voluptuous as vol

from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import (
SOURCE_REAUTH,
ConfigEntryState,
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
Expand Down Expand Up @@ -165,10 +170,23 @@ async def async_step_hassio(
self.token = discovery_info.config["auth_token"]

self.server_info = server_info
await self.async_set_unique_id(server_info.server_id)
self._abort_if_unique_id_configured(
updates={CONF_URL: self.url, CONF_TOKEN: self.token}
)

# Check if there's an existing entry
if entry := await self.async_set_unique_id(server_info.server_id):
# Update the entry with new URL and token
if self.hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_URL: self.url, CONF_TOKEN: self.token}
):
# Reload the entry if it's in a state that can be reloaded
if entry.state in (
ConfigEntryState.LOADED,
ConfigEntryState.SETUP_ERROR,
ConfigEntryState.SETUP_RETRY,
):
self.hass.config_entries.async_schedule_reload(entry.entry_id)

# Abort since entry already exists
return self.async_abort(reason="already_configured")

return await self.async_step_hassio_confirm()

Expand Down
51 changes: 51 additions & 0 deletions tests/components/music_assistant/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
SOURCE_REAUTH,
SOURCE_USER,
SOURCE_ZEROCONF,
ConfigEntryState,
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
Expand Down Expand Up @@ -465,6 +466,56 @@ async def test_hassio_flow_duplicate(
assert result["reason"] == "already_configured"


async def test_hassio_flow_updates_failed_entry_and_reloads(
hass: HomeAssistant,
mock_get_server_info: AsyncMock,
) -> None:
"""Test hassio discovery updates entry in SETUP_ERROR state and schedules reload."""
# Create an entry with old URL and token
failed_entry = MockConfigEntry(
domain=DOMAIN,
title="Music Assistant",
data={CONF_URL: "http://old-url:8094", CONF_TOKEN: "old_token"},
unique_id="1234",
)
failed_entry.add_to_hass(hass)

# First, setup the entry with invalid auth to get it into SETUP_ERROR state
with patch(
"homeassistant.components.music_assistant.MusicAssistantClient"
) as mock_client:
mock_client.return_value.connect.side_effect = AuthenticationFailed(
"Invalid token"
)
await hass.config_entries.async_setup(failed_entry.entry_id)
await hass.async_block_till_done()

# Verify entry is in SETUP_ERROR state
assert failed_entry.state is ConfigEntryState.SETUP_ERROR

# Now trigger hassio discovery with valid token
# Mock async_schedule_reload to prevent actual reload attempt
with patch.object(
hass.config_entries, "async_schedule_reload"
) as mock_schedule_reload:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_HASSIO},
data=HASSIO_DATA,
)
await hass.async_block_till_done()

assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

# Verify the entry was updated with new URL and token
assert failed_entry.data[CONF_URL] == "http://addon-music-assistant:8094"
assert failed_entry.data[CONF_TOKEN] == "test_token"

# Verify reload was scheduled
mock_schedule_reload.assert_called_once_with(failed_entry.entry_id)


@pytest.mark.parametrize(
("exception", "error_reason"),
[
Expand Down
42 changes: 40 additions & 2 deletions tests/components/music_assistant/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@
from unittest.mock import AsyncMock, MagicMock

from music_assistant_models.enums import EventType
from music_assistant_models.errors import ActionUnavailable
from music_assistant_models.errors import ActionUnavailable, AuthenticationRequired

from homeassistant.components.music_assistant.const import (
ATTR_CONF_EXPOSE_PLAYER_TO_HA,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.setup import async_setup_component

from .common import setup_integration_from_fixtures, trigger_subscription_callback

from tests.common import MockConfigEntry
from tests.typing import WebSocketGenerator


Expand Down Expand Up @@ -151,3 +157,35 @@ async def test_player_config_expose_to_ha_toggle(
assert entity_registry.async_get(entity_id)
device_entry = device_registry.async_get_device({(DOMAIN, player_id)})
assert device_entry


async def test_authentication_required_triggers_reauth(
hass: HomeAssistant,
music_assistant_client: MagicMock,
) -> None:
"""Test that AuthenticationRequired exception triggers reauth flow."""
# Create a config entry
config_entry = MockConfigEntry(
domain=DOMAIN,
title="Music Assistant",
data={"url": "http://localhost:8095", "token": "old_token"},
unique_id="test_server_id",
)
config_entry.add_to_hass(hass)

# Mock the client to raise AuthenticationRequired during connect
music_assistant_client.connect.side_effect = AuthenticationRequired(
"Authentication required"
)

# Try to set up the integration
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

# Verify the entry is in SETUP_ERROR state (auth failed)
assert config_entry.state is ConfigEntryState.SETUP_ERROR

# Verify a reauth repair issue was created
issue_reg = ir.async_get(hass)
issue_id = f"config_entry_reauth_{DOMAIN}_{config_entry.entry_id}"
assert issue_reg.async_get_issue("homeassistant", issue_id)
Loading