Proactive Alexa ChangeReport messages (#18114)

* Alexa: implement auth and proactive ChangeReport messages

* refactor after rebase from dev to use the new AlexaDirective and Response classes

* move to aiohttp; cleanup

* better function name

* move endpoint to config

* allow passing token function

* remove uneeded state get

* use iterable directly

Co-Authored-By: abmantis <abmantis@users.noreply.github.com>

* missing delete from previous commit

* checks for when user has no auth config

* update cloud component

* PR suggestions

* string lint

* Revert "string lint"

This reverts commit a05a1f134c9ebc7a6e67c093009744f142256365.

* linters are now happier

* more happy linters

* use internal date parser; improve json response handling

* remove unused import

* use await instead of async_add_job

* protect access token update method

* add test_report_state

* line too long

* add docstring

* Update test_smart_home.py

* test accept grant api

* init prefs if None

* add tests for auth and token requests

* replace global with hass.data

* doc lint
This commit is contained in:
Abílio Costa 2019-01-03 21:28:43 +00:00 committed by Paulus Schoutsen
parent c2525bede2
commit ead38f6005
6 changed files with 526 additions and 26 deletions

View file

@ -11,11 +11,24 @@ from homeassistant.const import (
from homeassistant.setup import async_setup_component
from homeassistant.components import alexa
from homeassistant.components.alexa import smart_home
from homeassistant.components.alexa.auth import Auth
from homeassistant.helpers import entityfilter
from tests.common import async_mock_service
DEFAULT_CONFIG = smart_home.Config(should_expose=lambda entity_id: True)
async def get_access_token():
"""Return a test access token."""
return "thisisnotanacesstoken"
TEST_URL = "https://api.amazonalexa.com/v3/events"
TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token"
DEFAULT_CONFIG = smart_home.Config(
endpoint=TEST_URL,
async_get_access_token=get_access_token,
should_expose=lambda entity_id: True)
@pytest.fixture
@ -940,12 +953,15 @@ async def test_exclude_filters(hass):
hass.states.async_set(
'cover.deny', 'off', {'friendly_name': "Blocked cover"})
config = smart_home.Config(should_expose=entityfilter.generate_filter(
include_domains=[],
include_entities=[],
exclude_domains=['script'],
exclude_entities=['cover.deny'],
))
config = smart_home.Config(
endpoint=None,
async_get_access_token=None,
should_expose=entityfilter.generate_filter(
include_domains=[],
include_entities=[],
exclude_domains=['script'],
exclude_entities=['cover.deny'],
))
msg = await smart_home.async_handle_message(hass, config, request)
await hass.async_block_till_done()
@ -972,12 +988,15 @@ async def test_include_filters(hass):
hass.states.async_set(
'group.allow', 'off', {'friendly_name': "Allowed group"})
config = smart_home.Config(should_expose=entityfilter.generate_filter(
include_domains=['automation', 'group'],
include_entities=['script.deny'],
exclude_domains=[],
exclude_entities=[],
))
config = smart_home.Config(
endpoint=None,
async_get_access_token=None,
should_expose=entityfilter.generate_filter(
include_domains=['automation', 'group'],
include_entities=['script.deny'],
exclude_domains=[],
exclude_entities=[],
))
msg = await smart_home.async_handle_message(hass, config, request)
await hass.async_block_till_done()
@ -998,12 +1017,15 @@ async def test_never_exposed_entities(hass):
hass.states.async_set(
'group.allow', 'off', {'friendly_name': "Allowed group"})
config = smart_home.Config(should_expose=entityfilter.generate_filter(
include_domains=['group'],
include_entities=[],
exclude_domains=[],
exclude_entities=[],
))
config = smart_home.Config(
endpoint=None,
async_get_access_token=None,
should_expose=entityfilter.generate_filter(
include_domains=['group'],
include_entities=[],
exclude_domains=[],
exclude_entities=[],
))
msg = await smart_home.async_handle_message(hass, config, request)
await hass.async_block_till_done()
@ -1293,6 +1315,33 @@ async def test_api_increase_color_temp(hass, result, initial):
assert msg['header']['name'] == 'Response'
async def test_api_accept_grant(hass):
"""Test api AcceptGrant process."""
request = get_new_request("Alexa.Authorization", "AcceptGrant")
# add payload
request['directive']['payload'] = {
'grant': {
'type': 'OAuth2.AuthorizationCode',
'code': 'VGhpcyBpcyBhbiBhdXRob3JpemF0aW9uIGNvZGUuIDotKQ=='
},
'grantee': {
'type': 'BearerToken',
'token': 'access-token-from-skill'
}
}
# setup test devices
msg = await smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
await hass.async_block_till_done()
assert 'event' in msg
msg = msg['event']
assert msg['header']['name'] == 'AcceptGrant.Response'
async def test_report_lock_state(hass):
"""Test LockController implements lockState property."""
hass.states.async_set(
@ -1412,6 +1461,8 @@ async def test_entity_config(hass):
'light.test_1', 'on', {'friendly_name': "Test light 1"})
config = smart_home.Config(
endpoint=None,
async_get_access_token=None,
should_expose=lambda entity_id: True,
entity_config={
'light.test_1': {
@ -1598,3 +1649,104 @@ async def test_disabled(hass):
assert msg['header']['name'] == 'ErrorResponse'
assert msg['header']['namespace'] == 'Alexa'
assert msg['payload']['type'] == 'BRIDGE_UNREACHABLE'
async def test_report_state(hass, aioclient_mock):
"""Test proactive state reports."""
aioclient_mock.post(TEST_URL, json={'data': 'is irrelevant'})
hass.states.async_set(
'binary_sensor.test_contact',
'on',
{
'friendly_name': "Test Contact Sensor",
'device_class': 'door',
}
)
await smart_home.async_enable_proactive_mode(hass, DEFAULT_CONFIG)
hass.states.async_set(
'binary_sensor.test_contact',
'off',
{
'friendly_name': "Test Contact Sensor",
'device_class': 'door',
}
)
# To trigger event listener
await hass.async_block_till_done()
assert len(aioclient_mock.mock_calls) == 1
call = aioclient_mock.mock_calls
call_json = json.loads(call[0][2])
assert call_json["event"]["payload"]["change"]["properties"][0][
"value"] == "NOT_DETECTED"
assert call_json["event"]["endpoint"][
"endpointId"] == "binary_sensor#test_contact"
async def run_auth_get_access_token(hass, aioclient_mock, expires_in,
client_id, client_secret,
accept_grant_code, refresh_token):
"""Do auth and request a new token for tests."""
aioclient_mock.post(TEST_TOKEN_URL,
json={'access_token': 'the_access_token',
'refresh_token': refresh_token,
'expires_in': expires_in})
auth = Auth(hass, client_id, client_secret)
await auth.async_do_auth(accept_grant_code)
await auth.async_get_access_token()
async def test_auth_get_access_token_expired(hass, aioclient_mock):
"""Test the auth get access token function."""
client_id = "client123"
client_secret = "shhhhh"
accept_grant_code = "abcdefg"
refresh_token = "refresher"
await run_auth_get_access_token(hass, aioclient_mock, -5,
client_id, client_secret,
accept_grant_code, refresh_token)
assert len(aioclient_mock.mock_calls) == 2
calls = aioclient_mock.mock_calls
auth_call_json = calls[0][2]
token_call_json = calls[1][2]
assert auth_call_json["grant_type"] == "authorization_code"
assert auth_call_json["code"] == accept_grant_code
assert auth_call_json["client_id"] == client_id
assert auth_call_json["client_secret"] == client_secret
assert token_call_json["grant_type"] == "refresh_token"
assert token_call_json["refresh_token"] == refresh_token
assert token_call_json["client_id"] == client_id
assert token_call_json["client_secret"] == client_secret
async def test_auth_get_access_token_not_expired(hass, aioclient_mock):
"""Test the auth get access token function."""
client_id = "client123"
client_secret = "shhhhh"
accept_grant_code = "abcdefg"
refresh_token = "refresher"
await run_auth_get_access_token(hass, aioclient_mock, 555,
client_id, client_secret,
accept_grant_code, refresh_token)
assert len(aioclient_mock.mock_calls) == 1
call = aioclient_mock.mock_calls
auth_call_json = call[0][2]
assert auth_call_json["grant_type"] == "authorization_code"
assert auth_call_json["code"] == accept_grant_code
assert auth_call_json["client_id"] == client_id
assert auth_call_json["client_secret"] == client_secret