Display and log google_travel_time errors (#77604)

* Display and log google_travel_time errors

* Rename is_valid_config_entry to validate_config_entry

Signed-off-by: Kevin Stillhammer <kevin.stillhammer@gmail.com>

Signed-off-by: Kevin Stillhammer <kevin.stillhammer@gmail.com>
This commit is contained in:
Kevin Stillhammer 2022-10-16 19:06:53 +02:00 committed by GitHub
parent 5d09fe8dc1
commit ef90fe9aee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 142 additions and 28 deletions

View File

@ -1,13 +1,12 @@
"""Config flow for Google Maps Travel Time integration.""" """Config flow for Google Maps Travel Time integration."""
from __future__ import annotations from __future__ import annotations
import logging
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from .const import ( from .const import (
@ -36,9 +35,7 @@ from .const import (
TRAVEL_MODEL, TRAVEL_MODEL,
UNITS, UNITS,
) )
from .helpers import is_valid_config_entry from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
_LOGGER = logging.getLogger(__name__)
class GoogleOptionsFlow(config_entries.OptionsFlow): class GoogleOptionsFlow(config_entries.OptionsFlow):
@ -48,7 +45,7 @@ class GoogleOptionsFlow(config_entries.OptionsFlow):
"""Initialize google options flow.""" """Initialize google options flow."""
self.config_entry = config_entry self.config_entry = config_entry
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input=None) -> FlowResult:
"""Handle the initial step.""" """Handle the initial step."""
if user_input is not None: if user_input is not None:
time_type = user_input.pop(CONF_TIME_TYPE) time_type = user_input.pop(CONF_TIME_TYPE)
@ -122,24 +119,26 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return GoogleOptionsFlow(config_entry) return GoogleOptionsFlow(config_entry)
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None) -> FlowResult:
"""Handle the initial step.""" """Handle the initial step."""
errors = {} errors = {}
user_input = user_input or {} user_input = user_input or {}
if user_input: if user_input:
if await self.hass.async_add_executor_job( try:
is_valid_config_entry, await self.hass.async_add_executor_job(
validate_config_entry,
self.hass, self.hass,
user_input[CONF_API_KEY], user_input[CONF_API_KEY],
user_input[CONF_ORIGIN], user_input[CONF_ORIGIN],
user_input[CONF_DESTINATION], user_input[CONF_DESTINATION],
): )
return self.async_create_entry( return self.async_create_entry(
title=user_input.get(CONF_NAME, DEFAULT_NAME), title=user_input.get(CONF_NAME, DEFAULT_NAME),
data=user_input, data=user_input,
) )
except InvalidApiKeyException:
# If we get here, it's because we couldn't connect errors["base"] = "invalid_auth"
except UnknownException:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
return self.async_show_form( return self.async_show_form(

View File

@ -1,18 +1,46 @@
"""Helpers for Google Time Travel integration.""" """Helpers for Google Time Travel integration."""
import logging
from googlemaps import Client from googlemaps import Client
from googlemaps.distance_matrix import distance_matrix from googlemaps.distance_matrix import distance_matrix
from googlemaps.exceptions import ApiError from googlemaps.exceptions import ApiError, Timeout, TransportError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.location import find_coordinates from homeassistant.helpers.location import find_coordinates
_LOGGER = logging.getLogger(__name__)
def is_valid_config_entry(hass, api_key, origin, destination):
def validate_config_entry(
hass: HomeAssistant, api_key: str, origin: str, destination: str
) -> None:
"""Return whether the config entry data is valid.""" """Return whether the config entry data is valid."""
origin = find_coordinates(hass, origin) resolved_origin = find_coordinates(hass, origin)
destination = find_coordinates(hass, destination) resolved_destination = find_coordinates(hass, destination)
client = Client(api_key, timeout=10)
try: try:
distance_matrix(client, origin, destination, mode="driving") client = Client(api_key, timeout=10)
except ApiError: except ValueError as value_error:
return False _LOGGER.error("Malformed API key")
return True raise InvalidApiKeyException from value_error
try:
distance_matrix(client, resolved_origin, resolved_destination, mode="driving")
except ApiError as api_error:
if api_error.status == "REQUEST_DENIED":
_LOGGER.error("Request denied: %s", api_error.message)
raise InvalidApiKeyException from api_error
_LOGGER.error("Unknown error: %s", api_error.message)
raise UnknownException() from api_error
except TransportError as transport_error:
_LOGGER.error("Unknown error: %s", transport_error)
raise UnknownException() from transport_error
except Timeout as timeout_error:
_LOGGER.error("Timeout error")
raise UnknownException() from timeout_error
class InvalidApiKeyException(Exception):
"""Invalid API Key Error."""
class UnknownException(Exception):
"""Unknown API Error."""

View File

@ -13,6 +13,7 @@
} }
}, },
"error": { "error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}, },
"abort": { "abort": {

View File

@ -4,7 +4,8 @@
"already_configured": "Location is already configured" "already_configured": "Location is already configured"
}, },
"error": { "error": {
"cannot_connect": "Failed to connect" "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication"
}, },
"step": { "step": {
"user": { "user": {

View File

@ -1,7 +1,7 @@
"""Fixtures for Google Time Travel tests.""" """Fixtures for Google Time Travel tests."""
from unittest.mock import patch from unittest.mock import patch
from googlemaps.exceptions import ApiError from googlemaps.exceptions import ApiError, Timeout, TransportError
import pytest import pytest
from homeassistant.components.google_travel_time.const import DOMAIN from homeassistant.components.google_travel_time.const import DOMAIN
@ -58,3 +58,21 @@ def validate_config_entry_fixture():
def invalidate_config_entry_fixture(validate_config_entry): def invalidate_config_entry_fixture(validate_config_entry):
"""Return invalid config entry.""" """Return invalid config entry."""
validate_config_entry.side_effect = ApiError("test") validate_config_entry.side_effect = ApiError("test")
@pytest.fixture(name="invalid_api_key")
def invalid_api_key_fixture(validate_config_entry):
"""Throw a REQUEST_DENIED ApiError."""
validate_config_entry.side_effect = ApiError("REQUEST_DENIED", "Invalid API key.")
@pytest.fixture(name="timeout")
def timeout_fixture(validate_config_entry):
"""Throw a Timeout exception."""
validate_config_entry.side_effect = Timeout()
@pytest.fixture(name="transport_error")
def transport_error_fixture(validate_config_entry):
"""Throw a TransportError exception."""
validate_config_entry.side_effect = TransportError("Unknown.")

View File

@ -67,6 +67,73 @@ async def test_invalid_config_entry(hass):
assert result2["errors"] == {"base": "cannot_connect"} assert result2["errors"] == {"base": "cannot_connect"}
@pytest.mark.usefixtures("invalid_api_key")
async def test_invalid_api_key(hass):
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
MOCK_CONFIG,
)
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["errors"] == {"base": "invalid_auth"}
@pytest.mark.usefixtures("transport_error")
async def test_transport_error(hass):
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
MOCK_CONFIG,
)
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["errors"] == {"base": "cannot_connect"}
@pytest.mark.usefixtures("timeout")
async def test_timeout(hass):
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
MOCK_CONFIG,
)
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["errors"] == {"base": "cannot_connect"}
async def test_malformed_api_key(hass):
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
MOCK_CONFIG,
)
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["errors"] == {"base": "invalid_auth"}
@pytest.mark.parametrize( @pytest.mark.parametrize(
"data,options", "data,options",
[ [