mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 00:37:13 +00:00
Address Google mail late review (#86847)
This commit is contained in:
parent
3b5fd4bd06
commit
032a37b121
@ -13,14 +13,22 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
|||||||
OAuth2Session,
|
OAuth2Session,
|
||||||
async_get_config_entry_implementation,
|
async_get_config_entry_implementation,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .api import AsyncConfigEntryAuth
|
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
|
from .services import async_setup_services
|
||||||
|
|
||||||
PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
|
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:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Google Mail from a config entry."""
|
"""Set up Google Mail from a config entry."""
|
||||||
implementation = await async_get_config_entry_implementation(hass, 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
|
raise ConfigEntryNotReady from err
|
||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = auth
|
hass.data[DOMAIN][entry.entry_id] = auth
|
||||||
|
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
discovery.async_load_platform(
|
discovery.async_load_platform(
|
||||||
@ -44,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
Platform.NOTIFY,
|
Platform.NOTIFY,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
{DATA_AUTH: auth, CONF_NAME: entry.title},
|
{DATA_AUTH: auth, CONF_NAME: entry.title},
|
||||||
{},
|
hass.data[DOMAIN][DATA_HASS_CONFIG],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
from google.oauth2.credentials import Credentials
|
from google.oauth2.credentials import Credentials
|
||||||
from googleapiclient.discovery import build
|
from googleapiclient.discovery import build
|
||||||
@ -57,23 +57,29 @@ class OAuth2FlowHandler(
|
|||||||
|
|
||||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult:
|
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult:
|
||||||
"""Create an entry for the flow, or update existing entry."""
|
"""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() -> str:
|
||||||
|
|
||||||
def _get_profile() -> dict[str, Any]:
|
|
||||||
"""Get profile from inside the executor."""
|
"""Get profile from inside the executor."""
|
||||||
users = build( # pylint: disable=no-member
|
users = build( # pylint: disable=no-member
|
||||||
"gmail", "v1", credentials=credentials
|
"gmail", "v1", credentials=credentials
|
||||||
).users()
|
).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)
|
if not self.reauth_entry:
|
||||||
self._abort_if_unique_id_configured()
|
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)},
|
||||||
|
)
|
||||||
|
@ -16,6 +16,7 @@ ATTR_START = "start"
|
|||||||
ATTR_TITLE = "title"
|
ATTR_TITLE = "title"
|
||||||
|
|
||||||
DATA_AUTH = "auth"
|
DATA_AUTH = "auth"
|
||||||
|
DATA_HASS_CONFIG = "hass_config"
|
||||||
DEFAULT_ACCESS = [
|
DEFAULT_ACCESS = [
|
||||||
"https://www.googleapis.com/auth/gmail.compose",
|
"https://www.googleapis.com/auth/gmail.compose",
|
||||||
"https://www.googleapis.com/auth/gmail.settings.basic",
|
"https://www.googleapis.com/auth/gmail.settings.basic",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Entity representing a Google Mail account."""
|
"""Entity representing a Google Mail account."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||||
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
|
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
|
||||||
|
|
||||||
from .api import AsyncConfigEntryAuth
|
from .api import AsyncConfigEntryAuth
|
||||||
@ -24,6 +25,7 @@ class GoogleMailEntity(Entity):
|
|||||||
f"{auth.oauth_session.config_entry.entry_id}_{description.key}"
|
f"{auth.oauth_session.config_entry.entry_id}_{description.key}"
|
||||||
)
|
)
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
identifiers={(DOMAIN, auth.oauth_session.config_entry.entry_id)},
|
identifiers={(DOMAIN, auth.oauth_session.config_entry.entry_id)},
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
name=auth.oauth_session.config_entry.unique_id,
|
name=auth.oauth_session.config_entry.unique_id,
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"requirements": ["google-api-python-client==2.71.0"],
|
"requirements": ["google-api-python-client==2.71.0"],
|
||||||
"codeowners": ["@tkdrob"],
|
"codeowners": ["@tkdrob"],
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"integration_type": "device"
|
"integration_type": "service"
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
from email.message import EmailMessage
|
from email.message import EmailMessage
|
||||||
from typing import Any, cast
|
from typing import Any
|
||||||
|
|
||||||
from googleapiclient.http import HttpRequest
|
from googleapiclient.http import HttpRequest
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components.notify import (
|
from homeassistant.components.notify import (
|
||||||
ATTR_DATA,
|
ATTR_DATA,
|
||||||
@ -27,9 +26,9 @@ async def async_get_service(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> GMailNotificationService:
|
) -> GMailNotificationService | None:
|
||||||
"""Get the notification service."""
|
"""Get the notification service."""
|
||||||
return GMailNotificationService(cast(DiscoveryInfoType, discovery_info))
|
return GMailNotificationService(discovery_info) if discovery_info else None
|
||||||
|
|
||||||
|
|
||||||
class GMailNotificationService(BaseNotificationService):
|
class GMailNotificationService(BaseNotificationService):
|
||||||
@ -61,6 +60,6 @@ class GMailNotificationService(BaseNotificationService):
|
|||||||
msg = users.drafts().create(userId=email["From"], body={ATTR_MESSAGE: body})
|
msg = users.drafts().create(userId=email["From"], body={ATTR_MESSAGE: body})
|
||||||
else:
|
else:
|
||||||
if not to_addrs:
|
if not to_addrs:
|
||||||
raise vol.Invalid("recipient address required")
|
raise ValueError("recipient address required")
|
||||||
msg = users.messages().send(userId=email["From"], body=body)
|
msg = users.messages().send(userId=email["From"], body=body)
|
||||||
await self.hass.async_add_executor_job(msg.execute)
|
await self.hass.async_add_executor_job(msg.execute)
|
||||||
|
@ -21,7 +21,8 @@
|
|||||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
|
"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": {
|
"create_entry": {
|
||||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"oauth_error": "Received invalid token data.",
|
"oauth_error": "Received invalid token data.",
|
||||||
"reauth_successful": "Re-authentication was successful",
|
"reauth_successful": "Re-authentication was successful",
|
||||||
"timeout_connect": "Timeout establishing connection",
|
"timeout_connect": "Timeout establishing connection",
|
||||||
"unknown": "Unexpected error"
|
"unknown": "Unexpected error",
|
||||||
|
"wrong_account": "Wrong account: Please authenticate with {email}."
|
||||||
},
|
},
|
||||||
"create_entry": {
|
"create_entry": {
|
||||||
"default": "Successfully authenticated"
|
"default": "Successfully authenticated"
|
||||||
|
@ -2007,7 +2007,7 @@
|
|||||||
"name": "Google Domains"
|
"name": "Google Domains"
|
||||||
},
|
},
|
||||||
"google_mail": {
|
"google_mail": {
|
||||||
"integration_type": "device",
|
"integration_type": "service",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"name": "Google Mail"
|
"name": "Google Mail"
|
||||||
|
@ -115,6 +115,6 @@ async def mock_setup_integration(
|
|||||||
),
|
),
|
||||||
):
|
):
|
||||||
assert await async_setup_component(hass, DOMAIN, {})
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
return func
|
return func
|
||||||
|
6
tests/components/google_mail/fixtures/get_profile_2.json
Normal file
6
tests/components/google_mail/fixtures/get_profile_2.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"emailAddress": "example2@gmail.com",
|
||||||
|
"messagesTotal": 35308,
|
||||||
|
"threadsTotal": 33901,
|
||||||
|
"historyId": "4178212"
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from httplib2 import Response
|
from httplib2 import Response
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.google_mail.const import DOMAIN
|
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(
|
async def test_reauth(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_client_no_auth,
|
hass_client_no_auth,
|
||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
current_request_with_host,
|
current_request_with_host,
|
||||||
config_entry: MockConfigEntry,
|
config_entry: MockConfigEntry,
|
||||||
|
fixture: str,
|
||||||
|
abort_reason: str,
|
||||||
|
placeholders: dict[str, str],
|
||||||
|
calls: int,
|
||||||
|
access_token: str,
|
||||||
) -> None:
|
) -> 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.add_to_hass(hass)
|
||||||
|
|
||||||
config_entry.async_start_reauth(hass)
|
config_entry.async_start_reauth(hass)
|
||||||
@ -118,19 +141,26 @@ async def test_reauth(
|
|||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.google_mail.async_setup_entry", return_value=True
|
"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"])
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
|
||||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
assert len(mock_setup.mock_calls) == 1
|
|
||||||
|
|
||||||
assert result.get("type") == "abort"
|
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 config_entry.unique_id == TITLE
|
||||||
assert "token" in config_entry.data
|
assert "token" in config_entry.data
|
||||||
# Verify access token is refreshed
|
# 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"
|
assert config_entry.data["token"].get("refresh_token") == "mock-refresh-token"
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ async def test_setup_success(
|
|||||||
await hass.config_entries.async_unload(entries[0].entry_id)
|
await hass.config_entries.async_unload(entries[0].entry_id)
|
||||||
await hass.async_block_till_done()
|
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"])
|
@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]
|
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||||
device = device_registry.async_get_device({(DOMAIN, entry.entry_id)})
|
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.identifiers == {(DOMAIN, entry.entry_id)}
|
||||||
assert device.manufacturer == "Google, Inc."
|
assert device.manufacturer == "Google, Inc."
|
||||||
assert device.name == "example@gmail.com"
|
assert device.name == "example@gmail.com"
|
||||||
|
@ -52,7 +52,7 @@ async def test_notify_voluptuous_error(
|
|||||||
"""Test voluptuous error thrown when drafting email."""
|
"""Test voluptuous error thrown when drafting email."""
|
||||||
await setup_integration()
|
await setup_integration()
|
||||||
|
|
||||||
with pytest.raises(Invalid) as ex:
|
with pytest.raises(ValueError) as ex:
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
NOTIFY_DOMAIN,
|
NOTIFY_DOMAIN,
|
||||||
"example_gmail_com",
|
"example_gmail_com",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user