From 55a4769172e9bee7f0e1964a9dd4f1cd671e205f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 31 Oct 2023 11:32:17 +0100 Subject: [PATCH] Abort config flow if Google Tasks API is not enabled (#103114) Co-authored-by: Martin Hjelmare --- .../components/google_tasks/config_flow.py | 28 ++++ .../components/google_tasks/strings.json | 4 +- .../fixtures/api_not_enabled_response.json | 15 +++ .../google_tasks/test_config_flow.py | 123 +++++++++++++++++- 4 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 tests/components/google_tasks/fixtures/api_not_enabled_response.json diff --git a/homeassistant/components/google_tasks/config_flow.py b/homeassistant/components/google_tasks/config_flow.py index 77570f0377f..b8e5e26f42c 100644 --- a/homeassistant/components/google_tasks/config_flow.py +++ b/homeassistant/components/google_tasks/config_flow.py @@ -2,6 +2,13 @@ import logging from typing import Any +from google.oauth2.credentials import Credentials +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError +from googleapiclient.http import HttpRequest + +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN, OAUTH2_SCOPES @@ -28,3 +35,24 @@ class OAuth2FlowHandler( "access_type": "offline", "prompt": "consent", } + + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + """Create an entry for the flow.""" + try: + resource = build( + "tasks", + "v1", + credentials=Credentials(token=data[CONF_TOKEN][CONF_ACCESS_TOKEN]), + ) + cmd: HttpRequest = resource.tasklists().list() + await self.hass.async_add_executor_job(cmd.execute) + except HttpError as ex: + error = ex.reason + return self.async_abort( + reason="access_not_configured", + description_placeholders={"message": error}, + ) + except Exception as ex: # pylint: disable=broad-except + self.logger.exception("Unknown error occurred: %s", ex) + return self.async_abort(reason="unknown") + return self.async_create_entry(title=self.flow_impl.name, data=data) diff --git a/homeassistant/components/google_tasks/strings.json b/homeassistant/components/google_tasks/strings.json index e7dbbc2b625..f15c31f42d4 100644 --- a/homeassistant/components/google_tasks/strings.json +++ b/homeassistant/components/google_tasks/strings.json @@ -15,7 +15,9 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]" + "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", + "access_not_configured": "Unable to access the Google API:\n\n{message}", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/tests/components/google_tasks/fixtures/api_not_enabled_response.json b/tests/components/google_tasks/fixtures/api_not_enabled_response.json new file mode 100644 index 00000000000..75ecfddab20 --- /dev/null +++ b/tests/components/google_tasks/fixtures/api_not_enabled_response.json @@ -0,0 +1,15 @@ +{ + "error": { + "code": 403, + "message": "Google Tasks API has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/tasks.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.", + "errors": [ + { + "message": "Google Tasks API has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/tasks.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.", + "domain": "usageLimits", + "reason": "accessNotConfigured", + "extendedHelp": "https://console.developers.google.com" + } + ], + "status": "PERMISSION_DENIED" + } +} diff --git a/tests/components/google_tasks/test_config_flow.py b/tests/components/google_tasks/test_config_flow.py index b05e1eb108d..e92da605697 100644 --- a/tests/components/google_tasks/test_config_flow.py +++ b/tests/components/google_tasks/test_config_flow.py @@ -2,6 +2,9 @@ from unittest.mock import patch +from googleapiclient.errors import HttpError +from httplib2 import Response + from homeassistant import config_entries from homeassistant.components.google_tasks.const import ( DOMAIN, @@ -9,8 +12,11 @@ from homeassistant.components.google_tasks.const import ( OAUTH2_TOKEN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow +from tests.common import load_fixture + CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -59,8 +65,119 @@ async def test_full_flow( with patch( "homeassistant.components.google_tasks.async_setup_entry", return_value=True - ) as mock_setup: - await hass.config_entries.flow.async_configure(result["flow_id"]) - + ) as mock_setup, patch("homeassistant.components.google_tasks.config_flow.build"): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.CREATE_ENTRY assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 + + +async def test_api_not_enabled( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, + setup_credentials, +) -> None: + """Check flow aborts if api is not enabled.""" + result = await hass.config_entries.flow.async_init( + "google_tasks", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=https://www.googleapis.com/auth/tasks" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.google_tasks.config_flow.build", + side_effect=HttpError( + Response({"status": "403"}), + bytes(load_fixture("google_tasks/api_not_enabled_response.json"), "utf-8"), + ), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "access_not_configured" + assert ( + result["description_placeholders"]["message"] + == "Google Tasks API has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/tasks.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry." + ) + + +async def test_general_exception( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, + setup_credentials, +) -> None: + """Check flow aborts if exception happens.""" + result = await hass.config_entries.flow.async_init( + "google_tasks", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=https://www.googleapis.com/auth/tasks" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.google_tasks.config_flow.build", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "unknown"