From 6b05f51413e491e05f17f1c8d841dd1eec5e0f8e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 16 Oct 2023 10:28:11 +0200 Subject: [PATCH] Migrate unique id in Trafikverket Camera (#101937) --- .../trafikverket_camera/__init__.py | 44 ++++- .../trafikverket_camera/config_flow.py | 16 +- .../trafikverket_camera/conftest.py | 3 +- .../trafikverket_camera/test_config_flow.py | 6 +- .../trafikverket_camera/test_coordinator.py | 9 +- .../trafikverket_camera/test_init.py | 152 +++++++++++++++++- 6 files changed, 215 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/trafikverket_camera/__init__.py b/homeassistant/components/trafikverket_camera/__init__.py index 5575f32788a..d9d28cfe13b 100644 --- a/homeassistant/components/trafikverket_camera/__init__.py +++ b/homeassistant/components/trafikverket_camera/__init__.py @@ -1,15 +1,23 @@ """The trafikverket_camera component.""" from __future__ import annotations +import logging + +from pytrafikverket.trafikverket_camera import TrafikverketCamera + from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN, PLATFORMS +from .const import CONF_LOCATION, DOMAIN, PLATFORMS from .coordinator import TVDataUpdateCoordinator CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Trafikverket Camera from a config entry.""" @@ -30,3 +38,37 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old entry.""" + # Change entry unique id from location to camera id + if entry.version == 1: + location = entry.data[CONF_LOCATION] + api_key = entry.data[CONF_API_KEY] + + web_session = async_get_clientsession(hass) + camera_api = TrafikverketCamera(web_session, api_key) + + try: + camera_info = await camera_api.async_get_camera(location) + except Exception: # pylint: disable=broad-except + _LOGGER.error( + "Could not migrate the config entry. No connection to the api" + ) + return False + + if camera_id := camera_info.camera_id: + entry.version = 2 + _LOGGER.debug( + "Migrate Trafikverket Camera config entry unique id to %s", + camera_id, + ) + hass.config_entries.async_update_entry( + entry, + unique_id=f"{DOMAIN}-{camera_id}", + ) + return True + _LOGGER.error("Could not migrate the config entry. Camera has no id") + return False + return True diff --git a/homeassistant/components/trafikverket_camera/config_flow.py b/homeassistant/components/trafikverket_camera/config_flow.py index d4a282cb344..e75bc0bfa30 100644 --- a/homeassistant/components/trafikverket_camera/config_flow.py +++ b/homeassistant/components/trafikverket_camera/config_flow.py @@ -25,17 +25,18 @@ from .const import CONF_LOCATION, DOMAIN class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Trafikverket Camera integration.""" - VERSION = 1 + VERSION = 2 entry: config_entries.ConfigEntry | None async def validate_input( self, sensor_api: str, location: str - ) -> tuple[dict[str, str], str | None]: + ) -> tuple[dict[str, str], str | None, str | None]: """Validate input from user input.""" errors: dict[str, str] = {} camera_info: CameraInfo | None = None camera_location: str | None = None + camera_id: str | None = None web_session = async_get_clientsession(self.hass) camera_api = TrafikverketCamera(web_session, sensor_api) @@ -51,12 +52,13 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" if camera_info: + camera_id = camera_info.camera_id if _location := camera_info.location: camera_location = _location else: camera_location = camera_info.camera_name - return (errors, camera_location) + return (errors, camera_location, camera_id) async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle re-authentication with Trafikverket.""" @@ -74,7 +76,7 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): api_key = user_input[CONF_API_KEY] assert self.entry is not None - errors, _ = await self.validate_input( + errors, _, _ = await self.validate_input( api_key, self.entry.data[CONF_LOCATION] ) @@ -109,11 +111,13 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): api_key = user_input[CONF_API_KEY] location = user_input[CONF_LOCATION] - errors, camera_location = await self.validate_input(api_key, location) + errors, camera_location, camera_id = await self.validate_input( + api_key, location + ) if not errors: assert camera_location - await self.async_set_unique_id(f"{DOMAIN}-{camera_location}") + await self.async_set_unique_id(f"{DOMAIN}-{camera_id}") self._abort_if_unique_id_configured() return self.async_create_entry( title=camera_location, diff --git a/tests/components/trafikverket_camera/conftest.py b/tests/components/trafikverket_camera/conftest.py index 95c145bbeb3..a4902ac2950 100644 --- a/tests/components/trafikverket_camera/conftest.py +++ b/tests/components/trafikverket_camera/conftest.py @@ -32,7 +32,8 @@ async def load_integration_from_entry( source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - unique_id="123", + version=2, + unique_id="trafikverket_camera-1234", title="Test location", ) diff --git a/tests/components/trafikverket_camera/test_config_flow.py b/tests/components/trafikverket_camera/test_config_flow.py index ae3410d20b3..b53763c0ac7 100644 --- a/tests/components/trafikverket_camera/test_config_flow.py +++ b/tests/components/trafikverket_camera/test_config_flow.py @@ -53,7 +53,7 @@ async def test_form(hass: HomeAssistant, get_camera: CameraInfo) -> None: "location": "Test location", } assert len(mock_setup_entry.mock_calls) == 1 - assert result2["result"].unique_id == "trafikverket_camera-Test location" + assert result2["result"].unique_id == "trafikverket_camera-1234" async def test_form_no_location_data( @@ -90,7 +90,7 @@ async def test_form_no_location_data( "location": "Test Camera", } assert len(mock_setup_entry.mock_calls) == 1 - assert result2["result"].unique_id == "trafikverket_camera-Test Camera" + assert result2["result"].unique_id == "trafikverket_camera-1234" @pytest.mark.parametrize( @@ -153,6 +153,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: CONF_LOCATION: "Test location", }, unique_id="1234", + version=2, ) entry.add_to_hass(hass) @@ -225,6 +226,7 @@ async def test_reauth_flow_error( CONF_LOCATION: "Test location", }, unique_id="1234", + version=2, ) entry.add_to_hass(hass) await hass.async_block_till_done() diff --git a/tests/components/trafikverket_camera/test_coordinator.py b/tests/components/trafikverket_camera/test_coordinator.py index 2b21ce935b2..4183aa9fffa 100644 --- a/tests/components/trafikverket_camera/test_coordinator.py +++ b/tests/components/trafikverket_camera/test_coordinator.py @@ -40,7 +40,8 @@ async def test_coordinator( source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - unique_id="123", + version=2, + unique_id="trafikverket_camera-1234", title="Test location", ) entry.add_to_hass(hass) @@ -100,7 +101,8 @@ async def test_coordinator_failed_update( source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - unique_id="123", + version=2, + unique_id="trafikverket_camera-1234", title="Test location", ) entry.add_to_hass(hass) @@ -133,7 +135,8 @@ async def test_coordinator_failed_get_image( source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - unique_id="123", + version=2, + unique_id="trafikverket_camera-1234", title="Test location", ) entry.add_to_hass(hass) diff --git a/tests/components/trafikverket_camera/test_init.py b/tests/components/trafikverket_camera/test_init.py index d9de0a830a6..83a3fc1486a 100644 --- a/tests/components/trafikverket_camera/test_init.py +++ b/tests/components/trafikverket_camera/test_init.py @@ -1,14 +1,18 @@ """Test for Trafikverket Ferry component Init.""" from __future__ import annotations +from datetime import datetime from unittest.mock import patch +from pytrafikverket.exceptions import UnknownError from pytrafikverket.trafikverket_camera import CameraInfo from homeassistant import config_entries +from homeassistant.components.trafikverket_camera import async_migrate_entry from homeassistant.components.trafikverket_camera.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util from . import ENTRY_CONFIG @@ -31,7 +35,8 @@ async def test_setup_entry( source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - unique_id="123", + version=2, + unique_id="trafikverket_camera-1234", title="Test location", ) entry.add_to_hass(hass) @@ -62,7 +67,8 @@ async def test_unload_entry( source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - unique_id="321", + version=2, + unique_id="trafikverket_camera-1234", title="Test location", ) entry.add_to_hass(hass) @@ -78,3 +84,145 @@ async def test_unload_entry( assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + + +async def test_migrate_entry( + hass: HomeAssistant, + get_camera: CameraInfo, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test migrate entry to version 2.""" + aioclient_mock.get( + "https://www.testurl.com/test_photo.jpg?type=fullsize", content=b"0123456789" + ) + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="trafikverket_camera-Test location", + title="Test location", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_camera.coordinator.TrafikverketCamera.async_get_camera", + return_value=get_camera, + ) as mock_tvt_camera: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.version == 2 + assert entry.unique_id == "trafikverket_camera-1234" + assert len(mock_tvt_camera.mock_calls) == 2 + + +async def test_migrate_entry_fails_with_error( + hass: HomeAssistant, + get_camera: CameraInfo, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test migrate entry fails with api error.""" + aioclient_mock.get( + "https://www.testurl.com/test_photo.jpg?type=fullsize", content=b"0123456789" + ) + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="trafikverket_camera-Test location", + title="Test location", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_camera.coordinator.TrafikverketCamera.async_get_camera", + side_effect=UnknownError, + ) as mock_tvt_camera: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR + assert entry.version == 1 + assert entry.unique_id == "trafikverket_camera-Test location" + assert len(mock_tvt_camera.mock_calls) == 1 + + +async def test_migrate_entry_fails_no_id( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test migrate entry fails, camera returns no id.""" + aioclient_mock.get( + "https://www.testurl.com/test_photo.jpg?type=fullsize", content=b"0123456789" + ) + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="trafikverket_camera-Test location", + title="Test location", + ) + entry.add_to_hass(hass) + + _camera = CameraInfo( + camera_name="Test_camera", + camera_id=None, + active=True, + deleted=False, + description="Test Camera for testing", + direction="180", + fullsizephoto=True, + location="Test location", + modified=datetime(2022, 4, 4, 4, 4, 4, tzinfo=dt_util.UTC), + phototime=datetime(2022, 4, 4, 4, 4, 4, tzinfo=dt_util.UTC), + photourl="https://www.testurl.com/test_photo.jpg", + status="Running", + camera_type="Road", + ) + + with patch( + "homeassistant.components.trafikverket_camera.coordinator.TrafikverketCamera.async_get_camera", + return_value=_camera, + ) as mock_tvt_camera: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR + assert entry.version == 1 + assert entry.unique_id == "trafikverket_camera-Test location" + assert len(mock_tvt_camera.mock_calls) == 1 + + +async def test_no_migration_needed( + hass: HomeAssistant, + get_camera: CameraInfo, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test migrate entry fails, camera returns no id.""" + aioclient_mock.get( + "https://www.testurl.com/test_photo.jpg?type=fullsize", content=b"0123456789" + ) + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + version=2, + entry_id="1234", + unique_id="trafikverket_camera-1234", + title="Test location", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_camera.coordinator.TrafikverketCamera.async_get_camera", + return_value=get_camera, + ): + assert await async_migrate_entry(hass, entry) is True