Skip to content

Commit 7c48e6e

Browse files
authored
Delete leftover SmartThings smartapps (home-assistant#157188)
1 parent 38d8da4 commit 7c48e6e

File tree

6 files changed

+213
-30
lines changed

6 files changed

+213
-30
lines changed

homeassistant/components/smartthings/__init__.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from collections.abc import Callable
66
import contextlib
7+
from copy import deepcopy
78
from dataclasses import dataclass
89
from http import HTTPStatus
910
import logging
@@ -22,6 +23,7 @@
2223
SmartThings,
2324
SmartThingsAuthenticationFailedError,
2425
SmartThingsConnectionError,
26+
SmartThingsError,
2527
SmartThingsSinkError,
2628
Status,
2729
)
@@ -413,6 +415,33 @@ def migrate_entities(entity_entry: RegistryEntry) -> dict[str, Any] | None:
413415
minor_version=2,
414416
)
415417

418+
if entry.minor_version < 3:
419+
data = deepcopy(dict(entry.data))
420+
old_data: dict[str, Any] | None = data.pop(OLD_DATA, None)
421+
if old_data is not None:
422+
_LOGGER.info("Found old data during migration")
423+
client = SmartThings(session=async_get_clientsession(hass))
424+
access_token = old_data[CONF_ACCESS_TOKEN]
425+
installed_app_id = old_data[CONF_INSTALLED_APP_ID]
426+
try:
427+
app = await client.get_installed_app(access_token, installed_app_id)
428+
_LOGGER.info("Found old app %s, named %s", app.app_id, app.display_name)
429+
await client.delete_installed_app(access_token, installed_app_id)
430+
await client.delete_smart_app(access_token, app.app_id)
431+
except SmartThingsError as err:
432+
_LOGGER.warning(
433+
"Could not clean up old smart app during migration: %s", err
434+
)
435+
else:
436+
_LOGGER.info("Successfully cleaned up old smart app during migration")
437+
if CONF_TOKEN not in data:
438+
data[OLD_DATA] = {CONF_LOCATION_ID: old_data[CONF_LOCATION_ID]}
439+
hass.config_entries.async_update_entry(
440+
entry,
441+
data=data,
442+
minor_version=3,
443+
)
444+
416445
return True
417446

418447

homeassistant/components/smartthings/config_flow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class SmartThingsConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
2020
"""Handle configuration of SmartThings integrations."""
2121

2222
VERSION = 3
23-
MINOR_VERSION = 2
23+
MINOR_VERSION = 3
2424
DOMAIN = DOMAIN
2525

2626
@property

tests/components/smartthings/conftest.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from collections.abc import Generator
44
import time
5+
from typing import Any
56
from unittest.mock import AsyncMock, patch
67

78
from pysmartthings import (
@@ -13,14 +14,14 @@
1314
SceneResponse,
1415
Subscription,
1516
)
16-
from pysmartthings.models import HealthStatus
17+
from pysmartthings.models import HealthStatus, InstalledApp
1718
import pytest
1819

