Add authentication to tplink integration for newer devices (#105143)
* Add authentication flows to tplink integration to enable newer device protocol support * Add websession passing to tplink integration discover methods * Use SmartDevice.connect() * Update to use DeviceConfig * Use credential hashes * Bump python-kasa to 0.6.0.dev0 * Fix tests and address review comments * Add autodetection for L530, P110, and L900 This adds mac address prefixes for the devices I have. The wildcards are left quite lax assuming different series may share the same prefix. * Bump tplink to 0.6.0.dev1 * Add config flow tests * Use short_mac if alias is None and try legacy connect on discovery timeout * Add config_flow tests * Add init tests * Migrate to aiohttp * add some more ouis * final * ip change fix * add fixmes * fix O(n) searching * fix O(n) searching * move code that cannot fail outside of try block * fix missing reauth_successful string * add doc strings, cleanups * error message by password * dry * adjust discovery timeout * integration discovery already formats mac * tweaks * cleanups * cleanups * Update post review and fix broken tests * Fix TODOs and FIXMEs in test_config_flow * Add pragma no cover * bump, apply suggestions * remove no cover * use iden check * Apply suggestions from code review * Fix branched test and update integration title * legacy typing * Update homeassistant/components/tplink/__init__.py * lint * Remove more unused consts * Update test docstrings * Add sdb9696 to tplink codeowners * Update docstring on test for invalid DeviceConfig * Update test stored credentials test --------- Co-authored-by: Teemu Rytilahti <tpr@iki.fi> Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
c3da51db4e
commit
9b3d3b3b2d
18 changed files with 1661 additions and 161 deletions
|
@ -1,25 +1,35 @@
|
|||
"""Tests for the TP-Link component."""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import setup
|
||||
from homeassistant.components import tplink
|
||||
from homeassistant.components.tplink.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
CONF_AUTHENTICATION,
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import (
|
||||
CREATE_ENTRY_DATA_AUTH,
|
||||
DEVICE_CONFIG_AUTH,
|
||||
IP_ADDRESS,
|
||||
MAC_ADDRESS,
|
||||
_mocked_dimmer,
|
||||
_patch_connect,
|
||||
_patch_discovery,
|
||||
_patch_single_discovery,
|
||||
)
|
||||
|
@ -57,7 +67,7 @@ async def test_config_entry_reload(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
with _patch_discovery(), _patch_single_discovery():
|
||||
with _patch_discovery(), _patch_single_discovery(), _patch_connect():
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
assert already_migrated_config_entry.state == ConfigEntryState.LOADED
|
||||
|
@ -72,7 +82,9 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None:
|
|||
domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
with _patch_discovery(no_device=True), _patch_single_discovery(no_device=True):
|
||||
with _patch_discovery(no_device=True), _patch_single_discovery(
|
||||
no_device=True
|
||||
), _patch_connect(no_device=True):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
@ -102,7 +114,9 @@ async def test_dimmer_switch_unique_id_fix_original_entity_still_exists(
|
|||
original_name="Rollout dimmer",
|
||||
)
|
||||
|
||||
with _patch_discovery(device=dimmer), _patch_single_discovery(device=dimmer):
|
||||
with _patch_discovery(device=dimmer), _patch_single_discovery(
|
||||
device=dimmer
|
||||
), _patch_connect(device=dimmer):
|
||||
await setup.async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -126,7 +140,7 @@ async def test_config_entry_wrong_mac_Address(
|
|||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=mismatched_mac
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
with _patch_discovery(), _patch_single_discovery():
|
||||
with _patch_discovery(), _patch_single_discovery(), _patch_connect():
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
@ -135,3 +149,110 @@ async def test_config_entry_wrong_mac_Address(
|
|||
"Unexpected device found at 127.0.0.1; expected aa:bb:cc:dd:ee:f0, found aa:bb:cc:dd:ee:ff"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_config_entry_device_config(
|
||||
hass: HomeAssistant,
|
||||
mock_discovery: AsyncMock,
|
||||
mock_connect: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that a config entry can be loaded with DeviceConfig."""
|
||||
mock_config_entry = MockConfigEntry(
|
||||
title="TPLink",
|
||||
domain=DOMAIN,
|
||||
data={**CREATE_ENTRY_DATA_AUTH},
|
||||
unique_id=MAC_ADDRESS,
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_config_entry_with_stored_credentials(
|
||||
hass: HomeAssistant,
|
||||
mock_discovery: AsyncMock,
|
||||
mock_connect: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that a config entry can be loaded when stored credentials are set."""
|
||||
stored_credentials = tplink.Credentials("fake_username1", "fake_password1")
|
||||
mock_config_entry = MockConfigEntry(
|
||||
title="TPLink",
|
||||
domain=DOMAIN,
|
||||
data={**CREATE_ENTRY_DATA_AUTH},
|
||||
unique_id=MAC_ADDRESS,
|
||||
)
|
||||
auth = {
|
||||
CONF_USERNAME: stored_credentials.username,
|
||||
CONF_PASSWORD: stored_credentials.password,
|
||||
}
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[CONF_AUTHENTICATION] = auth
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
config = DEVICE_CONFIG_AUTH
|
||||
assert config.credentials != stored_credentials
|
||||
config.credentials = stored_credentials
|
||||
mock_connect["connect"].assert_called_once_with(config=config)
|
||||
|
||||
|
||||
async def test_config_entry_device_config_invalid(
|
||||
hass: HomeAssistant,
|
||||
mock_discovery: AsyncMock,
|
||||
mock_connect: AsyncMock,
|
||||
caplog,
|
||||
) -> None:
|
||||
"""Test that an invalid device config logs an error and loads the config entry."""
|
||||
entry_data = copy.deepcopy(CREATE_ENTRY_DATA_AUTH)
|
||||
entry_data[CONF_DEVICE_CONFIG] = {"foo": "bar"}
|
||||
mock_config_entry = MockConfigEntry(
|
||||
title="TPLink",
|
||||
domain=DOMAIN,
|
||||
data={**entry_data},
|
||||
unique_id=MAC_ADDRESS,
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert (
|
||||
f"Invalid connection type dict for {IP_ADDRESS}: {entry_data.get(CONF_DEVICE_CONFIG)}"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("error_type", "entry_state", "reauth_flows"),
|
||||
[
|
||||
(tplink.AuthenticationException, ConfigEntryState.SETUP_ERROR, True),
|
||||
(tplink.SmartDeviceException, ConfigEntryState.SETUP_RETRY, False),
|
||||
],
|
||||
ids=["invalid-auth", "unknown-error"],
|
||||
)
|
||||
async def test_config_entry_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_discovery: AsyncMock,
|
||||
mock_connect: AsyncMock,
|
||||
error_type,
|
||||
entry_state,
|
||||
reauth_flows,
|
||||
) -> None:
|
||||
"""Test that device exceptions are handled correctly during init."""
|
||||
mock_connect["connect"].side_effect = error_type
|
||||
mock_config_entry = MockConfigEntry(
|
||||
title="TPLink",
|
||||
domain=DOMAIN,
|
||||
data={**CREATE_ENTRY_DATA_AUTH},
|
||||
unique_id=MAC_ADDRESS,
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is entry_state
|
||||
assert (
|
||||
any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
== reauth_flows
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue