core/tests/components/test_alexa.py

467 lines
15 KiB
Python

"""The tests for the Alexa component."""
# pylint: disable=protected-access
import asyncio
import json
import datetime
import pytest
from homeassistant.core import callback
from homeassistant.setup import async_setup_component
from homeassistant.components import alexa
SESSION_ID = "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000"
APPLICATION_ID = "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe"
REQUEST_ID = "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000"
# pylint: disable=invalid-name
calls = []
NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3"
# 2016-10-10T19:51:42+00:00
STATIC_TIME = datetime.datetime.utcfromtimestamp(1476129102)
@pytest.fixture
def alexa_client(loop, hass, test_client):
"""Initialize a Home Assistant server for testing this module."""
@callback
def mock_service(call):
calls.append(call)
hass.services.async_register("test", "alexa", mock_service)
assert loop.run_until_complete(async_setup_component(hass, alexa.DOMAIN, {
# Key is here to verify we allow other keys in config too
"homeassistant": {},
"alexa": {
"flash_briefings": {
"weather": [
{"title": "Weekly forecast",
"text": "This week it will be sunny.",
"date": "2016-10-09T19:51:42.0Z"},
{"title": "Current conditions",
"text": "Currently it is 80 degrees fahrenheit.",
"date": STATIC_TIME}
],
"news_audio": {
"title": "NPR",
"audio": NPR_NEWS_MP3_URL,
"display_url": "https://npr.org",
"date": STATIC_TIME,
"uid": "uuid"
}
},
"intents": {
"WhereAreWeIntent": {
"speech": {
"type": "plaintext",
"text":
"""
{%- if is_state("device_tracker.paulus", "home")
and is_state("device_tracker.anne_therese",
"home") -%}
You are both home, you silly
{%- else -%}
Anne Therese is at {{
states("device_tracker.anne_therese")
}} and Paulus is at {{
states("device_tracker.paulus")
}}
{% endif %}
""",
}
},
"GetZodiacHoroscopeIntent": {
"speech": {
"type": "plaintext",
"text": "You told us your sign is {{ ZodiacSign }}.",
}
},
"AMAZON.PlaybackAction<object@MusicCreativeWork>": {
"speech": {
"type": "plaintext",
"text": "Playing {{ object_byArtist_name }}.",
}
},
"CallServiceIntent": {
"speech": {
"type": "plaintext",
"text": "Service called",
},
"action": {
"service": "test.alexa",
"data_template": {
"hello": "{{ ZodiacSign }}"
},
"entity_id": "switch.test",
}
}
}
}
}))
return loop.run_until_complete(test_client(hass.http.app))
def _intent_req(client, data={}):
return client.post(alexa.INTENTS_API_ENDPOINT, data=json.dumps(data),
headers={'content-type': 'application/json'})
def _flash_briefing_req(client, briefing_id):
return client.get(
"/api/alexa/flash_briefings/{}".format(briefing_id))
@asyncio.coroutine
def test_intent_launch_request(alexa_client):
"""Test the launch of a request."""
data = {
"version": "1.0",
"session": {
"new": True,
"sessionId": SESSION_ID,
"application": {
"applicationId": APPLICATION_ID
},
"attributes": {},
"user": {
"userId": "amzn1.account.AM3B00000000000000000000000"
}
},
"request": {
"type": "LaunchRequest",
"requestId": REQUEST_ID,
"timestamp": "2015-05-13T12:34:56Z"
}
}
req = yield from _intent_req(alexa_client, data)
assert req.status == 200
resp = yield from req.json()
assert "outputSpeech" in resp["response"]
@asyncio.coroutine
def test_intent_request_with_slots(alexa_client):
"""Test a request with slots."""
data = {
"version": "1.0",
"session": {
"new": False,
"sessionId": SESSION_ID,
"application": {
"applicationId": APPLICATION_ID
},
"attributes": {
"supportedHoroscopePeriods": {
"daily": True,
"weekly": False,
"monthly": False
}
},
"user": {
"userId": "amzn1.account.AM3B00000000000000000000000"
}
},
"request": {
"type": "IntentRequest",
"requestId": REQUEST_ID,
"timestamp": "2015-05-13T12:34:56Z",
"intent": {
"name": "GetZodiacHoroscopeIntent",
"slots": {
"ZodiacSign": {
"name": "ZodiacSign",
"value": "virgo"
}
}
}
}
}
req = yield from _intent_req(alexa_client, data)
assert req.status == 200
data = yield from req.json()
text = data.get("response", {}).get("outputSpeech",
{}).get("text")
assert text == "You told us your sign is virgo."
@asyncio.coroutine
def test_intent_request_with_slots_but_no_value(alexa_client):
"""Test a request with slots but no value."""
data = {
"version": "1.0",
"session": {
"new": False,
"sessionId": SESSION_ID,
"application": {
"applicationId": APPLICATION_ID
},
"attributes": {
"supportedHoroscopePeriods": {
"daily": True,
"weekly": False,
"monthly": False
}
},
"user": {
"userId": "amzn1.account.AM3B00000000000000000000000"
}
},
"request": {
"type": "IntentRequest",
"requestId": REQUEST_ID,
"timestamp": "2015-05-13T12:34:56Z",
"intent": {
"name": "GetZodiacHoroscopeIntent",
"slots": {
"ZodiacSign": {
"name": "ZodiacSign",
}
}
}
}
}
req = yield from _intent_req(alexa_client, data)
assert req.status == 200
data = yield from req.json()
text = data.get("response", {}).get("outputSpeech",
{}).get("text")
assert text == "You told us your sign is ."
@asyncio.coroutine
def test_intent_request_without_slots(hass, alexa_client):
"""Test a request without slots."""
data = {
"version": "1.0",
"session": {
"new": False,
"sessionId": SESSION_ID,
"application": {
"applicationId": APPLICATION_ID
},
"attributes": {
"supportedHoroscopePeriods": {
"daily": True,
"weekly": False,
"monthly": False
}
},
"user": {
"userId": "amzn1.account.AM3B00000000000000000000000"
}
},
"request": {
"type": "IntentRequest",
"requestId": REQUEST_ID,
"timestamp": "2015-05-13T12:34:56Z",
"intent": {
"name": "WhereAreWeIntent",
}
}
}
req = yield from _intent_req(alexa_client, data)
assert req.status == 200
json = yield from req.json()
text = json.get("response", {}).get("outputSpeech",
{}).get("text")
assert text == "Anne Therese is at unknown and Paulus is at unknown"
hass.states.async_set("device_tracker.paulus", "home")
hass.states.async_set("device_tracker.anne_therese", "home")
req = yield from _intent_req(alexa_client, data)
assert req.status == 200
json = yield from req.json()
text = json.get("response", {}).get("outputSpeech",
{}).get("text")
assert text == "You are both home, you silly"
@asyncio.coroutine
def test_intent_request_calling_service(alexa_client):
"""Test a request for calling a service."""
data = {
"version": "1.0",
"session": {
"new": False,
"sessionId": SESSION_ID,
"application": {
"applicationId": APPLICATION_ID
},
"attributes": {},
"user": {
"userId": "amzn1.account.AM3B00000000000000000000000"
}
},
"request": {
"type": "IntentRequest",
"requestId": REQUEST_ID,
"timestamp": "2015-05-13T12:34:56Z",
"intent": {
"name": "CallServiceIntent",
"slots": {
"ZodiacSign": {
"name": "ZodiacSign",
"value": "virgo",
}
}
}
}
}
call_count = len(calls)
req = yield from _intent_req(alexa_client, data)
assert req.status == 200
assert call_count + 1 == len(calls)
call = calls[-1]
assert call.domain == "test"
assert call.service == "alexa"
assert call.data.get("entity_id") == ["switch.test"]
assert call.data.get("hello") == "virgo"
@asyncio.coroutine
def test_intent_session_ended_request(alexa_client):
"""Test the request for ending the session."""
data = {
"version": "1.0",
"session": {
"new": False,
"sessionId": SESSION_ID,
"application": {
"applicationId": APPLICATION_ID
},
"attributes": {
"supportedHoroscopePeriods": {
"daily": True,
"weekly": False,
"monthly": False
}
},
"user": {
"userId": "amzn1.account.AM3B00000000000000000000000"
}
},
"request": {
"type": "SessionEndedRequest",
"requestId": REQUEST_ID,
"timestamp": "2015-05-13T12:34:56Z",
"reason": "USER_INITIATED"
}
}
req = yield from _intent_req(alexa_client, data)
assert req.status == 200
text = yield from req.text()
assert text == ''
@asyncio.coroutine
def test_intent_from_built_in_intent_library(alexa_client):
"""Test intents from the Built-in Intent Library."""
data = {
'request': {
'intent': {
'name': 'AMAZON.PlaybackAction<object@MusicCreativeWork>',
'slots': {
'object.byArtist.name': {
'name': 'object.byArtist.name',
'value': 'the shins'
},
'object.composer.name': {
'name': 'object.composer.name'
},
'object.contentSource': {
'name': 'object.contentSource'
},
'object.era': {
'name': 'object.era'
},
'object.genre': {
'name': 'object.genre'
},
'object.name': {
'name': 'object.name'
},
'object.owner.name': {
'name': 'object.owner.name'
},
'object.select': {
'name': 'object.select'
},
'object.sort': {
'name': 'object.sort'
},
'object.type': {
'name': 'object.type',
'value': 'music'
}
}
},
'timestamp': '2016-12-14T23:23:37Z',
'type': 'IntentRequest',
'requestId': REQUEST_ID,
},
'session': {
'sessionId': SESSION_ID,
'application': {
'applicationId': APPLICATION_ID
}
}
}
req = yield from _intent_req(alexa_client, data)
assert req.status == 200
data = yield from req.json()
text = data.get("response", {}).get("outputSpeech",
{}).get("text")
assert text == "Playing the shins."
@asyncio.coroutine
def test_flash_briefing_invalid_id(alexa_client):
"""Test an invalid Flash Briefing ID."""
req = yield from _flash_briefing_req(alexa_client, 10000)
assert req.status == 404
text = yield from req.text()
assert text == ''
@asyncio.coroutine
def test_flash_briefing_date_from_str(alexa_client):
"""Test the response has a valid date parsed from string."""
req = yield from _flash_briefing_req(alexa_client, "weather")
assert req.status == 200
data = yield from req.json()
assert data[0].get(alexa.ATTR_UPDATE_DATE) == "2016-10-09T19:51:42.0Z"
@asyncio.coroutine
def test_flash_briefing_date_from_datetime(alexa_client):
"""Test the response has a valid date from a datetime object."""
req = yield from _flash_briefing_req(alexa_client, "weather")
assert req.status == 200
data = yield from req.json()
assert data[1].get(alexa.ATTR_UPDATE_DATE) == '2016-10-10T19:51:42.0Z'
@asyncio.coroutine
def test_flash_briefing_valid(alexa_client):
"""Test the response is valid."""
data = [{
"titleText": "NPR",
"redirectionURL": "https://npr.org",
"streamUrl": NPR_NEWS_MP3_URL,
"mainText": "",
"uid": "uuid",
"updateDate": '2016-10-10T19:51:42.0Z'
}]
req = yield from _flash_briefing_req(alexa_client, "news_audio")
assert req.status == 200
json = yield from req.json()
assert json == data