Set unique_id in myuplink config entry (#132672)

pull/132726/head
Åke Strandberg 2024-12-09 15:47:40 +01:00 committed by GitHub
parent 74eddce3d3
commit 8d72443fd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 105 additions and 6 deletions

View File

@ -3,8 +3,10 @@
from __future__ import annotations
from http import HTTPStatus
import logging
from aiohttp import ClientError, ClientResponseError
import jwt
from myuplink import MyUplinkAPI, get_manufacturer, get_model, get_system_name
from homeassistant.config_entries import ConfigEntry
@ -22,6 +24,8 @@ from .api import AsyncConfigEntryAuth
from .const import DOMAIN, OAUTH2_SCOPES
from .coordinator import MyUplinkDataCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.NUMBER,
@ -109,3 +113,27 @@ async def async_remove_config_entry_device(
return not device_entry.identifiers.intersection(
(DOMAIN, device_id) for device_id in myuplink_data.data.devices
)
async def async_migrate_entry(
hass: HomeAssistant, config_entry: MyUplinkConfigEntry
) -> bool:
"""Migrate old entry."""
# Use sub(ject) from access_token as unique_id
if config_entry.version == 1 and config_entry.minor_version == 1:
token = jwt.decode(
config_entry.data["token"]["access_token"],
options={"verify_signature": False},
)
uid = token["sub"]
hass.config_entries.async_update_entry(
config_entry, unique_id=uid, minor_version=2
)
_LOGGER.info(
"Migration to version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True

View File

@ -4,6 +4,8 @@ from collections.abc import Mapping
import logging
from typing import Any
import jwt
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.helpers import config_entry_oauth2_flow
@ -15,6 +17,8 @@ class OAuth2FlowHandler(
):
"""Config flow to handle myUplink OAuth2 authentication."""
VERSION = 1
MINOR_VERSION = 2
DOMAIN = DOMAIN
@property
@ -46,8 +50,17 @@ class OAuth2FlowHandler(
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
"""Create or update the config entry."""
token = jwt.decode(
data["token"]["access_token"], options={"verify_signature": False}
)
uid = token["sub"]
await self.async_set_unique_id(uid)
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch(reason="account_mismatch")
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=data
)
self._abort_if_unique_id_configured()
return await super().async_oauth_create_entry(data)

View File

@ -23,6 +23,7 @@
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"account_mismatch": "The used account does not match the original account",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]"
},
"create_entry": {

View File

@ -15,10 +15,11 @@ from homeassistant.components.application_credentials import (
)
from homeassistant.components.myuplink.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.setup import async_setup_component
from homeassistant.util.json import json_loads
from .const import CLIENT_ID, CLIENT_SECRET
from .const import CLIENT_ID, CLIENT_SECRET, UNIQUE_ID
from tests.common import MockConfigEntry, load_fixture
@ -33,7 +34,7 @@ def mock_expires_at() -> float:
def mock_config_entry(hass: HomeAssistant, expires_at: float) -> MockConfigEntry:
"""Return the default mocked config entry."""
config_entry = MockConfigEntry(
version=1,
minor_version=2,
domain=DOMAIN,
title="myUplink test",
data={
@ -48,6 +49,7 @@ def mock_config_entry(hass: HomeAssistant, expires_at: float) -> MockConfigEntry
},
},
entry_id="myuplink_test",
unique_id=UNIQUE_ID,
)
config_entry.add_to_hass(hass)
return config_entry
@ -189,3 +191,21 @@ async def setup_platform(
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
yield
@pytest.fixture
async def access_token(hass: HomeAssistant) -> str:
"""Return a valid access token."""
return config_entry_oauth2_flow._encode_jwt(
hass,
{
"sub": UNIQUE_ID,
"aud": [],
"scp": [
"WRITESYSTEM",
"READSYSTEM",
"offline_access",
],
"ou_code": "NA",
},
)

View File

@ -2,3 +2,4 @@
CLIENT_ID = "12345"
CLIENT_SECRET = "67890"
UNIQUE_ID = "uid"

View File

@ -29,6 +29,7 @@ async def test_full_flow(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
access_token: str,
setup_credentials,
) -> None:
"""Check full flow."""
@ -59,7 +60,7 @@ async def test_full_flow(
OAUTH2_TOKEN,
json={
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
"access_token": access_token,
"type": "Bearer",
"expires_in": 60,
},
@ -81,6 +82,7 @@ async def test_flow_reauth(
aioclient_mock: AiohttpClientMocker,
setup_credentials: None,
mock_config_entry: MockConfigEntry,
access_token: str,
expires_at: float,
) -> None:
"""Test reauth step."""
@ -89,7 +91,7 @@ async def test_flow_reauth(
OLD_SCOPE_TOKEN = {
"auth_implementation": DOMAIN,
"token": {
"access_token": "Fake_token",
"access_token": access_token,
"scope": OLD_SCOPE,
"expires_in": 86399,
"refresh_token": "3012bc9f-7a65-4240-b817-9154ffdcc30f",
@ -137,7 +139,7 @@ async def test_flow_reauth(
OAUTH2_TOKEN,
json={
"refresh_token": "updated-refresh-token",
"access_token": "updated-access-token",
"access_token": access_token,
"type": "Bearer",
"expires_in": "60",
"scope": CURRENT_SCOPE,

View File

@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from .const import UNIQUE_ID
from tests.common import MockConfigEntry, load_fixture
from tests.test_util.aiohttp import AiohttpClientMocker
@ -92,7 +93,40 @@ async def test_devices_multiple_created_count(
mock_myuplink_client: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that multiple device are created."""
"""Test that multiple devices are created."""
await setup_integration(hass, mock_config_entry)
assert len(device_registry.devices) == 2
async def test_migrate_config_entry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_myuplink_client: MagicMock,
expires_at: float,
access_token: str,
) -> None:
"""Test migration of config entry."""
mock_entry_v1_1 = MockConfigEntry(
version=1,
minor_version=1,
domain=DOMAIN,
title="myUplink test",
data={
"auth_implementation": DOMAIN,
"token": {
"access_token": access_token,
"scope": "WRITESYSTEM READSYSTEM offline_access",
"expires_in": 86399,
"refresh_token": "3012bc9f-7a65-4240-b817-9154ffdcc30f",
"token_type": "Bearer",
"expires_at": expires_at,
},
},
entry_id="myuplink_test",
)
await setup_integration(hass, mock_entry_v1_1)
assert mock_entry_v1_1.version == 1
assert mock_entry_v1_1.minor_version == 2
assert mock_entry_v1_1.unique_id == UNIQUE_ID