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

pull/92325/head
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
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.http import KEY_HASS_USER
from homeassistant.components.http.view import HomeAssistantView
@ -52,11 +48,6 @@ from homeassistant.helpers import (
entity_registry as er,
)
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 . import api, config_flow
@ -69,8 +60,6 @@ from .const import (
DATA_SDM,
DATA_SUBSCRIBER,
DOMAIN,
INSTALLED_AUTH_DOMAIN,
WEB_AUTH_DOMAIN,
)
from .events import EVENT_NAME_MAP, NEST_EVENT
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:
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]
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:
return await async_setup_legacy_entry(hass, entry)
if config_mode == config_flow.ConfigMode.SDM:
await async_import_config(hass, entry)
elif entry.unique_id != entry.data[CONF_PROJECT_ID]:
if entry.unique_id != entry.data[CONF_PROJECT_ID]:
hass.config_entries.async_update_entry(
entry, unique_id=entry.data[CONF_PROJECT_ID]
)
async_delete_issue(hass, DOMAIN, "removed_app_auth")
subscriber = await api.new_subscriber(hass, entry)
if not subscriber:
return False
@ -239,71 +221,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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:
"""Unload a config entry."""
if DATA_SDM not in entry.data:

View File

@ -43,7 +43,6 @@ from .const import (
DATA_NEST_CONFIG,
DATA_SDM,
DOMAIN,
INSTALLED_AUTH_DOMAIN,
OAUTH2_AUTHORIZE,
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
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 = (
"https://console.nest.google.com/device-access/project/{project_id}/information"
)
@ -161,7 +156,6 @@ class NestFlowHandler(
def __init__(self) -> None:
"""Initialize NestFlowHandler."""
super().__init__()
self._upgrade = False
self._data: dict[str, Any] = {DATA_SDM: {}}
# Possible name to use for config entry based on the Google Home name
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"
if user_input is None:
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()
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(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@ -358,39 +322,6 @@ class NestFlowHandler(
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(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:

View File

@ -4,14 +4,6 @@
},
"config": {
"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": {
"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."
@ -90,14 +82,6 @@
}
},
"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": {
"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."

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.nest import DOMAIN
from homeassistant.components.nest.const import SDM_SCOPES
from tests.common import MockConfigEntry
# Typing helpers
PlatformSetup = Callable[[], Awaitable[None]]
@ -36,98 +33,28 @@ CLOUD_PROJECT_ID = "cloud-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
class NestTestConfig:
"""Holder for integration configuration."""
config: dict[str, Any] = field(default_factory=dict)
config_entry_data: dict[str, Any] | None = None
auth_implementation: str = WEB_AUTH_DOMAIN
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
TEST_CONFIG_APP_CREDS = NestTestConfig(
config_entry_data={
"sdm": {},
"token": create_token_entry(),
"project_id": PROJECT_ID,
"cloud_project_id": CLOUD_PROJECT_ID,
"subscriber_id": SUBSCRIBER_ID,
"auth_implementation": "imported-cred",
},
auth_implementation="imported-cred",
credential=ClientCredential(CLIENT_ID, CLIENT_SECRET),
)
TEST_CONFIGFLOW_APP_CREDS = NestTestConfig(
config=TEST_CONFIG_APP_CREDS.config,
auth_implementation="imported-cred",
credential=ClientCredential(CLIENT_ID, CLIENT_SECRET),
)

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Generator
import copy
import shutil
import time
from typing import Any
from unittest.mock import AsyncMock, patch
import uuid
@ -18,7 +19,7 @@ from homeassistant.components.application_credentials import (
async_import_client_credential,
)
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.setup import async_setup_component
@ -27,7 +28,6 @@ from .common import (
PROJECT_ID,
SUBSCRIBER_ID,
TEST_CONFIG_APP_CREDS,
TEST_CONFIG_YAML_ONLY,
CreateDevice,
FakeSubscriber,
NestTestConfig,
@ -37,6 +37,9 @@ from .common import (
from tests.common import MockConfigEntry
FAKE_TOKEN = "some-token"
FAKE_REFRESH_TOKEN = "some-refresh-token"
class FakeAuth(AbstractAuth):
"""A fake implementation of the auth class that records requests.
@ -186,18 +189,9 @@ def subscriber_id() -> str:
@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:
"""Fixture that sets up the configuration used for the test."""
return request.param
return TEST_CONFIG_APP_CREDS
@pytest.fixture
@ -220,12 +214,30 @@ def config_entry_unique_id() -> str:
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
def config_entry(
subscriber_id: str | None,
auth_implementation: str | None,
nest_test_config: NestTestConfig,
config_entry_unique_id: str,
token_entry: dict[str, Any],
) -> MockConfigEntry | None:
"""Fixture that sets up the ConfigEntry for the test."""
if nest_test_config.config_entry_data is None:
@ -236,7 +248,7 @@ def config_entry(
data[CONF_SUBSCRIBER_ID] = subscriber_id
else:
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)
@ -247,10 +259,7 @@ async def credential(hass: HomeAssistant, nest_test_config: NestTestConfig) -> N
return
assert await async_setup_component(hass, "application_credentials", {})
await async_import_client_credential(
hass,
DOMAIN,
nest_test_config.credential,
nest_test_config.auth_implementation,
hass, DOMAIN, nest_test_config.credential, "imported-cred"
)

View File

@ -12,43 +12,38 @@ from unittest.mock import patch
import pytest
from homeassistant.components.nest import DOMAIN
from homeassistant.components.nest.const import API_URL, OAUTH2_TOKEN, SDM_SCOPES
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
from .common import (
CLIENT_ID,
CLIENT_SECRET,
CONFIG,
FAKE_REFRESH_TOKEN,
FAKE_TOKEN,
PROJECT_ID,
TEST_CONFIGFLOW_YAML_ONLY,
create_config_entry,
)
from .common import CLIENT_ID, CLIENT_SECRET, PROJECT_ID, PlatformSetup
from .conftest import FAKE_REFRESH_TOKEN, FAKE_TOKEN
from tests.test_util.aiohttp import AiohttpClientMocker
FAKE_UPDATED_TOKEN = "fake-updated-token"
async def async_setup_sdm(hass):
"""Set up the integration."""
assert await async_setup_component(hass, DOMAIN, CONFIG)
await hass.async_block_till_done()
@pytest.fixture
def subscriber() -> None:
"""Disable default subscriber since tests use their own patch."""
return None
# This tests needs to be adjusted to remove lingering tasks
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY])
async def test_auth(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None:
@pytest.mark.parametrize(
"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."""
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
# no devices or structures are loaded.
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",
side_effect=async_new_subscriber,
) as new_subscriber_mock:
await async_setup_sdm(hass)
await setup_platform()
# Verify API requests are made with the correct credentials
calls = aioclient_mock.mock_calls
@ -85,7 +80,7 @@ async def test_auth(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) ->
creds = captured_creds
assert creds.token == FAKE_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 not creds.expired
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
@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(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
setup_platform: PlatformSetup,
token_expiration_time: float,
) -> None:
"""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
aioclient_mock.post(
OAUTH2_TOKEN,
@ -134,7 +132,7 @@ async def test_auth_expired_token(
"google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber",
side_effect=async_new_subscriber,
) as new_subscriber_mock:
await async_setup_sdm(hass)
await setup_platform()
calls = aioclient_mock.mock_calls
assert len(calls) == 3
@ -159,7 +157,7 @@ async def test_auth_expired_token(
creds = captured_creds
assert creds.token == FAKE_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 creds.expired
assert creds.token_uri == OAUTH2_TOKEN

View File

@ -14,10 +14,6 @@ import pytest
from homeassistant import config_entries
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.config_entries import ConfigEntry
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 .common import (
APP_AUTH_DOMAIN,
CLIENT_ID,
CLIENT_SECRET,
CLOUD_PROJECT_ID,
FAKE_TOKEN,
PROJECT_ID,
SUBSCRIBER_ID,
TEST_CONFIG_APP_CREDS,
TEST_CONFIG_HYBRID,
TEST_CONFIG_YAML_ONLY,
TEST_CONFIGFLOW_APP_CREDS,
TEST_CONFIGFLOW_YAML_ONLY,
WEB_AUTH_DOMAIN,
MockConfigEntry,
NestTestConfig,
)
from tests.common import MockConfigEntry
WEB_REDIRECT_URL = "https://example.com/auth/external/callback"
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:
"""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)
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_app_credentials(
hass: HomeAssistant, oauth, subscriber, setup_platform
) -> 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(
hass: HomeAssistant, oauth, subscriber, setup_platform
) -> 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(
hass: HomeAssistant, oauth, subscriber, setup_platform
) -> 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(
hass: HomeAssistant,
oauth,
@ -358,7 +350,6 @@ async def test_config_flow_pubsub_configuration_error(
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(
hass: HomeAssistant, oauth, setup_platform, mock_subscriber
) -> None:
@ -379,50 +370,7 @@ async def test_config_flow_pubsub_subscriber_error(
assert result["errors"]["cloud_project_id"] == "subscriber_error"
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY])
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
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_APP_CREDS])
async def test_multiple_config_entries(
hass: HomeAssistant, oauth, setup_platform
) -> None:
@ -444,6 +392,7 @@ async def test_multiple_config_entries(
assert len(entries) == 2
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_APP_CREDS])
async def test_duplicate_config_entries(
hass: HomeAssistant, oauth, setup_platform
) -> None:
@ -468,6 +417,7 @@ async def test_duplicate_config_entries(
assert result.get("reason") == "already_configured"
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_APP_CREDS])
async def test_reauth_multiple_config_entries(
hass: HomeAssistant, oauth, setup_platform, config_entry
) -> None:
@ -517,102 +467,6 @@ async def test_reauth_multiple_config_entries(
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(
hass: HomeAssistant, oauth, subscriber, setup_platform
) -> None:
@ -641,7 +495,6 @@ async def test_pubsub_subscription_strip_whitespace(
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(
hass: HomeAssistant, oauth, setup_platform, mock_subscriber
) -> None:
@ -668,7 +521,6 @@ async def test_pubsub_subscriber_config_entry_reauth(
setup_platform,
subscriber,
config_entry,
auth_implementation,
) -> None:
"""Test the pubsub subscriber id is preserved during reauth."""
await setup_platform()
@ -686,12 +538,11 @@ async def test_pubsub_subscriber_config_entry_reauth(
"type": "Bearer",
"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["cloud_project_id"] == CLOUD_PROJECT_ID
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_config_entry_title_from_home(
hass: HomeAssistant, oauth, setup_platform, subscriber
) -> None:
@ -725,7 +576,6 @@ async def test_config_entry_title_from_home(
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(
hass: HomeAssistant, oauth, setup_platform, subscriber
) -> None:
@ -768,7 +618,6 @@ async def test_config_entry_title_multiple_homes(
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(
hass: HomeAssistant, oauth, setup_platform, mock_subscriber
) -> None:
@ -788,7 +637,6 @@ async def test_title_failure_fallback(
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(
hass: HomeAssistant, oauth, setup_platform, subscriber
) -> None:
@ -818,7 +666,9 @@ async def test_structure_missing_trait(
@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."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
@ -834,7 +684,6 @@ async def test_dhcp_discovery(hass: HomeAssistant, oauth, subscriber) -> None:
assert result.get("reason") == "missing_credentials"
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_dhcp_discovery_with_creds(
hass: HomeAssistant, oauth, subscriber, setup_platform
) -> None:

View File

@ -26,9 +26,6 @@ from homeassistant.core import HomeAssistant
from .common import (
PROJECT_ID,
SUBSCRIBER_ID,
TEST_CONFIG_APP_CREDS,
TEST_CONFIG_HYBRID,
TEST_CONFIG_YAML_ONLY,
TEST_CONFIGFLOW_APP_CREDS,
FakeSubscriber,
YieldFixture,
@ -180,10 +177,7 @@ async def test_subscriber_configuration_failure(
assert entries[0].state is ConfigEntryState.SETUP_ERROR
@pytest.mark.parametrize(
"nest_test_config",
[TEST_CONFIGFLOW_APP_CREDS],
)
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS])
async def test_empty_config(
hass: HomeAssistant, error_caplog, config, setup_platform
) -> None:
@ -208,26 +202,9 @@ async def test_unload_entry(hass: HomeAssistant, setup_platform) -> None:
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(
hass: HomeAssistant, nest_test_config, setup_base_platform, delete_called
hass: HomeAssistant,
setup_base_platform,
) -> None:
"""Test successful unload of a ConfigEntry."""
with patch(
@ -250,19 +227,14 @@ async def test_remove_entry(
"homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription",
) as delete:
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)
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(
hass: HomeAssistant, nest_test_config, setup_base_platform
hass: HomeAssistant, setup_base_platform
) -> None:
"""Test a failure when deleting the subscription."""
with patch(