diff --git a/homeassistant/components/mastodon/__init__.py b/homeassistant/components/mastodon/__init__.py index 3c305ca655b..77e66b6e45c 100644 --- a/homeassistant/components/mastodon/__init__.py +++ b/homeassistant/components/mastodon/__init__.py @@ -17,10 +17,11 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery +from homeassistant.util import slugify -from .const import CONF_BASE_URL, DOMAIN +from .const import CONF_BASE_URL, DOMAIN, LOGGER from .coordinator import MastodonCoordinator -from .utils import create_mastodon_client +from .utils import construct_mastodon_username, create_mastodon_client PLATFORMS: list[Platform] = [Platform.NOTIFY, Platform.SENSOR] @@ -80,6 +81,39 @@ async def async_unload_entry(hass: HomeAssistant, entry: MastodonConfigEntry) -> ) +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old config.""" + + if entry.version == 1 and entry.minor_version == 1: + # Version 1.1 had the unique_id as client_id, this isn't necessarily unique + LOGGER.debug("Migrating config entry from version %s", entry.version) + + try: + _, instance, account = await hass.async_add_executor_job( + setup_mastodon, + entry, + ) + except MastodonError as ex: + LOGGER.error("Migration failed with error %s", ex) + return False + + entry.minor_version = 2 + + hass.config_entries.async_update_entry( + entry, + unique_id=slugify(construct_mastodon_username(instance, account)), + ) + + LOGGER.info( + "Entry %s successfully migrated to version %s.%s", + entry.entry_id, + entry.version, + entry.minor_version, + ) + + return True + + def setup_mastodon(entry: ConfigEntry) -> tuple[Mastodon, dict, dict]: """Get mastodon details.""" client = create_mastodon_client( diff --git a/homeassistant/components/mastodon/config_flow.py b/homeassistant/components/mastodon/config_flow.py index 7d1c9396cbb..4e856275736 100644 --- a/homeassistant/components/mastodon/config_flow.py +++ b/homeassistant/components/mastodon/config_flow.py @@ -20,6 +20,7 @@ from homeassistant.helpers.selector import ( TextSelectorType, ) from homeassistant.helpers.typing import ConfigType +from homeassistant.util import slugify from .const import CONF_BASE_URL, DEFAULT_URL, DOMAIN, LOGGER from .utils import construct_mastodon_username, create_mastodon_client @@ -47,6 +48,7 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 + MINOR_VERSION = 2 config_entry: ConfigEntry def check_connection( @@ -105,10 +107,6 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" errors: dict[str, str] | None = None if user_input: - self._async_abort_entries_match( - {CONF_CLIENT_ID: user_input[CONF_CLIENT_ID]} - ) - instance, account, errors = await self.hass.async_add_executor_job( self.check_connection, user_input[CONF_BASE_URL], @@ -119,7 +117,8 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN): if not errors: name = construct_mastodon_username(instance, account) - await self.async_set_unique_id(user_input[CONF_CLIENT_ID]) + await self.async_set_unique_id(slugify(name)) + self._abort_if_unique_id_configured() return self.async_create_entry( title=name, data=user_input, @@ -148,7 +147,8 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN): ) if not errors: - await self.async_set_unique_id(client_id) + name = construct_mastodon_username(instance, account) + await self.async_set_unique_id(slugify(name)) self._abort_if_unique_id_configured() if not name: diff --git a/tests/components/mastodon/conftest.py b/tests/components/mastodon/conftest.py index 03c3e754c11..c64de44d496 100644 --- a/tests/components/mastodon/conftest.py +++ b/tests/components/mastodon/conftest.py @@ -53,5 +53,7 @@ def mock_config_entry() -> MockConfigEntry: CONF_ACCESS_TOKEN: "access_token", }, entry_id="01J35M4AH9HYRC2V0G6RNVNWJH", - unique_id="client_id", + unique_id="trwnh_mastodon_social", + version=1, + minor_version=2, ) diff --git a/tests/components/mastodon/snapshots/test_init.ambr b/tests/components/mastodon/snapshots/test_init.ambr new file mode 100644 index 00000000000..37fa765acea --- /dev/null +++ b/tests/components/mastodon/snapshots/test_init.ambr @@ -0,0 +1,33 @@ +# serializer version: 1 +# name: test_device_info + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'mastodon', + 'trwnh_mastodon_social', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Mastodon gGmbH', + 'model': '@trwnh@mastodon.social', + 'model_id': None, + 'name': 'Mastodon @trwnh@mastodon.social', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '4.0.0rc1', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/mastodon/snapshots/test_sensor.ambr b/tests/components/mastodon/snapshots/test_sensor.ambr index f94e34c00ab..c8df8cdab19 100644 --- a/tests/components/mastodon/snapshots/test_sensor.ambr +++ b/tests/components/mastodon/snapshots/test_sensor.ambr @@ -30,7 +30,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'followers', - 'unique_id': 'client_id_followers', + 'unique_id': 'trwnh_mastodon_social_followers', 'unit_of_measurement': 'accounts', }) # --- @@ -80,7 +80,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'following', - 'unique_id': 'client_id_following', + 'unique_id': 'trwnh_mastodon_social_following', 'unit_of_measurement': 'accounts', }) # --- @@ -130,7 +130,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'posts', - 'unique_id': 'client_id_posts', + 'unique_id': 'trwnh_mastodon_social_posts', 'unit_of_measurement': 'posts', }) # --- diff --git a/tests/components/mastodon/test_config_flow.py b/tests/components/mastodon/test_config_flow.py index 01cdc061d3e..073a6534d7d 100644 --- a/tests/components/mastodon/test_config_flow.py +++ b/tests/components/mastodon/test_config_flow.py @@ -44,7 +44,7 @@ async def test_full_flow( CONF_CLIENT_SECRET: "client_secret", CONF_ACCESS_TOKEN: "access_token", } - assert result["result"].unique_id == "client_id" + assert result["result"].unique_id == "trwnh_mastodon_social" @pytest.mark.parametrize( diff --git a/tests/components/mastodon/test_init.py b/tests/components/mastodon/test_init.py index 53796e39782..c3d0728fe08 100644 --- a/tests/components/mastodon/test_init.py +++ b/tests/components/mastodon/test_init.py @@ -3,15 +3,36 @@ from unittest.mock import AsyncMock from mastodon.Mastodon import MastodonError +from syrupy.assertion import SnapshotAssertion +from homeassistant.components.mastodon.config_flow import MastodonConfigFlow +from homeassistant.components.mastodon.const import CONF_BASE_URL, DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from . import setup_integration from tests.common import MockConfigEntry +async def test_device_info( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device registry integration.""" + await setup_integration(hass, mock_config_entry) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry.unique_id)} + ) + assert device_entry is not None + assert device_entry == snapshot + + async def test_initialization_failure( hass: HomeAssistant, mock_mastodon_client: AsyncMock, @@ -23,3 +44,39 @@ async def test_initialization_failure( await setup_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_migrate( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, +) -> None: + """Test migration.""" + # Setup the config entry + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + CONF_ACCESS_TOKEN: "access_token", + }, + title="@trwnh@mastodon.social", + unique_id="client_id", + 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() + + # Check migration was successful + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.data == { + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + CONF_ACCESS_TOKEN: "access_token", + } + assert config_entry.version == MastodonConfigFlow.VERSION + assert config_entry.minor_version == MastodonConfigFlow.MINOR_VERSION + assert config_entry.unique_id == "trwnh_mastodon_social"