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:
Joost Lekkerkerker 2023-09-13 15:49:36 +02:00 committed by GitHub
parent 80aa19263b
commit 8498cdfb3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 257 additions and 375 deletions

View file

@ -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(

View file

@ -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],
),
)

View file

@ -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")

View file

@ -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"

View file

@ -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

View file

@ -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,
}
},
)

View file

@ -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,
},
)

View file

@ -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()

View file

@ -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

View file

@ -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"

View file

@ -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