Migrate Mastodon unique id (#123877)

* Migrate unique id

* Fix unique id check

* Switch to minor version and other fixes
This commit is contained in:
Andrew Jackson 2024-08-14 11:55:59 +01:00 committed by GitHub
parent d50bac3b3e
commit ac223e64f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 139 additions and 13 deletions

View File

@ -17,10 +17,11 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import discovery 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 .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] 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]: def setup_mastodon(entry: ConfigEntry) -> tuple[Mastodon, dict, dict]:
"""Get mastodon details.""" """Get mastodon details."""
client = create_mastodon_client( client = create_mastodon_client(

View File

@ -20,6 +20,7 @@ from homeassistant.helpers.selector import (
TextSelectorType, TextSelectorType,
) )
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
from .const import CONF_BASE_URL, DEFAULT_URL, DOMAIN, LOGGER from .const import CONF_BASE_URL, DEFAULT_URL, DOMAIN, LOGGER
from .utils import construct_mastodon_username, create_mastodon_client from .utils import construct_mastodon_username, create_mastodon_client
@ -47,6 +48,7 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow.""" """Handle a config flow."""
VERSION = 1 VERSION = 1
MINOR_VERSION = 2
config_entry: ConfigEntry config_entry: ConfigEntry
def check_connection( def check_connection(
@ -105,10 +107,6 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
errors: dict[str, str] | None = None errors: dict[str, str] | None = None
if user_input: 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( instance, account, errors = await self.hass.async_add_executor_job(
self.check_connection, self.check_connection,
user_input[CONF_BASE_URL], user_input[CONF_BASE_URL],
@ -119,7 +117,8 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN):
if not errors: if not errors:
name = construct_mastodon_username(instance, account) 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( return self.async_create_entry(
title=name, title=name,
data=user_input, data=user_input,
@ -148,7 +147,8 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN):
) )
if not errors: 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() self._abort_if_unique_id_configured()
if not name: if not name:

View File

@ -53,5 +53,7 @@ def mock_config_entry() -> MockConfigEntry:
CONF_ACCESS_TOKEN: "access_token", CONF_ACCESS_TOKEN: "access_token",
}, },
entry_id="01J35M4AH9HYRC2V0G6RNVNWJH", entry_id="01J35M4AH9HYRC2V0G6RNVNWJH",
unique_id="client_id", unique_id="trwnh_mastodon_social",
version=1,
minor_version=2,
) )

View File

@ -0,0 +1,33 @@
# serializer version: 1
# name: test_device_info
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': <DeviceEntryType.SERVICE: 'service'>,
'hw_version': None,
'id': <ANY>,
'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': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '4.0.0rc1',
'via_device_id': None,
})
# ---

View File

@ -30,7 +30,7 @@
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
'translation_key': 'followers', 'translation_key': 'followers',
'unique_id': 'client_id_followers', 'unique_id': 'trwnh_mastodon_social_followers',
'unit_of_measurement': 'accounts', 'unit_of_measurement': 'accounts',
}) })
# --- # ---
@ -80,7 +80,7 @@
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
'translation_key': 'following', 'translation_key': 'following',
'unique_id': 'client_id_following', 'unique_id': 'trwnh_mastodon_social_following',
'unit_of_measurement': 'accounts', 'unit_of_measurement': 'accounts',
}) })
# --- # ---
@ -130,7 +130,7 @@
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
'translation_key': 'posts', 'translation_key': 'posts',
'unique_id': 'client_id_posts', 'unique_id': 'trwnh_mastodon_social_posts',
'unit_of_measurement': 'posts', 'unit_of_measurement': 'posts',
}) })
# --- # ---

View File

@ -44,7 +44,7 @@ async def test_full_flow(
CONF_CLIENT_SECRET: "client_secret", CONF_CLIENT_SECRET: "client_secret",
CONF_ACCESS_TOKEN: "access_token", CONF_ACCESS_TOKEN: "access_token",
} }
assert result["result"].unique_id == "client_id" assert result["result"].unique_id == "trwnh_mastodon_social"
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -3,15 +3,36 @@
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from mastodon.Mastodon import MastodonError 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.config_entries import ConfigEntryState
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration from . import setup_integration
from tests.common import MockConfigEntry 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( async def test_initialization_failure(
hass: HomeAssistant, hass: HomeAssistant,
mock_mastodon_client: AsyncMock, mock_mastodon_client: AsyncMock,
@ -23,3 +44,39 @@ async def test_initialization_failure(
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY 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"