1920
from homeassistant.components.application_credentials import (
2021
ClientCredential,
2122
async_import_client_credential,
2223
)
23-
from homeassistant.components.smartthings import CONF_INSTALLED_APP_ID
24+
from homeassistant.components.smartthings import CONF_INSTALLED_APP_ID, OLD_DATA
2425
from homeassistant.components.smartthings.const import (
2526
CONF_LOCATION_ID,
2627
CONF_REFRESH_TOKEN,
@@ -91,6 +92,9 @@ def mock_smartthings() -> Generator[AsyncMock]:
9192
client.get_device_health.return_value = DeviceHealth.from_json(
9293
load_fixture("device_health.json", DOMAIN)
9394
)
95+
client.get_installed_app.return_value = InstalledApp.from_json(
96+
load_fixture("installed_app.json", DOMAIN)
97+
)
9498
yield client
9599

96100

@@ -222,6 +226,49 @@ def mock_config_entry(expires_at: int) -> MockConfigEntry:
222226
CONF_INSTALLED_APP_ID: "123",
223227
},
224228
version=3,
229+
minor_version=3,
230+
)
231+
232+
233+
@pytest.fixture
234+
def old_data() -> dict[str, Any]:
235+
"""Return old data for config entry."""
236+
return {
237+
OLD_DATA: {
238+
CONF_ACCESS_TOKEN: "mock-access-token",
239+
CONF_REFRESH_TOKEN: "mock-refresh-token",
240+
CONF_CLIENT_ID: "CLIENT_ID",
241+
CONF_CLIENT_SECRET: "CLIENT_SECRET",
242+
CONF_LOCATION_ID: "397678e5-9995-4a39-9d9f-ae6ba310236c",
243+
CONF_INSTALLED_APP_ID: "123aa123-2be1-4e40-b257-e4ef59083324",
244+
}
245+
}
246+
247+
248+
@pytest.fixture
249+
def mock_migrated_config_entry(
250+
expires_at: int, old_data: dict[str, Any]
251+
) -> MockConfigEntry:
252+
"""Mock a config entry."""
253+
return MockConfigEntry(
254+
domain=DOMAIN,
255+
title="My home",
256+
unique_id="397678e5-9995-4a39-9d9f-ae6ba310236c",
257+
data={
258+
"auth_implementation": DOMAIN,
259+
"token": {
260+
"access_token": "mock-access-token",
261+
"refresh_token": "mock-refresh-token",
262+
"expires_at": expires_at,
263+
"scope": " ".join(SCOPES),
264+
"access_tier": 0,
265+
"installed_app_id": "5aaaa925-2be1-4e40-b257-e4ef59083324",
266+
},
267+
CONF_LOCATION_ID: "397678e5-9995-4a39-9d9f-ae6ba310236c",
268+
CONF_INSTALLED_APP_ID: "123",
269+
**old_data,
270+
},
271+
version=3,
225272
minor_version=2,
226273
)
227274

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"installedAppId": "123aa123-2be1-4e40-b257-e4ef59083324",
3+
"installedAppType": "WEBHOOK_SMART_APP",
4+
"installedAppStatus": "PENDING",
5+
"displayName": "pysmartthings",
6+
"appId": "c6cde2b0-203e-44cf-a510-3b3ed4706996",
7+
"referenceId": null,
8+
"locationId": "397678e5-9995-4a39-9d9f-ae6ba310236b",
9+
"owner": {
10+
"ownerType": "USER",
11+
"ownerId": "3c19270b-fca6-5cde-82bc-86a37e52cfa8"
12+
},
13+
"notices": [],
14+
"createdDate": "2018-12-19T02:49:58Z",
15+
"lastUpdatedDate": "2018-12-19T02:49:58Z",
16+
"ui": {
17+
"pluginId": null,
18+
"dashboardCardsEnabled": false,
19+
"preInstallDashboardCardsEnabled": false
20+
},
21+
"iconImage": {
22+
"url": null
23+
},
24+
"classifications": ["AUTOMATION"]
25+
}

tests/components/smartthings/test_config_flow.py

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,12 @@
77

