Enhance SmartThings component subscription (#21124)

* Move to config v2 to store SmartApp oauth keys

* Add migration functionality.

* Regenerate refresh token on periodic basis

* Fix regenerate and misc. optimizations

* Review feedback

* Subscription sync logic now performs a difference operation

* Removed config entry reloading.
This commit is contained in:
Andrew Sayre authored and Martin Hjelmare committed 2019-02-22 20:35:12 +01:00
1 parent d9712027e8
commit 8b38b82e73
14 files changed
+529 -275

No files matched your search

+113 -36
View File
@@ -5,7 +5,9 @@ from uuid import uuid4
from pysmartthings import AppEntity, Capability
from homeassistant.components.smartthings import smartapp
from homeassistant.components.smartthings.const import DATA_MANAGER, DOMAIN
from homeassistant.components.smartthings.const import (
CONF_INSTALLED_APP_ID, CONF_INSTALLED_APPS, CONF_LOCATION_ID,
CONF_REFRESH_TOKEN, DATA_MANAGER, DOMAIN)
from tests.common import mock_coro
@@ -35,31 +37,26 @@ async def test_update_app_updated_needed(hass, app):
assert mock_app.classifications == app.classifications
async def test_smartapp_install_abort_if_no_other(
async def test_smartapp_install_store_if_no_other(
hass, smartthings_mock, device_factory):
"""Test aborts if no other app was configured already."""
# Arrange
api = smartthings_mock.return_value
api.create_subscription.return_value = mock_coro()
app = Mock()
app.app_id = uuid4()
request = Mock()
request.installed_app_id = uuid4()
request.auth_token = uuid4()
request.location_id = uuid4()
devices = [
device_factory('', [Capability.battery, 'ping']),
device_factory('', [Capability.switch, Capability.switch_level]),
device_factory('', [Capability.switch])
]
api.devices = Mock()
api.devices.return_value = mock_coro(return_value=devices)
request.installed_app_id = str(uuid4())
request.auth_token = str(uuid4())
request.location_id = str(uuid4())
request.refresh_token = str(uuid4())
# Act
await smartapp.smartapp_install(hass, request, None, app)
# Assert
entries = hass.config_entries.async_entries('smartthings')
assert not entries
assert api.create_subscription.call_count == 3
data = hass.data[DOMAIN][CONF_INSTALLED_APPS][0]
assert data[CONF_REFRESH_TOKEN] == request.refresh_token
assert data[CONF_LOCATION_ID] == request.location_id
assert data[CONF_INSTALLED_APP_ID] == request.installed_app_id
async def test_smartapp_install_creates_flow(
@@ -68,12 +65,12 @@ async def test_smartapp_install_creates_flow(
# Arrange
setattr(hass.config_entries, '_entries', [config_entry])
api = smartthings_mock.return_value
api.create_subscription.return_value = mock_coro()
app = Mock()
app.app_id = config_entry.data['app_id']
request = Mock()
request.installed_app_id = str(uuid4())
request.auth_token = str(uuid4())
request.refresh_token = str(uuid4())
request.location_id = location.location_id
devices = [
device_factory('', [Capability.battery, 'ping']),
@@ -88,42 +85,42 @@ async def test_smartapp_install_creates_flow(
await hass.async_block_till_done()
entries = hass.config_entries.async_entries('smartthings')
assert len(entries) == 2
assert api.create_subscription.call_count == 3
assert entries[1].data['app_id'] == app.app_id
assert entries[1].data['installed_app_id'] == request.installed_app_id
assert entries[1].data['location_id'] == request.location_id
assert entries[1].data['access_token'] == \
config_entry.data['access_token']
assert entries[1].data['refresh_token'] == request.refresh_token
assert entries[1].data['client_secret'] == \
config_entry.data['client_secret']
assert entries[1].data['client_id'] == config_entry.data['client_id']
assert entries[1].title == location.name
async def test_smartapp_update_syncs_subs(
hass, smartthings_mock, config_entry, location, device_factory):
"""Test update synchronizes subscriptions."""
async def test_smartapp_update_saves_token(
hass, smartthings_mock, location, device_factory):
"""Test update saves token."""
# Arrange
setattr(hass.config_entries, '_entries', [config_entry])
entry = Mock()
entry.data = {
'installed_app_id': str(uuid4()),
'app_id': str(uuid4())
}
entry.domain = DOMAIN
setattr(hass.config_entries, '_entries', [entry])
app = Mock()
app.app_id = config_entry.data['app_id']
api = smartthings_mock.return_value
api.delete_subscriptions = Mock()
api.delete_subscriptions.return_value = mock_coro()
api.create_subscription.return_value = mock_coro()
app.app_id = entry.data['app_id']
request = Mock()
request.installed_app_id = str(uuid4())
request.installed_app_id = entry.data['installed_app_id']
request.auth_token = str(uuid4())
request.refresh_token = str(uuid4())
request.location_id = location.location_id
devices = [
device_factory('', [Capability.battery, 'ping']),
device_factory('', [Capability.switch, Capability.switch_level]),
device_factory('', [Capability.switch])
]
api.devices = Mock()
api.devices.return_value = mock_coro(return_value=devices)
# Act
await smartapp.smartapp_update(hass, request, None, app)
# Assert
assert api.create_subscription.call_count == 3
assert api.delete_subscriptions.call_count == 1
assert entry.data[CONF_REFRESH_TOKEN] == request.refresh_token
async def test_smartapp_uninstall(hass, config_entry):
@@ -152,3 +149,83 @@ async def test_smartapp_webhook(hass):
result = await smartapp.smartapp_webhook(hass, '', request)
assert result.body == b'{}'
async def test_smartapp_sync_subscriptions(
hass, smartthings_mock, device_factory, subscription_factory):
"""Test synchronization adds and removes."""
api = smartthings_mock.return_value
api.delete_subscription.side_effect = lambda loc_id, sub_id: mock_coro()
api.create_subscription.side_effect = lambda sub: mock_coro()
subscriptions = [
subscription_factory(Capability.thermostat),
subscription_factory(Capability.switch),
subscription_factory(Capability.switch_level)
]
api.subscriptions.return_value = mock_coro(return_value=subscriptions)
devices = [
device_factory('', [Capability.battery, 'ping']),
device_factory('', [Capability.switch, Capability.switch_level]),
device_factory('', [Capability.switch])
]
await smartapp.smartapp_sync_subscriptions(
hass, str(uuid4()), str(uuid4()), str(uuid4()), devices)
assert api.subscriptions.call_count == 1
assert api.delete_subscription.call_count == 1
assert api.create_subscription.call_count == 1
async def test_smartapp_sync_subscriptions_up_to_date(
hass, smartthings_mock, device_factory, subscription_factory):
"""Test synchronization does nothing when current."""
api = smartthings_mock.return_value
api.delete_subscription.side_effect = lambda loc_id, sub_id: mock_coro()
api.create_subscription.side_effect = lambda sub: mock_coro()
subscriptions = [
subscription_factory(Capability.battery),
subscription_factory(Capability.switch),
subscription_factory(Capability.switch_level)
]
api.subscriptions.return_value = mock_coro(return_value=subscriptions)
devices = [
device_factory('', [Capability.battery, 'ping']),
device_factory('', [Capability.switch, Capability.switch_level]),
device_factory('', [Capability.switch])
]
await smartapp.smartapp_sync_subscriptions(
hass, str(uuid4()), str(uuid4()), str(uuid4()), devices)
assert api.subscriptions.call_count == 1
assert api.delete_subscription.call_count == 0
assert api.create_subscription.call_count == 0
async def test_smartapp_sync_subscriptions_handles_exceptions(
hass, smartthings_mock, device_factory, subscription_factory):
"""Test synchronization does nothing when current."""
api = smartthings_mock.return_value
api.delete_subscription.side_effect = \
lambda loc_id, sub_id: mock_coro(exception=Exception)
api.create_subscription.side_effect = \
lambda sub: mock_coro(exception=Exception)
subscriptions = [
subscription_factory(Capability.battery),
subscription_factory(Capability.switch),
subscription_factory(Capability.switch_level)
]
api.subscriptions.return_value = mock_coro(return_value=subscriptions)
devices = [
device_factory('', [Capability.thermostat, 'ping']),
device_factory('', [Capability.switch, Capability.switch_level]),
device_factory('', [Capability.switch])
]
await smartapp.smartapp_sync_subscriptions(
hass, str(uuid4()), str(uuid4()), str(uuid4()), devices)
assert api.subscriptions.call_count == 1
assert api.delete_subscription.call_count == 1
assert api.create_subscription.call_count == 1