Improve SmartThings test mocking (#25028)

* Migrate to asynctest

* Simplify mock access

* Use mocks
This commit is contained in:
Andrew Sayre 2019-07-08 22:39:55 -04:00 committed by GitHub
parent 2fbbcafaed
commit a31e49c857
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 186 additions and 336 deletions

View file

@ -1,9 +1,9 @@
"""Tests for the SmartThings component init module."""
from unittest.mock import Mock, patch
from uuid import uuid4
from aiohttp import ClientConnectionError, ClientResponseError
from pysmartthings import InstalledAppStatus
from asynctest import Mock, patch
from pysmartthings import InstalledAppStatus, OAuthToken
import pytest
from homeassistant.components import cloud, smartthings
@ -14,7 +14,7 @@ from homeassistant.components.smartthings.const import (
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from tests.common import MockConfigEntry, mock_coro
from tests.common import MockConfigEntry
async def test_migration_creates_new_flow(
@ -22,15 +22,12 @@ async def test_migration_creates_new_flow(
"""Test migration deletes app and creates new flow."""
config_entry.version = 1
setattr(hass.config_entries, '_entries', [config_entry])
api = smartthings_mock.return_value
api.delete_installed_app.side_effect = lambda _: mock_coro()
api.delete_app.side_effect = lambda _: mock_coro()
await smartthings.async_migrate_entry(hass, config_entry)
await hass.async_block_till_done()
assert api.delete_installed_app.call_count == 1
assert api.delete_app.call_count == 1
assert smartthings_mock.delete_installed_app.call_count == 1
assert smartthings_mock.delete_app.call_count == 1
assert not hass.config_entries.async_entries(DOMAIN)
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
@ -47,34 +44,30 @@ async def test_unrecoverable_api_errors_create_new_flow(
403 (forbidden/not found): Occurs when the app or installed app could
not be retrieved/found (likely deleted?)
"""
api = smartthings_mock.return_value
for error_status in (401, 403):
setattr(hass.config_entries, '_entries', [config_entry])
api.app.return_value = mock_coro(
exception=ClientResponseError(None, None,
status=error_status))
setattr(hass.config_entries, '_entries', [config_entry])
smartthings_mock.app.side_effect = \
ClientResponseError(None, None, status=401)
# Assert setup returns false
result = await smartthings.async_setup_entry(hass, config_entry)
assert not result
# Assert setup returns false
result = await smartthings.async_setup_entry(hass, config_entry)
assert not result
# Assert entry was removed and new flow created
await hass.async_block_till_done()
assert not hass.config_entries.async_entries(DOMAIN)
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]['handler'] == 'smartthings'
assert flows[0]['context'] == {'source': 'import'}
hass.config_entries.flow.async_abort(flows[0]['flow_id'])
# Assert entry was removed and new flow created
await hass.async_block_till_done()
assert not hass.config_entries.async_entries(DOMAIN)
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]['handler'] == 'smartthings'
assert flows[0]['context'] == {'source': 'import'}
hass.config_entries.flow.async_abort(flows[0]['flow_id'])
async def test_recoverable_api_errors_raise_not_ready(
hass, config_entry, smartthings_mock):
"""Test config entry not ready raised for recoverable API errors."""
setattr(hass.config_entries, '_entries', [config_entry])
api = smartthings_mock.return_value
api.app.return_value = mock_coro(
exception=ClientResponseError(None, None, status=500))
smartthings_mock.app.side_effect = \
ClientResponseError(None, None, status=500)
with pytest.raises(ConfigEntryNotReady):
await smartthings.async_setup_entry(hass, config_entry)
@ -84,11 +77,10 @@ async def test_scenes_api_errors_raise_not_ready(
hass, config_entry, app, installed_app, smartthings_mock):
"""Test if scenes are unauthorized we continue to load platforms."""
setattr(hass.config_entries, '_entries', [config_entry])
api = smartthings_mock.return_value
api.app.return_value = mock_coro(return_value=app)
api.installed_app.return_value = mock_coro(return_value=installed_app)
api.scenes.return_value = mock_coro(
exception=ClientResponseError(None, None, status=500))
smartthings_mock.app.return_value = app
smartthings_mock.installed_app.return_value = installed_app
smartthings_mock.scenes.side_effect = \
ClientResponseError(None, None, status=500)
with pytest.raises(ConfigEntryNotReady):
await smartthings.async_setup_entry(hass, config_entry)
@ -97,9 +89,7 @@ async def test_connection_errors_raise_not_ready(
hass, config_entry, smartthings_mock):
"""Test config entry not ready raised for connection errors."""
setattr(hass.config_entries, '_entries', [config_entry])
api = smartthings_mock.return_value
api.app.return_value = mock_coro(
exception=ClientConnectionError())
smartthings_mock.app.side_effect = ClientConnectionError()
with pytest.raises(ConfigEntryNotReady):
await smartthings.async_setup_entry(hass, config_entry)
@ -110,8 +100,7 @@ async def test_base_url_no_longer_https_does_not_load(
"""Test base_url no longer valid creates a new flow."""
hass.config.api.base_url = 'http://0.0.0.0'
setattr(hass.config_entries, '_entries', [config_entry])
api = smartthings_mock.return_value
api.app.return_value = mock_coro(return_value=app)
smartthings_mock.app.return_value = app
# Assert setup returns false
result = await smartthings.async_setup_entry(hass, config_entry)
@ -123,12 +112,10 @@ async def test_unauthorized_installed_app_raises_not_ready(
smartthings_mock):
"""Test config entry not ready raised when the app isn't authorized."""
setattr(hass.config_entries, '_entries', [config_entry])
setattr(installed_app, '_installed_app_status',
InstalledAppStatus.PENDING)
installed_app.installed_app_status = InstalledAppStatus.PENDING
api = smartthings_mock.return_value
api.app.return_value = mock_coro(return_value=app)
api.installed_app.return_value = mock_coro(return_value=installed_app)
smartthings_mock.app.return_value = app
smartthings_mock.installed_app.return_value = installed_app
with pytest.raises(ConfigEntryNotReady):
await smartthings.async_setup_entry(hass, config_entry)
@ -139,23 +126,21 @@ async def test_scenes_unauthorized_loads_platforms(
device, smartthings_mock, subscription_factory):
"""Test if scenes are unauthorized we continue to load platforms."""
setattr(hass.config_entries, '_entries', [config_entry])
api = smartthings_mock.return_value
api.app.return_value = mock_coro(return_value=app)
api.installed_app.return_value = mock_coro(return_value=installed_app)
api.devices.side_effect = \
lambda *args, **kwargs: mock_coro(return_value=[device])
api.scenes.return_value = mock_coro(
exception=ClientResponseError(None, None, status=403))
smartthings_mock.app.return_value = app
smartthings_mock.installed_app.return_value = installed_app
smartthings_mock.devices.return_value = [device]
smartthings_mock.scenes.side_effect = \
ClientResponseError(None, None, status=403)
mock_token = Mock()
mock_token.access_token.return_value = str(uuid4())
mock_token.refresh_token.return_value = str(uuid4())
api.generate_tokens.return_value = mock_coro(return_value=mock_token)
smartthings_mock.generate_tokens.return_value = mock_token
subscriptions = [subscription_factory(capability)
for capability in device.capabilities]
api.subscriptions.return_value = mock_coro(return_value=subscriptions)
smartthings_mock.subscriptions.return_value = subscriptions
with patch.object(hass.config_entries, 'async_forward_entry_setup',
return_value=mock_coro()) as forward_mock:
with patch.object(hass.config_entries,
'async_forward_entry_setup') as forward_mock:
assert await smartthings.async_setup_entry(hass, config_entry)
# Assert platforms loaded
await hass.async_block_till_done()
@ -167,22 +152,20 @@ async def test_config_entry_loads_platforms(
device, smartthings_mock, subscription_factory, scene):
"""Test config entry loads properly and proxies to platforms."""
setattr(hass.config_entries, '_entries', [config_entry])
api = smartthings_mock.return_value
api.app.return_value = mock_coro(return_value=app)
api.installed_app.return_value = mock_coro(return_value=installed_app)
api.devices.side_effect = \
lambda *args, **kwargs: mock_coro(return_value=[device])
api.scenes.return_value = mock_coro(return_value=[scene])
smartthings_mock.app.return_value = app
smartthings_mock.installed_app.return_value = installed_app
smartthings_mock.devices.return_value = [device]
smartthings_mock.scenes.return_value = [scene]
mock_token = Mock()
mock_token.access_token.return_value = str(uuid4())
mock_token.refresh_token.return_value = str(uuid4())
api.generate_tokens.return_value = mock_coro(return_value=mock_token)
smartthings_mock.generate_tokens.return_value = mock_token
subscriptions = [subscription_factory(capability)
for capability in device.capabilities]
api.subscriptions.return_value = mock_coro(return_value=subscriptions)
smartthings_mock.subscriptions.return_value = subscriptions
with patch.object(hass.config_entries, 'async_forward_entry_setup',
return_value=mock_coro()) as forward_mock:
with patch.object(hass.config_entries,
'async_forward_entry_setup') as forward_mock:
assert await smartthings.async_setup_entry(hass, config_entry)
# Assert platforms loaded
await hass.async_block_till_done()
@ -196,21 +179,19 @@ async def test_config_entry_loads_unconnected_cloud(
setattr(hass.config_entries, '_entries', [config_entry])
hass.data[DOMAIN][CONF_CLOUDHOOK_URL] = "https://test.cloud"
hass.config.api.base_url = 'http://0.0.0.0'
api = smartthings_mock.return_value
api.app.return_value = mock_coro(return_value=app)
api.installed_app.return_value = mock_coro(return_value=installed_app)
api.devices.side_effect = \
lambda *args, **kwargs: mock_coro(return_value=[device])
api.scenes.return_value = mock_coro(return_value=[scene])
smartthings_mock.app.return_value = app
smartthings_mock.installed_app.return_value = installed_app
smartthings_mock.devices.return_value = [device]
smartthings_mock.scenes.return_value = [scene]
mock_token = Mock()
mock_token.access_token.return_value = str(uuid4())
mock_token.refresh_token.return_value = str(uuid4())
api.generate_tokens.return_value = mock_coro(return_value=mock_token)
smartthings_mock.generate_tokens.return_value = mock_token
subscriptions = [subscription_factory(capability)
for capability in device.capabilities]
api.subscriptions.return_value = mock_coro(return_value=subscriptions)
with patch.object(hass.config_entries, 'async_forward_entry_setup',
return_value=mock_coro()) as forward_mock:
smartthings_mock.subscriptions.return_value = subscriptions
with patch.object(
hass.config_entries, 'async_forward_entry_setup') as forward_mock:
assert await smartthings.async_setup_entry(hass, config_entry)
await hass.async_block_till_done()
assert forward_mock.call_count == len(SUPPORTED_PLATFORMS)
@ -227,9 +208,7 @@ async def test_unload_entry(hass, config_entry):
hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] = broker
with patch.object(hass.config_entries, 'async_forward_entry_unload',
return_value=mock_coro(
return_value=True
)) as forward_mock:
return_value=True) as forward_mock:
assert await smartthings.async_unload_entry(hass, config_entry)
assert connect_disconnect.call_count == 1
@ -241,15 +220,11 @@ async def test_unload_entry(hass, config_entry):
async def test_remove_entry(hass, config_entry, smartthings_mock):
"""Test that the installed app and app are removed up."""
# Arrange
api = smartthings_mock.return_value
api.delete_installed_app.side_effect = lambda _: mock_coro()
api.delete_app.side_effect = lambda _: mock_coro()
# Act
await smartthings.async_remove_entry(hass, config_entry)
# Assert
assert api.delete_installed_app.call_count == 1
assert api.delete_app.call_count == 1
assert smartthings_mock.delete_installed_app.call_count == 1
assert smartthings_mock.delete_app.call_count == 1
async def test_remove_entry_cloudhook(hass, config_entry, smartthings_mock):
@ -257,20 +232,15 @@ async def test_remove_entry_cloudhook(hass, config_entry, smartthings_mock):
# Arrange
setattr(hass.config_entries, '_entries', [config_entry])
hass.data[DOMAIN][CONF_CLOUDHOOK_URL] = "https://test.cloud"
api = smartthings_mock.return_value
api.delete_installed_app.side_effect = lambda _: mock_coro()
api.delete_app.side_effect = lambda _: mock_coro()
mock_async_is_logged_in = Mock(return_value=True)
mock_async_delete_cloudhook = Mock(return_value=mock_coro())
# Act
with patch.object(cloud, 'async_is_logged_in',
new=mock_async_is_logged_in), \
patch.object(cloud, 'async_delete_cloudhook',
new=mock_async_delete_cloudhook):
return_value=True) as mock_async_is_logged_in, \
patch.object(cloud, 'async_delete_cloudhook') \
as mock_async_delete_cloudhook:
await smartthings.async_remove_entry(hass, config_entry)
# Assert
assert api.delete_installed_app.call_count == 1
assert api.delete_app.call_count == 1
assert smartthings_mock.delete_installed_app.call_count == 1
assert smartthings_mock.delete_app.call_count == 1
assert mock_async_is_logged_in.call_count == 1
assert mock_async_delete_cloudhook.call_count == 1
@ -282,99 +252,87 @@ async def test_remove_entry_app_in_use(hass, config_entry, smartthings_mock):
data[CONF_INSTALLED_APP_ID] = str(uuid4())
entry2 = MockConfigEntry(version=2, domain=DOMAIN, data=data)
setattr(hass.config_entries, '_entries', [config_entry, entry2])
api = smartthings_mock.return_value
api.delete_installed_app.side_effect = lambda _: mock_coro()
# Act
await smartthings.async_remove_entry(hass, config_entry)
# Assert
assert api.delete_installed_app.call_count == 1
assert api.delete_app.call_count == 0
assert smartthings_mock.delete_installed_app.call_count == 1
assert smartthings_mock.delete_app.call_count == 0
async def test_remove_entry_already_deleted(
hass, config_entry, smartthings_mock):
"""Test handles when the apps have already been removed."""
# Arrange
api = smartthings_mock.return_value
api.delete_installed_app.side_effect = lambda _: mock_coro(
exception=ClientResponseError(None, None, status=403))
api.delete_app.side_effect = lambda _: mock_coro(
exception=ClientResponseError(None, None, status=403))
smartthings_mock.delete_installed_app.side_effect = ClientResponseError(
None, None, status=403)
smartthings_mock.delete_app.side_effect = ClientResponseError(
None, None, status=403)
# Act
await smartthings.async_remove_entry(hass, config_entry)
# Assert
assert api.delete_installed_app.call_count == 1
assert api.delete_app.call_count == 1
assert smartthings_mock.delete_installed_app.call_count == 1
assert smartthings_mock.delete_app.call_count == 1
async def test_remove_entry_installedapp_api_error(
hass, config_entry, smartthings_mock):
"""Test raises exceptions removing the installed app."""
# Arrange
api = smartthings_mock.return_value
api.delete_installed_app.side_effect = lambda _: mock_coro(
exception=ClientResponseError(None, None, status=500))
smartthings_mock.delete_installed_app.side_effect = \
ClientResponseError(None, None, status=500)
# Act
with pytest.raises(ClientResponseError):
await smartthings.async_remove_entry(hass, config_entry)
# Assert
assert api.delete_installed_app.call_count == 1
assert api.delete_app.call_count == 0
assert smartthings_mock.delete_installed_app.call_count == 1
assert smartthings_mock.delete_app.call_count == 0
async def test_remove_entry_installedapp_unknown_error(
hass, config_entry, smartthings_mock):
"""Test raises exceptions removing the installed app."""
# Arrange
api = smartthings_mock.return_value
api.delete_installed_app.side_effect = lambda _: mock_coro(
exception=Exception)
smartthings_mock.delete_installed_app.side_effect = Exception
# Act
with pytest.raises(Exception):
await smartthings.async_remove_entry(hass, config_entry)
# Assert
assert api.delete_installed_app.call_count == 1
assert api.delete_app.call_count == 0
assert smartthings_mock.delete_installed_app.call_count == 1
assert smartthings_mock.delete_app.call_count == 0
async def test_remove_entry_app_api_error(
hass, config_entry, smartthings_mock):
"""Test raises exceptions removing the app."""
# Arrange
api = smartthings_mock.return_value
api.delete_installed_app.side_effect = lambda _: mock_coro()
api.delete_app.side_effect = lambda _: mock_coro(
exception=ClientResponseError(None, None, status=500))
smartthings_mock.delete_app.side_effect = \
ClientResponseError(None, None, status=500)
# Act
with pytest.raises(ClientResponseError):
await smartthings.async_remove_entry(hass, config_entry)
# Assert
assert api.delete_installed_app.call_count == 1
assert api.delete_app.call_count == 1
assert smartthings_mock.delete_installed_app.call_count == 1
assert smartthings_mock.delete_app.call_count == 1
async def test_remove_entry_app_unknown_error(
hass, config_entry, smartthings_mock):
"""Test raises exceptions removing the app."""
# Arrange
api = smartthings_mock.return_value
api.delete_installed_app.side_effect = lambda _: mock_coro()
api.delete_app.side_effect = lambda _: mock_coro(
exception=Exception)
smartthings_mock.delete_app.side_effect = Exception
# Act
with pytest.raises(Exception):
await smartthings.async_remove_entry(hass, config_entry)
# Assert
assert api.delete_installed_app.call_count == 1
assert api.delete_app.call_count == 1
assert smartthings_mock.delete_installed_app.call_count == 1
assert smartthings_mock.delete_app.call_count == 1
async def test_broker_regenerates_token(
hass, config_entry):
"""Test the device broker regenerates the refresh token."""
token = Mock()
token = Mock(OAuthToken)
token.refresh_token = str(uuid4())
token.refresh.return_value = mock_coro()
stored_action = None
def async_track_time_interval(hass, action, interval):