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>
pull/80435/head
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."""
from __future__ import annotations
import logging
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from .const import (
@ -36,9 +35,7 @@ from .const import (
TRAVEL_MODEL,
UNITS,
)
from .helpers import is_valid_config_entry
_LOGGER = logging.getLogger(__name__)
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
class GoogleOptionsFlow(config_entries.OptionsFlow):
@ -48,7 +45,7 @@ class GoogleOptionsFlow(config_entries.OptionsFlow):
"""Initialize google options flow."""
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."""
if user_input is not None:
time_type = user_input.pop(CONF_TIME_TYPE)
@ -122,25 +119,27 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Get the options flow for this handler."""
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."""
errors = {}
user_input = user_input or {}
if user_input:
if await self.hass.async_add_executor_job(
is_valid_config_entry,
self.hass,
user_input[CONF_API_KEY],
user_input[CONF_ORIGIN],
user_input[CONF_DESTINATION],
):
try:
await self.hass.async_add_executor_job(
validate_config_entry,
self.hass,
user_input[CONF_API_KEY],
user_input[CONF_ORIGIN],
user_input[CONF_DESTINATION],
)
return self.async_create_entry(
title=user_input.get(CONF_NAME, DEFAULT_NAME),
data=user_input,
)
# If we get here, it's because we couldn't connect
errors["base"] = "cannot_connect"
except InvalidApiKeyException:
errors["base"] = "invalid_auth"
except UnknownException:
errors["base"] = "cannot_connect"
return self.async_show_form(
step_id="user",

View File

@ -1,18 +1,46 @@
"""Helpers for Google Time Travel integration."""
import logging
from googlemaps import Client
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
_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."""
origin = find_coordinates(hass, origin)
destination = find_coordinates(hass, destination)
client = Client(api_key, timeout=10)
resolved_origin = find_coordinates(hass, origin)
resolved_destination = find_coordinates(hass, destination)
try:
distance_matrix(client, origin, destination, mode="driving")
except ApiError:
return False
return True
client = Client(api_key, timeout=10)
except ValueError as value_error:
_LOGGER.error("Malformed API key")
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": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {

View File

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

View File

@ -1,7 +1,7 @@
"""Fixtures for Google Time Travel tests."""
from unittest.mock import patch
from googlemaps.exceptions import ApiError
from googlemaps.exceptions import ApiError, Timeout, TransportError
import pytest
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):
"""Return invalid config entry."""
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"}
@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(
"data,options",
[