88
from homeassistant.components.smartthings import OLD_DATA
99
from homeassistant.components.smartthings.const import (
10-
CONF_INSTALLED_APP_ID,
1110
CONF_LOCATION_ID,
12-
CONF_REFRESH_TOKEN,
1311
CONF_SUBSCRIPTION_ID,
1412
DOMAIN,
1513
)
1614
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
17-
from homeassistant.const import (
18-
CONF_ACCESS_TOKEN,
19-
CONF_CLIENT_ID,
20-
CONF_CLIENT_SECRET,
21-
CONF_TOKEN,
22-
)
15+
from homeassistant.const import CONF_TOKEN
2316
from homeassistant.core import HomeAssistant
2417
from homeassistant.data_entry_flow import FlowResultType
2518
from homeassistant.helpers import config_entry_oauth2_flow
@@ -489,14 +482,7 @@ async def test_migration(
489482
mock_old_config_entry.data[CONF_TOKEN].pop("expires_at")
490483
assert mock_old_config_entry.data == {
491484
"auth_implementation": DOMAIN,
492-
"old_data": {
493-
CONF_ACCESS_TOKEN: "mock-access-token",
494-
CONF_REFRESH_TOKEN: "mock-refresh-token",
495-
CONF_CLIENT_ID: "CLIENT_ID",
496-
CONF_CLIENT_SECRET: "CLIENT_SECRET",
497-
CONF_LOCATION_ID: "397678e5-9995-4a39-9d9f-ae6ba310236c",
498-
CONF_INSTALLED_APP_ID: "123aa123-2be1-4e40-b257-e4ef59083324",
499-
},
485+
"old_data": {CONF_LOCATION_ID: "397678e5-9995-4a39-9d9f-ae6ba310236c"},
500486
CONF_TOKEN: {
501487
"refresh_token": "new-refresh-token",
502488
"access_token": "new-access-token",
@@ -513,7 +499,19 @@ async def test_migration(
513499
}
514500
assert mock_old_config_entry.unique_id == "397678e5-9995-4a39-9d9f-ae6ba310236c"
515501
assert mock_old_config_entry.version == 3
516-
assert mock_old_config_entry.minor_version == 2
502+
assert mock_old_config_entry.minor_version == 3
503+
mock_smartthings.get_installed_app.assert_called_once_with(
504+
"mock-access-token",
505+
"123aa123-2be1-4e40-b257-e4ef59083324",
506+
)
507+
mock_smartthings.delete_installed_app.assert_called_once_with(
508+
"mock-access-token",
509+
"123aa123-2be1-4e40-b257-e4ef59083324",
510+
)
511+
mock_smartthings.delete_smart_app.assert_called_once_with(
512+
"mock-access-token",
513+
"c6cde2b0-203e-44cf-a510-3b3ed4706996",
514+
)
517515

518516

519517
@pytest.mark.usefixtures("current_request_with_host", "use_cloud")
@@ -572,21 +570,26 @@ async def test_migration_wrong_location(
572570
assert result["reason"] == "reauth_location_mismatch"
573571
assert mock_old_config_entry.state is ConfigEntryState.SETUP_ERROR
574572
assert mock_old_config_entry.data == {
575-
OLD_DATA: {
576-
CONF_ACCESS_TOKEN: "mock-access-token",
577-
CONF_REFRESH_TOKEN: "mock-refresh-token",
578-
CONF_CLIENT_ID: "CLIENT_ID",
579-
CONF_CLIENT_SECRET: "CLIENT_SECRET",
580-
CONF_LOCATION_ID: "397678e5-9995-4a39-9d9f-ae6ba310236c",
581-
CONF_INSTALLED_APP_ID: "123aa123-2be1-4e40-b257-e4ef59083324",
582-
}
573+
OLD_DATA: {CONF_LOCATION_ID: "397678e5-9995-4a39-9d9f-ae6ba310236c"}
583574
}
584575
assert (
585576
mock_old_config_entry.unique_id
586577
== "appid123-2be1-4e40-b257-e4ef59083324_397678e5-9995-4a39-9d9f-ae6ba310236c"
587578
)
588579
assert mock_old_config_entry.version == 3
589-
assert mock_old_config_entry.minor_version == 2
580+
assert mock_old_config_entry.minor_version == 3
581+
mock_smartthings.get_installed_app.assert_called_once_with(
582+
"mock-access-token",
583+
"123aa123-2be1-4e40-b257-e4ef59083324",
584+
)
585+
mock_smartthings.delete_installed_app.assert_called_once_with(
586+
"mock-access-token",
587+
"123aa123-2be1-4e40-b257-e4ef59083324",
588+
)
589+
mock_smartthings.delete_smart_app.assert_called_once_with(
590+
"mock-access-token",
591+
"c6cde2b0-203e-44cf-a510-3b3ed4706996",
592+
)
590593

591594

592595
@pytest.mark.usefixtures("current_request_with_host")

tests/components/smartthings/test_init.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
DeviceResponse,
1010
DeviceStatus,
1111
Lifecycle,
12+
SmartThingsConnectionError,
1213
SmartThingsSinkError,
1314
Subscription,
1415
)
@@ -22,7 +23,7 @@
2223
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
2324
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
2425
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
25-
from homeassistant.components.smartthings import EVENT_BUTTON
26+
from homeassistant.components.smartthings import EVENT_BUTTON, OLD_DATA
2627
from homeassistant.components.smartthings.const import (
2728
CONF_INSTALLED_APP_ID,
2829
CONF_LOCATION_ID,
@@ -750,3 +751,81 @@ async def test_oauth_implementation_not_available(
750751
await hass.async_block_till_done()
751752

752753
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
754+
755+
756+
async def test_3_3_migration(
757+
hass: HomeAssistant,
758+
mock_migrated_config_entry: MockConfigEntry,
759+
mock_setup_entry: AsyncMock,
760+
mock_smartthings: AsyncMock,
761+
) -> None:
762+
"""Test migration from minor version 2 to 3."""
763+
mock_migrated_config_entry.add_to_hass(hass)
764+
765+
assert OLD_DATA in mock_migrated_config_entry.data
766+
767+
await hass.config_entries.async_setup(mock_migrated_config_entry.entry_id)
768+
await hass.async_block_till_done()
769+
assert mock_migrated_config_entry.minor_version == 3
770+
771+
assert OLD_DATA not in mock_migrated_config_entry.data
772+
mock_smartthings.get_installed_app.assert_called_once_with(
773+
"mock-access-token",
774+
"123aa123-2be1-4e40-b257-e4ef59083324",
775+
)
776+
mock_smartthings.delete_installed_app.assert_called_once_with(
777+
"mock-access-token",
778+
"123aa123-2be1-4e40-b257-e4ef59083324",
779+
)
780+
mock_smartthings.delete_smart_app.assert_called_once_with(
781+
"mock-access-token",
782+
"c6cde2b0-203e-44cf-a510-3b3ed4706996",
783+
)
784+
785+
786+
async def test_3_3_migration_fail(
787+
hass: HomeAssistant,
788+
mock_migrated_config_entry: MockConfigEntry,
789+
mock_setup_entry: AsyncMock,
790+
mock_smartthings: AsyncMock,
791+
) -> None:
792+
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
793+
mock_migrated_config_entry.add_to_hass(hass)
794+
795+
mock_smartthings.get_installed_app.side_effect = SmartThingsConnectionError("Boom")
796+
797+
assert OLD_DATA in mock_migrated_config_entry.data
798+
799+
await hass.config_entries.async_setup(mock_migrated_config_entry.entry_id)
800+
await hass.async_block_till_done()
801+
assert mock_migrated_config_entry.minor_version == 3
802+
803+
assert OLD_DATA not in mock_migrated_config_entry.data
804+
mock_smartthings.get_installed_app.assert_called_once_with(
805+
"mock-access-token",
806+
"123aa123-2be1-4e40-b257-e4ef59083324",
807+
)
808+
mock_smartthings.delete_installed_app.assert_not_called()
809+
mock_smartthings.delete_smart_app.assert_not_called()
810+
811+
812+
@pytest.mark.parametrize("old_data", [({})])
813+
async def test_3_3_migration_no_old_data(
814+
hass: HomeAssistant,
815+
mock_migrated_config_entry: MockConfigEntry,
816+
mock_setup_entry: AsyncMock,
817+
mock_smartthings: AsyncMock,
818+
) -> None:
819+
"""Test migration from minor version 2 to 3 when no old data is present."""
820+
mock_migrated_config_entry.add_to_hass(hass)
821+
822+
assert OLD_DATA not in mock_migrated_config_entry.data
823+
824+
await hass.config_entries.async_setup(mock_migrated_config_entry.entry_id)
825+
await hass.async_block_till_done()
826+
assert mock_migrated_config_entry.minor_version == 3
827+
828+
assert OLD_DATA not in mock_migrated_config_entry.data
829+
mock_smartthings.get_installed_app.assert_not_called()
830+
mock_smartthings.delete_installed_app.assert_not_called()
831+
mock_smartthings.delete_smart_app.assert_not_called()

0 commit comments

Comments
 (0)