diff --git a/homeassistant/components/google_mail/__init__.py b/homeassistant/components/google_mail/__init__.py index e769bc239f4..a24d5c17874 100644 --- a/homeassistant/components/google_mail/__init__.py +++ b/homeassistant/components/google_mail/__init__.py @@ -13,14 +13,22 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) +from homeassistant.helpers.typing import ConfigType from .api import AsyncConfigEntryAuth -from .const import DATA_AUTH, DOMAIN +from .const import DATA_AUTH, DATA_HASS_CONFIG, DOMAIN from .services import async_setup_services PLATFORMS = [Platform.NOTIFY, Platform.SENSOR] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Google Mail platform.""" + hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google Mail from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) @@ -36,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err except ClientError as err: raise ConfigEntryNotReady from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = auth + hass.data[DOMAIN][entry.entry_id] = auth hass.async_create_task( discovery.async_load_platform( @@ -44,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: Platform.NOTIFY, DOMAIN, {DATA_AUTH: auth, CONF_NAME: entry.title}, - {}, + hass.data[DOMAIN][DATA_HASS_CONFIG], ) ) diff --git a/homeassistant/components/google_mail/config_flow.py b/homeassistant/components/google_mail/config_flow.py index e7631199ddd..0552f57bf5c 100644 --- a/homeassistant/components/google_mail/config_flow.py +++ b/homeassistant/components/google_mail/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Mapping import logging -from typing import Any +from typing import Any, cast from google.oauth2.credentials import Credentials from googleapiclient.discovery import build @@ -57,23 +57,29 @@ class OAuth2FlowHandler( async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an entry for the flow, or update existing entry.""" - if self.reauth_entry: - self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) - await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) - return self.async_abort(reason="reauth_successful") - credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) - - def _get_profile() -> dict[str, Any]: + def _get_profile() -> str: """Get profile from inside the executor.""" users = build( # pylint: disable=no-member "gmail", "v1", credentials=credentials ).users() - return users.getProfile(userId="me").execute() + return users.getProfile(userId="me").execute()["emailAddress"] - email = (await self.hass.async_add_executor_job(_get_profile))["emailAddress"] + credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) + email = await self.hass.async_add_executor_job(_get_profile) - await self.async_set_unique_id(email) - self._abort_if_unique_id_configured() + if not self.reauth_entry: + await self.async_set_unique_id(email) + self._abort_if_unique_id_configured() - return self.async_create_entry(title=email, data=data) + return self.async_create_entry(title=email, data=data) + + if self.reauth_entry.unique_id == email: + self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_abort( + reason="wrong_account", + description_placeholders={"email": cast(str, self.reauth_entry.unique_id)}, + ) diff --git a/homeassistant/components/google_mail/const.py b/homeassistant/components/google_mail/const.py index b9c2157e031..6e70ea9838c 100644 --- a/homeassistant/components/google_mail/const.py +++ b/homeassistant/components/google_mail/const.py @@ -16,6 +16,7 @@ ATTR_START = "start" ATTR_TITLE = "title" DATA_AUTH = "auth" +DATA_HASS_CONFIG = "hass_config" DEFAULT_ACCESS = [ "https://www.googleapis.com/auth/gmail.compose", "https://www.googleapis.com/auth/gmail.settings.basic", diff --git a/homeassistant/components/google_mail/entity.py b/homeassistant/components/google_mail/entity.py index bfa93f48107..5e447125e82 100644 --- a/homeassistant/components/google_mail/entity.py +++ b/homeassistant/components/google_mail/entity.py @@ -1,6 +1,7 @@ """Entity representing a Google Mail account.""" from __future__ import annotations +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from .api import AsyncConfigEntryAuth @@ -24,6 +25,7 @@ class GoogleMailEntity(Entity): f"{auth.oauth_session.config_entry.entry_id}_{description.key}" ) self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, auth.oauth_session.config_entry.entry_id)}, manufacturer=MANUFACTURER, name=auth.oauth_session.config_entry.unique_id, diff --git a/homeassistant/components/google_mail/manifest.json b/homeassistant/components/google_mail/manifest.json index 3693e8ac619..6e4757aa619 100644 --- a/homeassistant/components/google_mail/manifest.json +++ b/homeassistant/components/google_mail/manifest.json @@ -7,5 +7,5 @@ "requirements": ["google-api-python-client==2.71.0"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", - "integration_type": "device" + "integration_type": "service" } diff --git a/homeassistant/components/google_mail/notify.py b/homeassistant/components/google_mail/notify.py index 9abf75ea1e9..eba38c32491 100644 --- a/homeassistant/components/google_mail/notify.py +++ b/homeassistant/components/google_mail/notify.py @@ -3,10 +3,9 @@ from __future__ import annotations import base64 from email.message import EmailMessage -from typing import Any, cast +from typing import Any from googleapiclient.http import HttpRequest -import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, @@ -27,9 +26,9 @@ async def async_get_service( hass: HomeAssistant, config: ConfigType, discovery_info: DiscoveryInfoType | None = None, -) -> GMailNotificationService: +) -> GMailNotificationService | None: """Get the notification service.""" - return GMailNotificationService(cast(DiscoveryInfoType, discovery_info)) + return GMailNotificationService(discovery_info) if discovery_info else None class GMailNotificationService(BaseNotificationService): @@ -61,6 +60,6 @@ class GMailNotificationService(BaseNotificationService): msg = users.drafts().create(userId=email["From"], body={ATTR_MESSAGE: body}) else: if not to_addrs: - raise vol.Invalid("recipient address required") + raise ValueError("recipient address required") msg = users.messages().send(userId=email["From"], body=body) await self.hass.async_add_executor_job(msg.execute) diff --git a/homeassistant/components/google_mail/strings.json b/homeassistant/components/google_mail/strings.json index eaebca01e5d..eb44bffb134 100644 --- a/homeassistant/components/google_mail/strings.json +++ b/homeassistant/components/google_mail/strings.json @@ -21,7 +21,8 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "wrong_account": "Wrong account: Please authenticate with {email}." }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/google_mail/translations/en.json b/homeassistant/components/google_mail/translations/en.json index 51d69638ed2..9f9495d0ec0 100644 --- a/homeassistant/components/google_mail/translations/en.json +++ b/homeassistant/components/google_mail/translations/en.json @@ -12,7 +12,8 @@ "oauth_error": "Received invalid token data.", "reauth_successful": "Re-authentication was successful", "timeout_connect": "Timeout establishing connection", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "wrong_account": "Wrong account: Please authenticate with {email}." }, "create_entry": { "default": "Successfully authenticated" diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 5a90121eb2c..885131ceffa 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2007,7 +2007,7 @@ "name": "Google Domains" }, "google_mail": { - "integration_type": "device", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling", "name": "Google Mail" diff --git a/tests/components/google_mail/conftest.py b/tests/components/google_mail/conftest.py index 64e37e74b8f..c3318b37f0f 100644 --- a/tests/components/google_mail/conftest.py +++ b/tests/components/google_mail/conftest.py @@ -115,6 +115,6 @@ async def mock_setup_integration( ), ): assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() + await hass.async_block_till_done() return func diff --git a/tests/components/google_mail/fixtures/get_profile_2.json b/tests/components/google_mail/fixtures/get_profile_2.json new file mode 100644 index 00000000000..3b36d576183 --- /dev/null +++ b/tests/components/google_mail/fixtures/get_profile_2.json @@ -0,0 +1,6 @@ +{ + "emailAddress": "example2@gmail.com", + "messagesTotal": 35308, + "threadsTotal": 33901, + "historyId": "4178212" +} diff --git a/tests/components/google_mail/test_config_flow.py b/tests/components/google_mail/test_config_flow.py index b0814c4b643..08f71368cc2 100644 --- a/tests/components/google_mail/test_config_flow.py +++ b/tests/components/google_mail/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch from httplib2 import Response +import pytest from homeassistant import config_entries from homeassistant.components.google_mail.const import DOMAIN @@ -68,14 +69,36 @@ async def test_full_flow( ) +@pytest.mark.parametrize( + "fixture,abort_reason,placeholders,calls,access_token", + [ + ("get_profile", "reauth_successful", None, 1, "updated-access-token"), + ( + "get_profile_2", + "wrong_account", + {"email": "example@gmail.com"}, + 0, + "mock-access-token", + ), + ], +) async def test_reauth( hass: HomeAssistant, hass_client_no_auth, aioclient_mock: AiohttpClientMocker, current_request_with_host, config_entry: MockConfigEntry, + fixture: str, + abort_reason: str, + placeholders: dict[str, str], + calls: int, + access_token: str, ) -> None: - """Test the reauthentication case updates the existing config entry.""" + """Test the re-authentication case updates the correct config entry. + + Make sure we abort if the user selects the + wrong account on the consent screen. + """ config_entry.add_to_hass(hass) config_entry.async_start_reauth(hass) @@ -118,19 +141,26 @@ async def test_reauth( with patch( "homeassistant.components.google_mail.async_setup_entry", return_value=True - ) as mock_setup: + ) as mock_setup, patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture(f"google_mail/{fixture}.json"), encoding="UTF-8"), + ), + ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_setup.mock_calls) == 1 assert result.get("type") == "abort" - assert result.get("reason") == "reauth_successful" + assert result["reason"] == abort_reason + assert result["description_placeholders"] == placeholders + assert len(mock_setup.mock_calls) == calls assert config_entry.unique_id == TITLE assert "token" in config_entry.data # Verify access token is refreshed - assert config_entry.data["token"].get("access_token") == "updated-access-token" + assert config_entry.data["token"].get("access_token") == access_token assert config_entry.data["token"].get("refresh_token") == "mock-refresh-token" diff --git a/tests/components/google_mail/test_init.py b/tests/components/google_mail/test_init.py index b57547cfd70..239c7f9d51f 100644 --- a/tests/components/google_mail/test_init.py +++ b/tests/components/google_mail/test_init.py @@ -29,7 +29,7 @@ async def test_setup_success( await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() - assert not len(hass.services.async_services().get(DOMAIN, {})) + assert not hass.services.async_services().get(DOMAIN) @pytest.mark.parametrize("expires_at", [time.time() - 3600], ids=["expired"]) @@ -125,6 +125,7 @@ async def test_device_info( entry = hass.config_entries.async_entries(DOMAIN)[0] device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + assert device.entry_type is dr.DeviceEntryType.SERVICE assert device.identifiers == {(DOMAIN, entry.entry_id)} assert device.manufacturer == "Google, Inc." assert device.name == "example@gmail.com" diff --git a/tests/components/google_mail/test_notify.py b/tests/components/google_mail/test_notify.py index c95d0fa8df3..1e9a174d81f 100644 --- a/tests/components/google_mail/test_notify.py +++ b/tests/components/google_mail/test_notify.py @@ -52,7 +52,7 @@ async def test_notify_voluptuous_error( """Test voluptuous error thrown when drafting email.""" await setup_integration() - with pytest.raises(Invalid) as ex: + with pytest.raises(ValueError) as ex: await hass.services.async_call( NOTIFY_DOMAIN, "example_gmail_com",