Remove profile from Withings config flow (#100202)
* Remove profile from Withings config flow * Add config flow migration * Add config flow migration * Remove datamanager profile * Remove datamanager profile * Add manufacturer * Remove migration * Remove migration * Fix feedback
This commit is contained in:
parent
80aa19263b
commit
8498cdfb3c
11 changed files with 257 additions and 375 deletions
|
@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.web import Request, Response
|
||||
import voluptuous as vol
|
||||
|
@ -17,12 +16,14 @@ from homeassistant.components.application_credentials import (
|
|||
async_import_client_credential,
|
||||
)
|
||||
from homeassistant.components.webhook import (
|
||||
async_generate_id,
|
||||
async_unregister as async_unregister_webhook,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
CONF_CLIENT_SECRET,
|
||||
CONF_TOKEN,
|
||||
CONF_WEBHOOK_ID,
|
||||
Platform,
|
||||
)
|
||||
|
@ -39,6 +40,7 @@ from .common import (
|
|||
get_data_manager_by_webhook_id,
|
||||
json_message_response,
|
||||
)
|
||||
from .const import CONF_USE_WEBHOOK, CONFIG
|
||||
|
||||
DOMAIN = const.DOMAIN
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
@ -103,33 +105,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Withings from a config entry."""
|
||||
config_updates: dict[str, Any] = {}
|
||||
|
||||
# Add a unique id if it's an older config entry.
|
||||
if entry.unique_id != entry.data["token"]["userid"] or not isinstance(
|
||||
entry.unique_id, str
|
||||
):
|
||||
config_updates["unique_id"] = str(entry.data["token"]["userid"])
|
||||
|
||||
# Add the webhook configuration.
|
||||
if CONF_WEBHOOK_ID not in entry.data:
|
||||
webhook_id = webhook.async_generate_id()
|
||||
config_updates["data"] = {
|
||||
**entry.data,
|
||||
**{
|
||||
const.CONF_USE_WEBHOOK: hass.data[DOMAIN][const.CONFIG][
|
||||
const.CONF_USE_WEBHOOK
|
||||
],
|
||||
CONF_WEBHOOK_ID: webhook_id,
|
||||
},
|
||||
if CONF_USE_WEBHOOK not in entry.options:
|
||||
new_data = entry.data.copy()
|
||||
new_options = {
|
||||
CONF_USE_WEBHOOK: new_data.get(CONF_USE_WEBHOOK, False),
|
||||
}
|
||||
unique_id = str(entry.data[CONF_TOKEN]["userid"])
|
||||
if CONF_WEBHOOK_ID not in new_data:
|
||||
new_data[CONF_WEBHOOK_ID] = async_generate_id()
|
||||
|
||||
if config_updates:
|
||||
hass.config_entries.async_update_entry(entry, **config_updates)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data=new_data, options=new_options, unique_id=unique_id
|
||||
)
|
||||
use_webhook = hass.data[DOMAIN][CONFIG][CONF_USE_WEBHOOK]
|
||||
if use_webhook is not None and use_webhook != entry.options[CONF_USE_WEBHOOK]:
|
||||
new_options = entry.options.copy()
|
||||
new_options |= {CONF_USE_WEBHOOK: use_webhook}
|
||||
hass.config_entries.async_update_entry(entry, options=new_options)
|
||||
|
||||
data_manager = await async_get_data_manager(hass, entry)
|
||||
|
||||
_LOGGER.debug("Confirming %s is authenticated to withings", data_manager.profile)
|
||||
_LOGGER.debug("Confirming %s is authenticated to withings", entry.title)
|
||||
await data_manager.poll_data_update_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
webhook.async_register(
|
||||
|
|
|
@ -203,7 +203,6 @@ class DataManager:
|
|||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
profile: str,
|
||||
api: ConfigEntryWithingsApi,
|
||||
user_id: int,
|
||||
webhook_config: WebhookConfig,
|
||||
|
@ -212,7 +211,6 @@ class DataManager:
|
|||
self._hass = hass
|
||||
self._api = api
|
||||
self._user_id = user_id
|
||||
self._profile = profile
|
||||
self._webhook_config = webhook_config
|
||||
self._notify_subscribe_delay = SUBSCRIBE_DELAY
|
||||
self._notify_unsubscribe_delay = UNSUBSCRIBE_DELAY
|
||||
|
@ -256,11 +254,6 @@ class DataManager:
|
|||
"""Get the user_id of the authenticated user."""
|
||||
return self._user_id
|
||||
|
||||
@property
|
||||
def profile(self) -> str:
|
||||
"""Get the profile."""
|
||||
return self._profile
|
||||
|
||||
def async_start_polling_webhook_subscriptions(self) -> None:
|
||||
"""Start polling webhook subscriptions (if enabled) to reconcile their setup."""
|
||||
self.async_stop_polling_webhook_subscriptions()
|
||||
|
@ -530,12 +523,11 @@ async def async_get_data_manager(
|
|||
config_entry_data = hass.data[const.DOMAIN][config_entry.entry_id]
|
||||
|
||||
if const.DATA_MANAGER not in config_entry_data:
|
||||
profile: str = config_entry.data[const.PROFILE]
|
||||
|
||||
_LOGGER.debug("Creating withings data manager for profile: %s", profile)
|
||||
_LOGGER.debug(
|
||||
"Creating withings data manager for profile: %s", config_entry.title
|
||||
)
|
||||
config_entry_data[const.DATA_MANAGER] = DataManager(
|
||||
hass,
|
||||
profile,
|
||||
ConfigEntryWithingsApi(
|
||||
hass=hass,
|
||||
config_entry=config_entry,
|
||||
|
@ -549,7 +541,7 @@ async def async_get_data_manager(
|
|||
url=webhook.async_generate_url(
|
||||
hass, config_entry.data[CONF_WEBHOOK_ID]
|
||||
),
|
||||
enabled=config_entry.data[const.CONF_USE_WEBHOOK],
|
||||
enabled=config_entry.options[const.CONF_USE_WEBHOOK],
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
@ -5,26 +5,24 @@ from collections.abc import Mapping
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from withings_api.common import AuthScope
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TOKEN
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import const
|
||||
from .const import CONF_USE_WEBHOOK, DEFAULT_TITLE, DOMAIN
|
||||
|
||||
|
||||
class WithingsFlowHandler(
|
||||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=const.DOMAIN
|
||||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
|
||||
):
|
||||
"""Handle a config flow."""
|
||||
|
||||
DOMAIN = const.DOMAIN
|
||||
DOMAIN = DOMAIN
|
||||
|
||||
# Temporarily holds authorization data during the profile step.
|
||||
_current_data: dict[str, None | str | int] = {}
|
||||
_reauth_profile: str | None = None
|
||||
reauth_entry: ConfigEntry | None = None
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
|
@ -45,64 +43,37 @@ class WithingsFlowHandler(
|
|||
)
|
||||
}
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult:
|
||||
"""Override the create entry so user can select a profile."""
|
||||
self._current_data = data
|
||||
return await self.async_step_profile(data)
|
||||
|
||||
async def async_step_profile(self, data: dict[str, Any]) -> FlowResult:
|
||||
"""Prompt the user to select a user profile."""
|
||||
errors = {}
|
||||
profile = data.get(const.PROFILE) or self._reauth_profile
|
||||
|
||||
if profile:
|
||||
existing_entries = [
|
||||
config_entry
|
||||
for config_entry in self._async_current_entries()
|
||||
if slugify(config_entry.data.get(const.PROFILE)) == slugify(profile)
|
||||
]
|
||||
|
||||
if self._reauth_profile or not existing_entries:
|
||||
new_data = {**self._current_data, **data, const.PROFILE: profile}
|
||||
self._current_data = {}
|
||||
return await self.async_step_finish(new_data)
|
||||
|
||||
errors["base"] = "already_configured"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="profile",
|
||||
data_schema=vol.Schema({vol.Required(const.PROFILE): str}),
|
||||
errors=errors,
|
||||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
self.reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
|
||||
async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult:
|
||||
"""Prompt user to re-authenticate."""
|
||||
self._reauth_profile = data.get(const.PROFILE)
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, data: dict[str, Any] | None = None
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Prompt user to re-authenticate."""
|
||||
if data is not None:
|
||||
return await self.async_step_user()
|
||||
"""Confirm reauth dialog."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
||||
|
||||
placeholders = {const.PROFILE: self._reauth_profile}
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult:
|
||||
"""Create an entry for the flow, or update existing entry."""
|
||||
user_id = str(data[CONF_TOKEN]["userid"])
|
||||
if not self.reauth_entry:
|
||||
await self.async_set_unique_id(user_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
self.context.update({"title_placeholders": placeholders})
|
||||
return self.async_create_entry(
|
||||
title=DEFAULT_TITLE,
|
||||
data=data,
|
||||
options={CONF_USE_WEBHOOK: False},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
description_placeholders=placeholders,
|
||||
)
|
||||
if self.reauth_entry.unique_id == user_id:
|
||||
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")
|
||||
|
||||
async def async_step_finish(self, data: dict[str, Any]) -> FlowResult:
|
||||
"""Finish the flow."""
|
||||
self._current_data = {}
|
||||
|
||||
await self.async_set_unique_id(
|
||||
str(data["token"]["userid"]), raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured(data)
|
||||
|
||||
return self.async_create_entry(title=data[const.PROFILE], data=data)
|
||||
return self.async_abort(reason="wrong_account")
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Constants used by the Withings component."""
|
||||
from enum import StrEnum
|
||||
|
||||
DEFAULT_TITLE = "Withings"
|
||||
CONF_PROFILES = "profiles"
|
||||
CONF_USE_WEBHOOK = "use_webhook"
|
||||
|
||||
|
|
|
@ -46,8 +46,7 @@ class BaseWithingsSensor(Entity):
|
|||
)
|
||||
self._state_data: Any | None = None
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, str(data_manager.user_id))},
|
||||
name=data_manager.profile,
|
||||
identifiers={(DOMAIN, str(data_manager.user_id))}, manufacturer="Withings"
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
|
@ -4,8 +4,10 @@ from typing import Any
|
|||
from urllib.parse import urlparse
|
||||
|
||||
from homeassistant.components.webhook import async_generate_url
|
||||
from homeassistant.components.withings.const import CONF_USE_WEBHOOK, DOMAIN
|
||||
from homeassistant.config import async_process_ha_core_config
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
@ -48,3 +50,16 @@ async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry)
|
|||
)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
|
||||
async def enable_webhooks(hass: HomeAssistant) -> None:
|
||||
"""Enable webhooks."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{
|
||||
DOMAIN: {
|
||||
CONF_USE_WEBHOOK: True,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -96,9 +96,11 @@ def mock_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry:
|
|||
"scope": ",".join(scopes),
|
||||
},
|
||||
"profile": TITLE,
|
||||
"use_webhook": True,
|
||||
"webhook_id": WEBHOOK_ID,
|
||||
},
|
||||
options={
|
||||
"use_webhook": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from withings_api.common import NotifyAppli
|
|||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import call_webhook, setup_integration
|
||||
from . import call_webhook, enable_webhooks, setup_integration
|
||||
from .conftest import USER_ID, WEBHOOK_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
@ -21,6 +21,7 @@ async def test_binary_sensor(
|
|||
hass_client_no_auth: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test binary sensor."""
|
||||
await enable_webhooks(hass)
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
"""Tests for the Withings component."""
|
||||
from http import HTTPStatus
|
||||
import re
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from aiohttp.test_utils import TestClient
|
||||
import pytest
|
||||
import requests_mock
|
||||
from withings_api.common import NotifyAppli
|
||||
|
||||
from homeassistant.components.withings.common import ConfigEntryWithingsApi
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2Implementation
|
||||
|
||||
from .common import ComponentFactory, get_data_manager_by_user_id, new_profile_config
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_config_entry_withings_api(hass: HomeAssistant) -> None:
|
||||
"""Test ConfigEntryWithingsApi."""
|
||||
config_entry = MockConfigEntry(
|
||||
data={"token": {"access_token": "mock_access_token", "expires_at": 1111111}}
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
implementation_mock = MagicMock(spec=AbstractOAuth2Implementation)
|
||||
implementation_mock.async_refresh_token.return_value = {
|
||||
"expires_at": 1111111,
|
||||
"access_token": "mock_access_token",
|
||||
}
|
||||
|
||||
with requests_mock.mock() as rqmck:
|
||||
rqmck.get(
|
||||
re.compile(".*"),
|
||||
status_code=HTTPStatus.OK,
|
||||
json={"status": 0, "body": {"message": "success"}},
|
||||
)
|
||||
|
||||
api = ConfigEntryWithingsApi(hass, config_entry, implementation_mock)
|
||||
response = await hass.async_add_executor_job(
|
||||
api.request, "test", {"arg1": "val1", "arg2": "val2"}
|
||||
)
|
||||
assert response == {"message": "success"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("user_id", "arg_user_id", "arg_appli", "expected_code"),
|
||||
[
|
||||
[0, 0, NotifyAppli.WEIGHT.value, 0], # Success
|
||||
[0, None, 1, 0], # Success, we ignore the user_id.
|
||||
[0, None, None, 12], # No request body.
|
||||
[0, "GG", None, 20], # appli not provided.
|
||||
[0, 0, None, 20], # appli not provided.
|
||||
[0, 0, 99, 21], # Invalid appli.
|
||||
[0, 11, NotifyAppli.WEIGHT.value, 0], # Success, we ignore the user_id
|
||||
],
|
||||
)
|
||||
async def test_webhook_post(
|
||||
hass: HomeAssistant,
|
||||
component_factory: ComponentFactory,
|
||||
aiohttp_client: ClientSessionGenerator,
|
||||
user_id: int,
|
||||
arg_user_id: Any,
|
||||
arg_appli: Any,
|
||||
expected_code: int,
|
||||
current_request_with_host: None,
|
||||
) -> None:
|
||||
"""Test webhook callback."""
|
||||
person0 = new_profile_config("person0", user_id)
|
||||
|
||||
await component_factory.configure_component(profile_configs=(person0,))
|
||||
await component_factory.setup_profile(person0.user_id)
|
||||
data_manager = get_data_manager_by_user_id(hass, user_id)
|
||||
|
||||
client: TestClient = await aiohttp_client(hass.http.app)
|
||||
|
||||
post_data = {}
|
||||
if arg_user_id is not None:
|
||||
post_data["userid"] = arg_user_id
|
||||
if arg_appli is not None:
|
||||
post_data["appli"] = arg_appli
|
||||
|
||||
resp = await client.post(
|
||||
urlparse(data_manager.webhook_config.url).path, data=post_data
|
||||
)
|
||||
|
||||
# Wait for remaining tasks to complete.
|
||||
await hass.async_block_till_done()
|
||||
|
||||
data = await resp.json()
|
||||
resp.close()
|
||||
|
||||
assert data["code"] == expected_code
|
|
@ -1,13 +1,14 @@
|
|||
"""Tests for config flow."""
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from homeassistant.components.withings.const import DOMAIN, PROFILE
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.components.withings.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from .conftest import CLIENT_ID
|
||||
from . import setup_integration
|
||||
from .conftest import CLIENT_ID, USER_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
@ -63,18 +64,12 @@ async def test_full_flow(
|
|||
"homeassistant.components.withings.async_setup_entry", return_value=True
|
||||
) as mock_setup:
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "profile"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={PROFILE: "Henk"}
|
||||
)
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Henk"
|
||||
assert result["title"] == "Withings"
|
||||
assert "result" in result
|
||||
assert result["result"].unique_id == "600"
|
||||
assert "token" in result["result"].data
|
||||
|
@ -86,12 +81,13 @@ async def test_config_non_unique_profile(
|
|||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
current_request_with_host: None,
|
||||
withings: AsyncMock,
|
||||
config_entry: MockConfigEntry,
|
||||
disable_webhook_delay,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test setup a non-unique profile."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data={PROFILE: "Henk"}, unique_id="0")
|
||||
config_entry.add_to_hass(hass)
|
||||
await setup_integration(hass, config_entry)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
@ -126,28 +122,13 @@ async def test_config_non_unique_profile(
|
|||
"access_token": "mock-access-token",
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
"userid": 10,
|
||||
"userid": USER_ID,
|
||||
},
|
||||
},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "profile"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={PROFILE: "Henk"}
|
||||
)
|
||||
|
||||
assert result
|
||||
assert result["errors"]["base"] == "already_configured"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={PROFILE: "Henk 2"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Henk 2"
|
||||
assert "result" in result
|
||||
assert result["result"].unique_id == "10"
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_config_reauth_profile(
|
||||
|
@ -155,18 +136,22 @@ async def test_config_reauth_profile(
|
|||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
config_entry: MockConfigEntry,
|
||||
withings: AsyncMock,
|
||||
disable_webhook_delay,
|
||||
current_request_with_host,
|
||||
) -> None:
|
||||
"""Test reauth an existing profile re-creates the config entry."""
|
||||
config_entry.add_to_hass(hass)
|
||||
"""Test reauth an existing profile reauthenticates the config entry."""
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
config_entry.async_start_reauth(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
result = flows[0]
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
"entry_id": config_entry.entry_id,
|
||||
},
|
||||
data=config_entry.data,
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
@ -198,12 +183,75 @@ async def test_config_reauth_profile(
|
|||
"access_token": "mock-access-token",
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
"userid": "0",
|
||||
"userid": USER_ID,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
assert result
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"]["token"]["refresh_token"] == "mock-refresh-token"
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
|
||||
async def test_config_reauth_wrong_account(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
config_entry: MockConfigEntry,
|
||||
withings: AsyncMock,
|
||||
disable_webhook_delay,
|
||||
current_request_with_host,
|
||||
) -> None:
|
||||
"""Test reauth with wrong account."""
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
"entry_id": config_entry.entry_id,
|
||||
},
|
||||
data=config_entry.data,
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
"flow_id": result["flow_id"],
|
||||
"redirect_uri": "https://example.com/auth/external/callback",
|
||||
},
|
||||
)
|
||||
assert result["url"] == (
|
||||
"https://account.withings.com/oauth2_user/authorize2?"
|
||||
f"response_type=code&client_id={CLIENT_ID}&"
|
||||
"redirect_uri=https://example.com/auth/external/callback&"
|
||||
f"state={state}"
|
||||
"&scope=user.info,user.metrics,user.activity,user.sleepevents"
|
||||
)
|
||||
client = await hass_client_no_auth()
|
||||
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||
assert resp.status == 200
|
||||
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.post(
|
||||
"https://wbsapi.withings.net/v2/oauth2",
|
||||
json={
|
||||
"body": {
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "mock-access-token",
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
"userid": 12346,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
assert result
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "wrong_account"
|
||||
|
|
|
@ -1,31 +1,20 @@
|
|||
"""Tests for the Withings component."""
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
from withings_api.common import NotifyAppli, UnauthorizedException
|
||||
from withings_api.common import NotifyAppli
|
||||
|
||||
import homeassistant.components.webhook as webhook
|
||||
from homeassistant.components.webhook import async_generate_url
|
||||
from homeassistant.components.withings import CONFIG_SCHEMA, DOMAIN, async_setup, const
|
||||
from homeassistant.components.withings.common import ConfigEntryWithingsApi, DataManager
|
||||
from homeassistant.config import async_process_ha_core_config
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
CONF_CLIENT_SECRET,
|
||||
CONF_EXTERNAL_URL,
|
||||
CONF_UNIT_SYSTEM,
|
||||
CONF_UNIT_SYSTEM_METRIC,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_WEBHOOK_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import setup_integration
|
||||
from .common import ComponentFactory, get_data_manager_by_user_id, new_profile_config
|
||||
from . import enable_webhooks, setup_integration
|
||||
from .conftest import WEBHOOK_ID
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
@ -113,126 +102,6 @@ async def test_async_setup_no_config(hass: HomeAssistant) -> None:
|
|||
hass.async_create_task.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
UnauthorizedException("401"),
|
||||
UnauthorizedException("401"),
|
||||
Exception("401, this is the message"),
|
||||
],
|
||||
)
|
||||
@patch("homeassistant.components.withings.common._RETRY_COEFFICIENT", 0)
|
||||
async def test_auth_failure(
|
||||
hass: HomeAssistant,
|
||||
component_factory: ComponentFactory,
|
||||
exception: Exception,
|
||||
current_request_with_host: None,
|
||||
) -> None:
|
||||
"""Test auth failure."""
|
||||
person0 = new_profile_config(
|
||||
"person0",
|
||||
0,
|
||||
api_response_user_get_device=exception,
|
||||
api_response_measure_get_meas=exception,
|
||||
api_response_sleep_get_summary=exception,
|
||||
)
|
||||
|
||||
await component_factory.configure_component(profile_configs=(person0,))
|
||||
assert not hass.config_entries.flow.async_progress()
|
||||
|
||||
await component_factory.setup_profile(person0.user_id)
|
||||
data_manager = get_data_manager_by_user_id(hass, person0.user_id)
|
||||
await data_manager.poll_data_update_coordinator.async_refresh()
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert flows
|
||||
assert len(flows) == 1
|
||||
|
||||
flow = flows[0]
|
||||
assert flow["handler"] == const.DOMAIN
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow["flow_id"], user_input={}
|
||||
)
|
||||
assert result
|
||||
assert result["type"] == "external"
|
||||
assert result["handler"] == const.DOMAIN
|
||||
assert result["step_id"] == "auth"
|
||||
|
||||
await component_factory.unload(person0)
|
||||
|
||||
|
||||
async def test_set_config_unique_id(
|
||||
hass: HomeAssistant, component_factory: ComponentFactory
|
||||
) -> None:
|
||||
"""Test upgrading configs to use a unique id."""
|
||||
person0 = new_profile_config("person0", 0)
|
||||
|
||||
await component_factory.configure_component(profile_configs=(person0,))
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"token": {"userid": "my_user_id"},
|
||||
"auth_implementation": "withings",
|
||||
"profile": person0.profile,
|
||||
},
|
||||
)
|
||||
|
||||
with patch("homeassistant.components.withings.async_get_data_manager") as mock:
|
||||
data_manager: DataManager = MagicMock(spec=DataManager)
|
||||
data_manager.poll_data_update_coordinator = MagicMock(
|
||||
spec=DataUpdateCoordinator
|
||||
)
|
||||
data_manager.poll_data_update_coordinator.last_update_success = True
|
||||
data_manager.subscription_update_coordinator = MagicMock(
|
||||
spec=DataUpdateCoordinator
|
||||
)
|
||||
data_manager.subscription_update_coordinator.last_update_success = True
|
||||
mock.return_value = data_manager
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert config_entry.unique_id == "my_user_id"
|
||||
|
||||
|
||||
async def test_set_convert_unique_id_to_string(hass: HomeAssistant) -> None:
|
||||
"""Test upgrading configs to use a unique id."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"token": {"userid": 1234},
|
||||
"auth_implementation": "withings",
|
||||
"profile": "person0",
|
||||
},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
hass_config = {
|
||||
HA_DOMAIN: {
|
||||
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
|
||||
CONF_EXTERNAL_URL: "http://127.0.0.1:8080/",
|
||||
},
|
||||
const.DOMAIN: {
|
||||
CONF_CLIENT_ID: "my_client_id",
|
||||
CONF_CLIENT_SECRET: "my_client_secret",
|
||||
const.CONF_USE_WEBHOOK: False,
|
||||
},
|
||||
}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.withings.common.ConfigEntryWithingsApi",
|
||||
spec=ConfigEntryWithingsApi,
|
||||
):
|
||||
await async_process_ha_core_config(hass, hass_config.get(HA_DOMAIN))
|
||||
assert await async_setup_component(hass, HA_DOMAIN, {})
|
||||
assert await async_setup_component(hass, webhook.DOMAIN, hass_config)
|
||||
assert await async_setup_component(hass, const.DOMAIN, hass_config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.unique_id == "1234"
|
||||
|
||||
|
||||
async def test_data_manager_webhook_subscription(
|
||||
hass: HomeAssistant,
|
||||
withings: AsyncMock,
|
||||
|
@ -241,6 +110,7 @@ async def test_data_manager_webhook_subscription(
|
|||
hass_client_no_auth: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test data manager webhook subscriptions."""
|
||||
await enable_webhooks(hass)
|
||||
await setup_integration(hass, config_entry)
|
||||
await hass_client_no_auth()
|
||||
await hass.async_block_till_done()
|
||||
|
@ -285,3 +155,87 @@ async def test_requests(
|
|||
path=urlparse(webhook_url).path,
|
||||
)
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry"),
|
||||
[
|
||||
MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="123",
|
||||
data={
|
||||
"token": {"userid": 123},
|
||||
"profile": "henk",
|
||||
"use_webhook": False,
|
||||
"webhook_id": "3290798afaebd28519c4883d3d411c7197572e0cc9b8d507471f59a700a61a55",
|
||||
},
|
||||
),
|
||||
MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="123",
|
||||
data={
|
||||
"token": {"userid": 123},
|
||||
"profile": "henk",
|
||||
"use_webhook": False,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_config_flow_upgrade(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test config flow upgrade."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entry = hass.config_entries.async_get_entry(config_entry.entry_id)
|
||||
|
||||
assert entry.unique_id == "123"
|
||||
assert entry.data["token"]["userid"] == 123
|
||||
assert CONF_WEBHOOK_ID in entry.data
|
||||
assert entry.options == {
|
||||
"use_webhook": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("body", "expected_code"),
|
||||
[
|
||||
[{"userid": 0, "appli": NotifyAppli.WEIGHT.value}, 0], # Success
|
||||
[{"userid": None, "appli": 1}, 0], # Success, we ignore the user_id.
|
||||
[{}, 12], # No request body.
|
||||
[{"userid": "GG"}, 20], # appli not provided.
|
||||
[{"userid": 0}, 20], # appli not provided.
|
||||
[{"userid": 0, "appli": 99}, 21], # Invalid appli.
|
||||
[
|
||||
{"userid": 11, "appli": NotifyAppli.WEIGHT.value},
|
||||
0,
|
||||
], # Success, we ignore the user_id
|
||||
],
|
||||
)
|
||||
async def test_webhook_post(
|
||||
hass: HomeAssistant,
|
||||
withings: AsyncMock,
|
||||
config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
disable_webhook_delay,
|
||||
body: dict[str, Any],
|
||||
expected_code: int,
|
||||
current_request_with_host: None,
|
||||
) -> None:
|
||||
"""Test webhook callback."""
|
||||
await setup_integration(hass, config_entry)
|
||||
client = await hass_client_no_auth()
|
||||
webhook_url = async_generate_url(hass, WEBHOOK_ID)
|
||||
|
||||
resp = await client.post(urlparse(webhook_url).path, data=body)
|
||||
|
||||
# Wait for remaining tasks to complete.
|
||||
await hass.async_block_till_done()
|
||||
|
||||
data = await resp.json()
|
||||
resp.close()
|
||||
|
||||
assert data["code"] == expected_code
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue