Cleanup code from nest yaml migration and OOB auth deprecation (#92311)

This commit is contained in:
Allen Porter 2023-04-30 18:00:40 -07:00 committed by GitHub
parent c0d0c89293
commit e7433c42b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 79 additions and 492 deletions

View file

@ -22,10 +22,6 @@ from google_nest_sdm.exceptions import (
import voluptuous as vol import voluptuous as vol
from homeassistant.auth.permissions.const import POLICY_READ from homeassistant.auth.permissions.const import POLICY_READ
from homeassistant.components.application_credentials import (
ClientCredential,
async_import_client_credential,
)
from homeassistant.components.camera import Image, img_util from homeassistant.components.camera import Image, img_util
from homeassistant.components.http import KEY_HASS_USER from homeassistant.components.http import KEY_HASS_USER
from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.http.view import HomeAssistantView
@ -52,11 +48,6 @@ from homeassistant.helpers import (
entity_registry as er, entity_registry as er,
) )
from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.entity_registry import async_entries_for_device
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from . import api, config_flow from . import api, config_flow
@ -69,8 +60,6 @@ from .const import (
DATA_SDM, DATA_SDM,
DATA_SUBSCRIBER, DATA_SUBSCRIBER,
DOMAIN, DOMAIN,
INSTALLED_AUTH_DOMAIN,
WEB_AUTH_DOMAIN,
) )
from .events import EVENT_NAME_MAP, NEST_EVENT from .events import EVENT_NAME_MAP, NEST_EVENT
from .legacy import async_setup_legacy, async_setup_legacy_entry from .legacy import async_setup_legacy, async_setup_legacy_entry
@ -128,9 +117,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if DOMAIN not in config: if DOMAIN not in config:
return True # ConfigMode.SDM_APPLICATION_CREDENTIALS return True # ConfigMode.SDM_APPLICATION_CREDENTIALS
# Note that configuration.yaml deprecation warnings are handled in the
# config entry since we don't know what type of credentials we have and
# whether or not they can be imported.
hass.data[DOMAIN][DATA_NEST_CONFIG] = config[DOMAIN] hass.data[DOMAIN][DATA_NEST_CONFIG] = config[DOMAIN]
config_mode = config_flow.get_config_mode(hass) config_mode = config_flow.get_config_mode(hass)
@ -185,15 +171,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if DATA_SDM not in entry.data or config_mode == config_flow.ConfigMode.LEGACY: if DATA_SDM not in entry.data or config_mode == config_flow.ConfigMode.LEGACY:
return await async_setup_legacy_entry(hass, entry) return await async_setup_legacy_entry(hass, entry)
if config_mode == config_flow.ConfigMode.SDM: if entry.unique_id != entry.data[CONF_PROJECT_ID]:
await async_import_config(hass, entry)
elif entry.unique_id != entry.data[CONF_PROJECT_ID]:
hass.config_entries.async_update_entry( hass.config_entries.async_update_entry(
entry, unique_id=entry.data[CONF_PROJECT_ID] entry, unique_id=entry.data[CONF_PROJECT_ID]
) )
async_delete_issue(hass, DOMAIN, "removed_app_auth")
subscriber = await api.new_subscriber(hass, entry) subscriber = await api.new_subscriber(hass, entry)
if not subscriber: if not subscriber:
return False return False
@ -239,71 +221,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True return True
async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Attempt to import configuration.yaml settings."""
config = hass.data[DOMAIN][DATA_NEST_CONFIG]
new_data = {
CONF_PROJECT_ID: config[CONF_PROJECT_ID],
**entry.data,
}
if CONF_SUBSCRIBER_ID not in entry.data:
if CONF_SUBSCRIBER_ID not in config:
raise ValueError("Configuration option 'subscriber_id' missing")
new_data.update(
{
CONF_SUBSCRIBER_ID: config[CONF_SUBSCRIBER_ID],
# Don't delete user managed subscriber
CONF_SUBSCRIBER_ID_IMPORTED: True,
}
)
hass.config_entries.async_update_entry(
entry, data=new_data, unique_id=new_data[CONF_PROJECT_ID]
)
if entry.data["auth_implementation"] == INSTALLED_AUTH_DOMAIN:
# App Auth credentials have been deprecated and must be re-created
# by the user in the config flow
async_create_issue(
hass,
DOMAIN,
"removed_app_auth",
is_fixable=False,
severity=IssueSeverity.ERROR,
translation_key="removed_app_auth",
translation_placeholders={
"more_info_url": (
"https://www.home-assistant.io/more-info/nest-auth-deprecation"
),
"documentation_url": "https://www.home-assistant.io/integrations/nest/",
},
)
raise ConfigEntryAuthFailed(
"Google has deprecated App Auth credentials, and the integration "
"must be reconfigured in the UI to restore access to Nest Devices."
)
if entry.data["auth_implementation"] == WEB_AUTH_DOMAIN:
await async_import_client_credential(
hass,
DOMAIN,
ClientCredential(
config[CONF_CLIENT_ID],
config[CONF_CLIENT_SECRET],
),
WEB_AUTH_DOMAIN,
)
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml",
breaks_in_ha_version="2022.10.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if DATA_SDM not in entry.data: if DATA_SDM not in entry.data:

View file

@ -43,7 +43,6 @@ from .const import (
DATA_NEST_CONFIG, DATA_NEST_CONFIG,
DATA_SDM, DATA_SDM,
DOMAIN, DOMAIN,
INSTALLED_AUTH_DOMAIN,
OAUTH2_AUTHORIZE, OAUTH2_AUTHORIZE,
SDM_SCOPES, SDM_SCOPES,
) )
@ -64,10 +63,6 @@ PUBSUB_API_URL = "https://console.cloud.google.com/apis/library/pubsub.googleapi
# URLs for Configure Device Access Project step # URLs for Configure Device Access Project step
DEVICE_ACCESS_CONSOLE_URL = "https://console.nest.google.com/device-access/" DEVICE_ACCESS_CONSOLE_URL = "https://console.nest.google.com/device-access/"
# URLs for App Auth deprecation and upgrade
UPGRADE_MORE_INFO_URL = (
"https://www.home-assistant.io/integrations/nest/#deprecated-app-auth-credentials"
)
DEVICE_ACCESS_CONSOLE_EDIT_URL = ( DEVICE_ACCESS_CONSOLE_EDIT_URL = (
"https://console.nest.google.com/device-access/project/{project_id}/information" "https://console.nest.google.com/device-access/project/{project_id}/information"
) )
@ -161,7 +156,6 @@ class NestFlowHandler(
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize NestFlowHandler.""" """Initialize NestFlowHandler."""
super().__init__() super().__init__()
self._upgrade = False
self._data: dict[str, Any] = {DATA_SDM: {}} self._data: dict[str, Any] = {DATA_SDM: {}}
# Possible name to use for config entry based on the Google Home name # Possible name to use for config entry based on the Google Home name
self._structure_config_title: str | None = None self._structure_config_title: str | None = None
@ -233,38 +227,8 @@ class NestFlowHandler(
assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API"
if user_input is None: if user_input is None:
return self.async_show_form(step_id="reauth_confirm") return self.async_show_form(step_id="reauth_confirm")
if self._data["auth_implementation"] == INSTALLED_AUTH_DOMAIN:
# The config entry points to an auth mechanism that no longer works and the
# user needs to take action in the google cloud console to resolve. First
# prompt to create app creds, then later ensure they've updated the device
# access console.
self._upgrade = True
implementations = await config_entry_oauth2_flow.async_get_implementations(
self.hass, self.DOMAIN
)
if not implementations:
return await self.async_step_auth_upgrade()
return await self.async_step_user() return await self.async_step_user()
async def async_step_auth_upgrade(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Give instructions for upgrade of deprecated app auth."""
assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API"
if user_input is None:
return self.async_show_form(
step_id="auth_upgrade",
description_placeholders={
"more_info_url": UPGRADE_MORE_INFO_URL,
},
)
# Abort this flow and ask the user for application credentials. The frontend
# will restart a new config flow after the user finishes so schedule a new
# re-auth config flow for the same entry so the user may resume.
if reauth_entry := self._async_reauth_entry():
self.hass.async_add_job(reauth_entry.async_start_reauth, self.hass)
return self.async_abort(reason="missing_credentials")
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
@ -358,39 +322,6 @@ class NestFlowHandler(
errors=errors, errors=errors,
) )
async def async_step_auth(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Verify any last pre-requisites before sending user through OAuth flow."""
if user_input is None and self._upgrade:
# During app auth upgrade we need the user to update their device
# access project before we redirect to the authentication flow.
return await self.async_step_device_project_upgrade()
return await super().async_step_auth(user_input)
async def async_step_device_project_upgrade(
self, user_input: dict | None = None
) -> FlowResult:
"""Update the device access project."""
if user_input is not None:
# Resume OAuth2 redirects
return await super().async_step_auth()
if not isinstance(
self.flow_impl, config_entry_oauth2_flow.LocalOAuth2Implementation
):
raise TypeError(f"Unexpected OAuth implementation: {self.flow_impl}")
client_id = self.flow_impl.client_id
return self.async_show_form(
step_id="device_project_upgrade",
description_placeholders={
"device_access_console_url": DEVICE_ACCESS_CONSOLE_EDIT_URL.format(
project_id=self._data[CONF_PROJECT_ID]
),
"more_info_url": UPGRADE_MORE_INFO_URL,
"client_id": client_id,
},
)
async def async_step_pubsub( async def async_step_pubsub(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:

View file

@ -4,14 +4,6 @@
}, },
"config": { "config": {
"step": { "step": {
"auth_upgrade": {
"title": "Nest: App Auth Deprecation",
"description": "App Auth has been deprecated by Google to improve security, and you need to take action by creating new application credentials.\n\nOpen the [documentation]({more_info_url}) to follow along as the next steps will guide you through the steps you need to take to restore access to your Nest devices."
},
"device_project_upgrade": {
"title": "Nest: Update Device Access Project",
"description": "Update the Nest Device Access Project with your new OAuth Client ID ([more info]({more_info_url}))\n1. Go to the [Device Access Console]({device_access_console_url}).\n1. Click the trash icon next to *OAuth Client ID*.\n1. Click the `...` overflow menu and *Add Client ID*.\n1. Enter your new OAuth Client ID and click **Add**.\n\nYour OAuth Client ID is: `{client_id}`"
},
"create_cloud_project": { "create_cloud_project": {
"title": "Nest: Create and configure Cloud Project", "title": "Nest: Create and configure Cloud Project",
"description": "The Nest integration allows you to integrate your Nest Thermostats, Cameras, and Doorbells using the Smart Device Management API. The SDM API **requires a US $5** one time setup fee. See documentation for [more info]({more_info_url}).\n\n1. Go to the [Google Cloud Console]({cloud_console_url}).\n1. If this is your first project, click **Create Project** then **New Project**.\n1. Give your Cloud Project a Name and then click **Create**.\n1. Save the Cloud Project ID e.g. *example-project-12345* as you will need it later\n1. Go to API Library for [Smart Device Management API]({sdm_api_url}) and click **Enable**.\n1. Go to API Library for [Cloud Pub/Sub API]({pubsub_api_url}) and click **Enable**.\n\nProceed when your cloud project is set up." "description": "The Nest integration allows you to integrate your Nest Thermostats, Cameras, and Doorbells using the Smart Device Management API. The SDM API **requires a US $5** one time setup fee. See documentation for [more info]({more_info_url}).\n\n1. Go to the [Google Cloud Console]({cloud_console_url}).\n1. If this is your first project, click **Create Project** then **New Project**.\n1. Give your Cloud Project a Name and then click **Create**.\n1. Save the Cloud Project ID e.g. *example-project-12345* as you will need it later\n1. Go to API Library for [Smart Device Management API]({sdm_api_url}) and click **Enable**.\n1. Go to API Library for [Cloud Pub/Sub API]({pubsub_api_url}) and click **Enable**.\n\nProceed when your cloud project is set up."
@ -90,14 +82,6 @@
} }
}, },
"issues": { "issues": {
"deprecated_yaml": {
"title": "The Nest YAML configuration is being removed",
"description": "Configuring Nest in configuration.yaml is being removed in Home Assistant 2022.10.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
},
"removed_app_auth": {
"title": "Nest Authentication Credentials must be updated",
"description": "To improve security and reduce phishing risk Google has deprecated the authentication method used by Home Assistant.\n\n**This requires action by you to resolve** ([more info]({more_info_url}))\n\n1. Visit the integrations page\n1. Click Reconfigure on the Nest integration.\n1. Home Assistant will walk you through the steps to upgrade to Web Authentication.\n\nSee the Nest [integration instructions]({documentation_url}) for troubleshooting information."
},
"legacy_nest_deprecated": { "legacy_nest_deprecated": {
"title": "Legacy Works With Nest is being removed", "title": "Legacy Works With Nest is being removed",
"description": "Legacy Works With Nest is being removed from Home Assistant.\n\nYou must take action to use the SDM API. Remove all `nest` configuration from `configuration.yaml` and restart Home Assistant, then see the Nest [integration instructions]({documentation_url}) for set up instructions and supported devices." "description": "Legacy Works With Nest is being removed from Home Assistant.\n\nYou must take action to use the SDM API. Remove all `nest` configuration from `configuration.yaml` and restart Home Assistant, then see the Nest [integration instructions]({documentation_url}) for set up instructions and supported devices."

View file

@ -17,9 +17,6 @@ from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
from homeassistant.components.application_credentials import ClientCredential from homeassistant.components.application_credentials import ClientCredential
from homeassistant.components.nest import DOMAIN from homeassistant.components.nest import DOMAIN
from homeassistant.components.nest.const import SDM_SCOPES
from tests.common import MockConfigEntry
# Typing helpers # Typing helpers
PlatformSetup = Callable[[], Awaitable[None]] PlatformSetup = Callable[[], Awaitable[None]]
@ -36,98 +33,28 @@ CLOUD_PROJECT_ID = "cloud-id-9876"
SUBSCRIBER_ID = "projects/cloud-id-9876/subscriptions/subscriber-id-9876" SUBSCRIBER_ID = "projects/cloud-id-9876/subscriptions/subscriber-id-9876"
CONFIG = {
"nest": {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"project_id": PROJECT_ID,
"subscriber_id": SUBSCRIBER_ID,
},
}
FAKE_TOKEN = "some-token"
FAKE_REFRESH_TOKEN = "some-refresh-token"
def create_token_entry(token_expiration_time=None):
"""Create OAuth 'token' data for a ConfigEntry."""
if token_expiration_time is None:
token_expiration_time = time.time() + 86400
return {
"access_token": FAKE_TOKEN,
"refresh_token": FAKE_REFRESH_TOKEN,
"scope": " ".join(SDM_SCOPES),
"token_type": "Bearer",
"expires_at": token_expiration_time,
}
def create_config_entry(token_expiration_time=None) -> MockConfigEntry:
"""Create a ConfigEntry and add it to Home Assistant."""
config_entry_data = {
"sdm": {}, # Indicates new SDM API, not legacy API
"auth_implementation": "nest",
"token": create_token_entry(token_expiration_time),
}
return MockConfigEntry(domain=DOMAIN, data=config_entry_data)
@dataclass @dataclass
class NestTestConfig: class NestTestConfig:
"""Holder for integration configuration.""" """Holder for integration configuration."""
config: dict[str, Any] = field(default_factory=dict) config: dict[str, Any] = field(default_factory=dict)
config_entry_data: dict[str, Any] | None = None config_entry_data: dict[str, Any] | None = None
auth_implementation: str = WEB_AUTH_DOMAIN
credential: ClientCredential | None = None credential: ClientCredential | None = None
# Exercises mode where all configuration is in configuration.yaml
TEST_CONFIG_YAML_ONLY = NestTestConfig(
config=CONFIG,
config_entry_data={
"sdm": {},
"token": create_token_entry(),
},
)
TEST_CONFIGFLOW_YAML_ONLY = NestTestConfig(
config=TEST_CONFIG_YAML_ONLY.config,
)
# Exercises mode where subscriber id is created in the config flow, but
# all authentication is defined in configuration.yaml
TEST_CONFIG_HYBRID = NestTestConfig(
config={
"nest": {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"project_id": PROJECT_ID,
},
},
config_entry_data={
"sdm": {},
"token": create_token_entry(),
"cloud_project_id": CLOUD_PROJECT_ID,
"subscriber_id": SUBSCRIBER_ID,
},
)
TEST_CONFIGFLOW_HYBRID = NestTestConfig(TEST_CONFIG_HYBRID.config)
# Exercises mode where all configuration is from the config flow # Exercises mode where all configuration is from the config flow
TEST_CONFIG_APP_CREDS = NestTestConfig( TEST_CONFIG_APP_CREDS = NestTestConfig(
config_entry_data={ config_entry_data={
"sdm": {}, "sdm": {},
"token": create_token_entry(),
"project_id": PROJECT_ID, "project_id": PROJECT_ID,
"cloud_project_id": CLOUD_PROJECT_ID, "cloud_project_id": CLOUD_PROJECT_ID,
"subscriber_id": SUBSCRIBER_ID, "subscriber_id": SUBSCRIBER_ID,
"auth_implementation": "imported-cred",
}, },
auth_implementation="imported-cred",
credential=ClientCredential(CLIENT_ID, CLIENT_SECRET), credential=ClientCredential(CLIENT_ID, CLIENT_SECRET),
) )
TEST_CONFIGFLOW_APP_CREDS = NestTestConfig( TEST_CONFIGFLOW_APP_CREDS = NestTestConfig(
config=TEST_CONFIG_APP_CREDS.config, config=TEST_CONFIG_APP_CREDS.config,
auth_implementation="imported-cred",
credential=ClientCredential(CLIENT_ID, CLIENT_SECRET), credential=ClientCredential(CLIENT_ID, CLIENT_SECRET),
) )

View file

@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Generator from collections.abc import Generator
import copy import copy
import shutil import shutil
import time
from typing import Any from typing import Any
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import uuid import uuid
@ -18,7 +19,7 @@ from homeassistant.components.application_credentials import (
async_import_client_credential, async_import_client_credential,
) )
from homeassistant.components.nest import DOMAIN from homeassistant.components.nest import DOMAIN
from homeassistant.components.nest.const import CONF_SUBSCRIBER_ID from homeassistant.components.nest.const import CONF_SUBSCRIBER_ID, SDM_SCOPES
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -27,7 +28,6 @@ from .common import (
PROJECT_ID, PROJECT_ID,
SUBSCRIBER_ID, SUBSCRIBER_ID,
TEST_CONFIG_APP_CREDS, TEST_CONFIG_APP_CREDS,
TEST_CONFIG_YAML_ONLY,
CreateDevice, CreateDevice,
FakeSubscriber, FakeSubscriber,
NestTestConfig, NestTestConfig,
@ -37,6 +37,9 @@ from .common import (
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
FAKE_TOKEN = "some-token"
FAKE_REFRESH_TOKEN = "some-refresh-token"
class FakeAuth(AbstractAuth): class FakeAuth(AbstractAuth):
"""A fake implementation of the auth class that records requests. """A fake implementation of the auth class that records requests.
@ -186,18 +189,9 @@ def subscriber_id() -> str:
@pytest.fixture @pytest.fixture
def auth_implementation(nest_test_config: NestTestConfig) -> str | None:
"""Fixture to let tests override the auth implementation in the config entry."""
return nest_test_config.auth_implementation
@pytest.fixture(
params=[TEST_CONFIG_YAML_ONLY, TEST_CONFIG_APP_CREDS],
ids=["yaml-config-only", "app-creds"],
)
def nest_test_config(request) -> NestTestConfig: def nest_test_config(request) -> NestTestConfig:
"""Fixture that sets up the configuration used for the test.""" """Fixture that sets up the configuration used for the test."""
return request.param return TEST_CONFIG_APP_CREDS
@pytest.fixture @pytest.fixture
@ -220,12 +214,30 @@ def config_entry_unique_id() -> str:
return PROJECT_ID return PROJECT_ID
@pytest.fixture
def token_expiration_time() -> float:
"""Fixture for expiration time of the config entry auth token."""
return time.time() + 86400
@pytest.fixture
def token_entry(token_expiration_time: float) -> dict[str, Any]:
"""Fixture for OAuth 'token' data for a ConfigEntry."""
return {
"access_token": FAKE_TOKEN,
"refresh_token": FAKE_REFRESH_TOKEN,
"scope": " ".join(SDM_SCOPES),
"token_type": "Bearer",
"expires_at": token_expiration_time,
}
@pytest.fixture @pytest.fixture
def config_entry( def config_entry(
subscriber_id: str | None, subscriber_id: str | None,
auth_implementation: str | None,
nest_test_config: NestTestConfig, nest_test_config: NestTestConfig,
config_entry_unique_id: str, config_entry_unique_id: str,
token_entry: dict[str, Any],
) -> MockConfigEntry | None: ) -> MockConfigEntry | None:
"""Fixture that sets up the ConfigEntry for the test.""" """Fixture that sets up the ConfigEntry for the test."""
if nest_test_config.config_entry_data is None: if nest_test_config.config_entry_data is None:
@ -236,7 +248,7 @@ def config_entry(
data[CONF_SUBSCRIBER_ID] = subscriber_id data[CONF_SUBSCRIBER_ID] = subscriber_id
else: else:
del data[CONF_SUBSCRIBER_ID] del data[CONF_SUBSCRIBER_ID]
data["auth_implementation"] = auth_implementation data["token"] = token_entry
return MockConfigEntry(domain=DOMAIN, data=data, unique_id=config_entry_unique_id) return MockConfigEntry(domain=DOMAIN, data=data, unique_id=config_entry_unique_id)
@ -247,10 +259,7 @@ async def credential(hass: HomeAssistant, nest_test_config: NestTestConfig) -> N
return return
assert await async_setup_component(hass, "application_credentials", {}) assert await async_setup_component(hass, "application_credentials", {})
await async_import_client_credential( await async_import_client_credential(
hass, hass, DOMAIN, nest_test_config.credential, "imported-cred"
DOMAIN,
nest_test_config.credential,
nest_test_config.auth_implementation,
) )

View file

@ -12,43 +12,38 @@ from unittest.mock import patch
import pytest import pytest
from homeassistant.components.nest import DOMAIN
from homeassistant.components.nest.const import API_URL, OAUTH2_TOKEN, SDM_SCOPES from homeassistant.components.nest.const import API_URL, OAUTH2_TOKEN, SDM_SCOPES
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util import dt from homeassistant.util import dt
from .common import ( from .common import CLIENT_ID, CLIENT_SECRET, PROJECT_ID, PlatformSetup
CLIENT_ID, from .conftest import FAKE_REFRESH_TOKEN, FAKE_TOKEN
CLIENT_SECRET,
CONFIG,
FAKE_REFRESH_TOKEN,
FAKE_TOKEN,
PROJECT_ID,
TEST_CONFIGFLOW_YAML_ONLY,
create_config_entry,
)
from tests.test_util.aiohttp import AiohttpClientMocker from tests.test_util.aiohttp import AiohttpClientMocker
FAKE_UPDATED_TOKEN = "fake-updated-token" FAKE_UPDATED_TOKEN = "fake-updated-token"
async def async_setup_sdm(hass): @pytest.fixture
"""Set up the integration.""" def subscriber() -> None:
assert await async_setup_component(hass, DOMAIN, CONFIG) """Disable default subscriber since tests use their own patch."""
await hass.async_block_till_done() return None
# This tests needs to be adjusted to remove lingering tasks # This tests needs to be adjusted to remove lingering tasks
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY]) @pytest.mark.parametrize(
async def test_auth(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: "token_expiration_time",
[time.time() + 7 * 86400],
ids=["expires-in-future"],
)
async def test_auth(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
setup_platform: PlatformSetup,
token_expiration_time: float,
) -> None:
"""Exercise authentication library creates valid credentials.""" """Exercise authentication library creates valid credentials."""
expiration_time = time.time() + 86400
create_config_entry(expiration_time).add_to_hass(hass)
# Prepare to capture credentials in API request. Empty payloads just mean # Prepare to capture credentials in API request. Empty payloads just mean
# no devices or structures are loaded. # no devices or structures are loaded.
aioclient_mock.get(f"{API_URL}/enterprises/{PROJECT_ID}/structures", json={}) aioclient_mock.get(f"{API_URL}/enterprises/{PROJECT_ID}/structures", json={})
@ -69,7 +64,7 @@ async def test_auth(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) ->
"google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber", "google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber",
side_effect=async_new_subscriber, side_effect=async_new_subscriber,
) as new_subscriber_mock: ) as new_subscriber_mock:
await async_setup_sdm(hass) await setup_platform()
# Verify API requests are made with the correct credentials # Verify API requests are made with the correct credentials
calls = aioclient_mock.mock_calls calls = aioclient_mock.mock_calls
@ -85,7 +80,7 @@ async def test_auth(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) ->
creds = captured_creds creds = captured_creds
assert creds.token == FAKE_TOKEN assert creds.token == FAKE_TOKEN
assert creds.refresh_token == FAKE_REFRESH_TOKEN assert creds.refresh_token == FAKE_REFRESH_TOKEN
assert int(dt.as_timestamp(creds.expiry)) == int(expiration_time) assert int(dt.as_timestamp(creds.expiry)) == int(token_expiration_time)
assert creds.valid assert creds.valid
assert not creds.expired assert not creds.expired
assert creds.token_uri == OAUTH2_TOKEN assert creds.token_uri == OAUTH2_TOKEN
@ -96,15 +91,18 @@ async def test_auth(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) ->
# This tests needs to be adjusted to remove lingering tasks # This tests needs to be adjusted to remove lingering tasks
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY]) @pytest.mark.parametrize(
"token_expiration_time",
[time.time() - 7 * 86400],
ids=["expires-in-past"],
)
async def test_auth_expired_token( async def test_auth_expired_token(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
setup_platform: PlatformSetup,
token_expiration_time: float,
) -> None: ) -> None:
"""Verify behavior of an expired token.""" """Verify behavior of an expired token."""
expiration_time = time.time() - 86400
create_config_entry(expiration_time).add_to_hass(hass)
# Prepare a token refresh response # Prepare a token refresh response
aioclient_mock.post( aioclient_mock.post(
OAUTH2_TOKEN, OAUTH2_TOKEN,
@ -134,7 +132,7 @@ async def test_auth_expired_token(
"google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber", "google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber",
side_effect=async_new_subscriber, side_effect=async_new_subscriber,
) as new_subscriber_mock: ) as new_subscriber_mock:
await async_setup_sdm(hass) await setup_platform()
calls = aioclient_mock.mock_calls calls = aioclient_mock.mock_calls
assert len(calls) == 3 assert len(calls) == 3
@ -159,7 +157,7 @@ async def test_auth_expired_token(
creds = captured_creds creds = captured_creds
assert creds.token == FAKE_TOKEN assert creds.token == FAKE_TOKEN
assert creds.refresh_token == FAKE_REFRESH_TOKEN assert creds.refresh_token == FAKE_REFRESH_TOKEN
assert int(dt.as_timestamp(creds.expiry)) == int(expiration_time) assert int(dt.as_timestamp(creds.expiry)) == int(token_expiration_time)
assert not creds.valid assert not creds.valid
assert creds.expired assert creds.expired
assert creds.token_uri == OAUTH2_TOKEN assert creds.token_uri == OAUTH2_TOKEN

View file

@ -14,10 +14,6 @@ import pytest
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import dhcp from homeassistant.components import dhcp
from homeassistant.components.application_credentials import (
ClientCredential,
async_import_client_credential,
)
from homeassistant.components.nest.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant.components.nest.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -25,23 +21,17 @@ from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow
from .common import ( from .common import (
APP_AUTH_DOMAIN,
CLIENT_ID, CLIENT_ID,
CLIENT_SECRET,
CLOUD_PROJECT_ID, CLOUD_PROJECT_ID,
FAKE_TOKEN,
PROJECT_ID, PROJECT_ID,
SUBSCRIBER_ID, SUBSCRIBER_ID,
TEST_CONFIG_APP_CREDS, TEST_CONFIG_APP_CREDS,
TEST_CONFIG_HYBRID,
TEST_CONFIG_YAML_ONLY,
TEST_CONFIGFLOW_APP_CREDS, TEST_CONFIGFLOW_APP_CREDS,
TEST_CONFIGFLOW_YAML_ONLY,
WEB_AUTH_DOMAIN,
MockConfigEntry,
NestTestConfig, NestTestConfig,
) )
from tests.common import MockConfigEntry
WEB_REDIRECT_URL = "https://example.com/auth/external/callback" WEB_REDIRECT_URL = "https://example.com/auth/external/callback"
APP_REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob" APP_REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob"
@ -51,6 +41,12 @@ FAKE_DHCP_DATA = dhcp.DhcpServiceInfo(
) )
@pytest.fixture
def nest_test_config(request) -> NestTestConfig:
"""Fixture with empty configuration and no existing config entry."""
return TEST_CONFIGFLOW_APP_CREDS
class OAuthFixture: class OAuthFixture:
"""Simulate the oauth flow used by the config flow.""" """Simulate the oauth flow used by the config flow."""
@ -196,7 +192,6 @@ async def oauth(hass, hass_client_no_auth, aioclient_mock, current_request_with_
return OAuthFixture(hass, hass_client_no_auth, aioclient_mock) return OAuthFixture(hass, hass_client_no_auth, aioclient_mock)
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_app_credentials( async def test_app_credentials(
hass: HomeAssistant, oauth, subscriber, setup_platform hass: HomeAssistant, oauth, subscriber, setup_platform
) -> None: ) -> None:
@ -230,7 +225,6 @@ async def test_app_credentials(
} }
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_config_flow_restart( async def test_config_flow_restart(
hass: HomeAssistant, oauth, subscriber, setup_platform hass: HomeAssistant, oauth, subscriber, setup_platform
) -> None: ) -> None:
@ -283,7 +277,6 @@ async def test_config_flow_restart(
} }
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_config_flow_wrong_project_id( async def test_config_flow_wrong_project_id(
hass: HomeAssistant, oauth, subscriber, setup_platform hass: HomeAssistant, oauth, subscriber, setup_platform
) -> None: ) -> None:
@ -335,7 +328,6 @@ async def test_config_flow_wrong_project_id(
} }
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_config_flow_pubsub_configuration_error( async def test_config_flow_pubsub_configuration_error(
hass: HomeAssistant, hass: HomeAssistant,
oauth, oauth,
@ -358,7 +350,6 @@ async def test_config_flow_pubsub_configuration_error(
assert result["errors"]["cloud_project_id"] == "bad_project_id" assert result["errors"]["cloud_project_id"] == "bad_project_id"
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_config_flow_pubsub_subscriber_error( async def test_config_flow_pubsub_subscriber_error(
hass: HomeAssistant, oauth, setup_platform, mock_subscriber hass: HomeAssistant, oauth, setup_platform, mock_subscriber
) -> None: ) -> None:
@ -379,50 +370,7 @@ async def test_config_flow_pubsub_subscriber_error(
assert result["errors"]["cloud_project_id"] == "subscriber_error" assert result["errors"]["cloud_project_id"] == "subscriber_error"
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY]) @pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_APP_CREDS])
async def test_config_yaml_ignored(hass: HomeAssistant, oauth, setup_platform) -> None:
"""Check full flow."""
await setup_platform()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == "form"
assert result["step_id"] == "create_cloud_project"
result = await oauth.async_configure(result, {})
assert result.get("type") == "abort"
assert result.get("reason") == "missing_credentials"
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_YAML_ONLY])
async def test_web_reauth(
hass: HomeAssistant, oauth, setup_platform, config_entry
) -> None:
"""Test Nest reauthentication."""
await setup_platform()
assert config_entry.data["token"].get("access_token") == FAKE_TOKEN
orig_subscriber_id = config_entry.data.get("subscriber_id")
result = await oauth.async_reauth(config_entry)
await oauth.async_oauth_web_flow(result)
entry = await oauth.async_finish_setup(result)
# Verify existing tokens are replaced
entry.data["token"].pop("expires_at")
assert entry.unique_id == PROJECT_ID
assert entry.data["token"] == {
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
"type": "Bearer",
"expires_in": 60,
}
assert entry.data["auth_implementation"] == WEB_AUTH_DOMAIN
assert entry.data.get("subscriber_id") == orig_subscriber_id # Not updated
async def test_multiple_config_entries( async def test_multiple_config_entries(
hass: HomeAssistant, oauth, setup_platform hass: HomeAssistant, oauth, setup_platform
) -> None: ) -> None:
@ -444,6 +392,7 @@ async def test_multiple_config_entries(
assert len(entries) == 2 assert len(entries) == 2
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_APP_CREDS])
async def test_duplicate_config_entries( async def test_duplicate_config_entries(
hass: HomeAssistant, oauth, setup_platform hass: HomeAssistant, oauth, setup_platform
) -> None: ) -> None:
@ -468,6 +417,7 @@ async def test_duplicate_config_entries(
assert result.get("reason") == "already_configured" assert result.get("reason") == "already_configured"
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_APP_CREDS])
async def test_reauth_multiple_config_entries( async def test_reauth_multiple_config_entries(
hass: HomeAssistant, oauth, setup_platform, config_entry hass: HomeAssistant, oauth, setup_platform, config_entry
) -> None: ) -> None:
@ -517,102 +467,6 @@ async def test_reauth_multiple_config_entries(
assert entry.data.get("extra_data") assert entry.data.get("extra_data")
@pytest.mark.parametrize(
("nest_test_config", "auth_implementation"), [(TEST_CONFIG_HYBRID, APP_AUTH_DOMAIN)]
)
async def test_app_auth_yaml_reauth(
hass: HomeAssistant, oauth, setup_platform, config_entry
) -> None:
"""Test reauth for deprecated app auth credentails upgrade instructions."""
await setup_platform()
orig_subscriber_id = config_entry.data.get("subscriber_id")
assert config_entry.data["auth_implementation"] == APP_AUTH_DOMAIN
result = oauth.async_progress()
assert result.get("step_id") == "reauth_confirm"
result = await oauth.async_configure(result, {})
assert result.get("type") == "form"
assert result.get("step_id") == "auth_upgrade"
result = await oauth.async_configure(result, {})
assert result.get("type") == "abort"
assert result.get("reason") == "missing_credentials"
await hass.async_block_till_done()
# Config flow is aborted, but new one created back in re-auth state waiting for user
# to create application credentials
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
# Emulate user entering credentials (different from configuration.yaml creds)
await async_import_client_credential(
hass,
DOMAIN,
ClientCredential(CLIENT_ID, CLIENT_SECRET),
)
# Config flow is placed back into a reuath state
result = oauth.async_progress()
assert result.get("step_id") == "reauth_confirm"
result = await oauth.async_configure(result, {})
assert result.get("type") == "form"
assert result.get("step_id") == "device_project_upgrade"
# Frontend sends user back through the config flow again
result = await oauth.async_configure(result, {})
await oauth.async_oauth_web_flow(result)
# Verify existing tokens are replaced
entry = await oauth.async_finish_setup(result, {"code": "1234"})
entry.data["token"].pop("expires_at")
assert entry.unique_id == PROJECT_ID
assert entry.data["token"] == {
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
"type": "Bearer",
"expires_in": 60,
}
assert entry.data["auth_implementation"] == DOMAIN
assert entry.data.get("subscriber_id") == orig_subscriber_id # Not updated
# Existing entry is updated
assert config_entry.data["auth_implementation"] == DOMAIN
@pytest.mark.parametrize(
("nest_test_config", "auth_implementation"),
[(TEST_CONFIG_YAML_ONLY, WEB_AUTH_DOMAIN)],
)
async def test_web_auth_yaml_reauth(
hass: HomeAssistant, oauth, setup_platform, config_entry
) -> None:
"""Test Nest reauthentication for Installed App Auth."""
await setup_platform()
orig_subscriber_id = config_entry.data.get("subscriber_id")
result = await oauth.async_reauth(config_entry)
await oauth.async_oauth_web_flow(result)
# Verify existing tokens are replaced
entry = await oauth.async_finish_setup(result, {"code": "1234"})
entry.data["token"].pop("expires_at")
assert entry.unique_id == PROJECT_ID
assert entry.data["token"] == {
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
"type": "Bearer",
"expires_in": 60,
}
assert entry.data["auth_implementation"] == WEB_AUTH_DOMAIN
assert entry.data.get("subscriber_id") == orig_subscriber_id # Not updated
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_pubsub_subscription_strip_whitespace( async def test_pubsub_subscription_strip_whitespace(
hass: HomeAssistant, oauth, subscriber, setup_platform hass: HomeAssistant, oauth, subscriber, setup_platform
) -> None: ) -> None:
@ -641,7 +495,6 @@ async def test_pubsub_subscription_strip_whitespace(
assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_pubsub_subscription_auth_failure( async def test_pubsub_subscription_auth_failure(
hass: HomeAssistant, oauth, setup_platform, mock_subscriber hass: HomeAssistant, oauth, setup_platform, mock_subscriber
) -> None: ) -> None:
@ -668,7 +521,6 @@ async def test_pubsub_subscriber_config_entry_reauth(
setup_platform, setup_platform,
subscriber, subscriber,
config_entry, config_entry,
auth_implementation,
) -> None: ) -> None:
"""Test the pubsub subscriber id is preserved during reauth.""" """Test the pubsub subscriber id is preserved during reauth."""
await setup_platform() await setup_platform()
@ -686,12 +538,11 @@ async def test_pubsub_subscriber_config_entry_reauth(
"type": "Bearer", "type": "Bearer",
"expires_in": 60, "expires_in": 60,
} }
assert entry.data["auth_implementation"] == auth_implementation assert entry.data["auth_implementation"] == "imported-cred"
assert entry.data["subscriber_id"] == SUBSCRIBER_ID assert entry.data["subscriber_id"] == SUBSCRIBER_ID
assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_config_entry_title_from_home( async def test_config_entry_title_from_home(
hass: HomeAssistant, oauth, setup_platform, subscriber hass: HomeAssistant, oauth, setup_platform, subscriber
) -> None: ) -> None:
@ -725,7 +576,6 @@ async def test_config_entry_title_from_home(
assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_config_entry_title_multiple_homes( async def test_config_entry_title_multiple_homes(
hass: HomeAssistant, oauth, setup_platform, subscriber hass: HomeAssistant, oauth, setup_platform, subscriber
) -> None: ) -> None:
@ -768,7 +618,6 @@ async def test_config_entry_title_multiple_homes(
assert entry.title == "Example Home #1, Example Home #2" assert entry.title == "Example Home #1, Example Home #2"
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_title_failure_fallback( async def test_title_failure_fallback(
hass: HomeAssistant, oauth, setup_platform, mock_subscriber hass: HomeAssistant, oauth, setup_platform, mock_subscriber
) -> None: ) -> None:
@ -788,7 +637,6 @@ async def test_title_failure_fallback(
assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_structure_missing_trait( async def test_structure_missing_trait(
hass: HomeAssistant, oauth, setup_platform, subscriber hass: HomeAssistant, oauth, setup_platform, subscriber
) -> None: ) -> None:
@ -818,7 +666,9 @@ async def test_structure_missing_trait(
@pytest.mark.parametrize("nest_test_config", [NestTestConfig()]) @pytest.mark.parametrize("nest_test_config", [NestTestConfig()])
async def test_dhcp_discovery(hass: HomeAssistant, oauth, subscriber) -> None: async def test_dhcp_discovery(
hass: HomeAssistant, oauth: OAuthFixture, nest_test_config: NestTestConfig
) -> None:
"""Exercise discovery dhcp starts the config flow and kicks user to frontend creds flow.""" """Exercise discovery dhcp starts the config flow and kicks user to frontend creds flow."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -834,7 +684,6 @@ async def test_dhcp_discovery(hass: HomeAssistant, oauth, subscriber) -> None:
assert result.get("reason") == "missing_credentials" assert result.get("reason") == "missing_credentials"
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_dhcp_discovery_with_creds( async def test_dhcp_discovery_with_creds(
hass: HomeAssistant, oauth, subscriber, setup_platform hass: HomeAssistant, oauth, subscriber, setup_platform
) -> None: ) -> None:

View file

@ -26,9 +26,6 @@ from homeassistant.core import HomeAssistant
from .common import ( from .common import (
PROJECT_ID, PROJECT_ID,
SUBSCRIBER_ID, SUBSCRIBER_ID,
TEST_CONFIG_APP_CREDS,
TEST_CONFIG_HYBRID,
TEST_CONFIG_YAML_ONLY,
TEST_CONFIGFLOW_APP_CREDS, TEST_CONFIGFLOW_APP_CREDS,
FakeSubscriber, FakeSubscriber,
YieldFixture, YieldFixture,
@ -180,10 +177,7 @@ async def test_subscriber_configuration_failure(
assert entries[0].state is ConfigEntryState.SETUP_ERROR assert entries[0].state is ConfigEntryState.SETUP_ERROR
@pytest.mark.parametrize( @pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
"nest_test_config",
[TEST_CONFIGFLOW_APP_CREDS],
)
async def test_empty_config( async def test_empty_config(
hass: HomeAssistant, error_caplog, config, setup_platform hass: HomeAssistant, error_caplog, config, setup_platform
) -> None: ) -> None:
@ -208,26 +202,9 @@ async def test_unload_entry(hass: HomeAssistant, setup_platform) -> None:
assert entry.state == ConfigEntryState.NOT_LOADED assert entry.state == ConfigEntryState.NOT_LOADED
@pytest.mark.parametrize(
("nest_test_config", "delete_called"),
[
(
TEST_CONFIG_YAML_ONLY,
False,
), # User manually created subscriber, preserve on remove
(
TEST_CONFIG_HYBRID,
True,
), # Integration created subscriber, garbage collect on remove
(
TEST_CONFIG_APP_CREDS,
True,
), # Integration created subscriber, garbage collect on remove
],
ids=["yaml-config-only", "hybrid-config", "config-entry"],
)
async def test_remove_entry( async def test_remove_entry(
hass: HomeAssistant, nest_test_config, setup_base_platform, delete_called hass: HomeAssistant,
setup_base_platform,
) -> None: ) -> None:
"""Test successful unload of a ConfigEntry.""" """Test successful unload of a ConfigEntry."""
with patch( with patch(
@ -250,19 +227,14 @@ async def test_remove_entry(
"homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription", "homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription",
) as delete: ) as delete:
assert await hass.config_entries.async_remove(entry.entry_id) assert await hass.config_entries.async_remove(entry.entry_id)
assert delete.called == delete_called assert delete.called
entries = hass.config_entries.async_entries(DOMAIN) entries = hass.config_entries.async_entries(DOMAIN)
assert not entries assert not entries
@pytest.mark.parametrize(
"nest_test_config",
[TEST_CONFIG_HYBRID, TEST_CONFIG_APP_CREDS],
ids=["hyrbid-config", "app-creds"],
)
async def test_remove_entry_delete_subscriber_failure( async def test_remove_entry_delete_subscriber_failure(
hass: HomeAssistant, nest_test_config, setup_base_platform hass: HomeAssistant, setup_base_platform
) -> None: ) -> None:
"""Test a failure when deleting the subscription.""" """Test a failure when deleting the subscription."""
with patch( with patch(