mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
Begin migrating unifiprotect to use the public API (#149126)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
9a6ba225e4
commit
51a46a128c
@ -16,9 +16,13 @@ from uiprotect.exceptions import ClientError, NotAuthorized
|
||||
from uiprotect.test_util.anonymize import anonymize_data # noqa: F401
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryError,
|
||||
ConfigEntryNotReady,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
@ -33,7 +37,6 @@ from .const import (
|
||||
DEVICES_THAT_ADOPT,
|
||||
DOMAIN,
|
||||
MIN_REQUIRED_PROTECT_V,
|
||||
OUTDATED_LOG_MESSAGE,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .data import ProtectData, UFPConfigEntry
|
||||
@ -69,6 +72,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool:
|
||||
"""Set up the UniFi Protect config entries."""
|
||||
|
||||
protect = async_create_api_client(hass, entry)
|
||||
_LOGGER.debug("Connect to UniFi Protect")
|
||||
|
||||
@ -89,6 +93,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool:
|
||||
bootstrap = protect.bootstrap
|
||||
nvr_info = bootstrap.nvr
|
||||
auth_user = bootstrap.users.get(bootstrap.auth_user_id)
|
||||
|
||||
# Check if API key is missing
|
||||
if not protect.is_api_key_set() and auth_user and nvr_info.can_write(auth_user):
|
||||
try:
|
||||
new_api_key = await protect.create_api_key(
|
||||
name=f"Home Assistant ({hass.config.location_name})"
|
||||
)
|
||||
except NotAuthorized as err:
|
||||
_LOGGER.error("Failed to create API key: %s", err)
|
||||
else:
|
||||
protect.set_api_key(new_api_key)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data={**entry.data, CONF_API_KEY: new_api_key}
|
||||
)
|
||||
|
||||
if not protect.is_api_key_set():
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_key_required",
|
||||
)
|
||||
|
||||
if auth_user and auth_user.cloud_account:
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
@ -103,12 +128,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
if nvr_info.version < MIN_REQUIRED_PROTECT_V:
|
||||
_LOGGER.error(
|
||||
OUTDATED_LOG_MESSAGE,
|
||||
nvr_info.version,
|
||||
MIN_REQUIRED_PROTECT_V,
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="protect_version",
|
||||
translation_placeholders={
|
||||
"current_version": str(nvr_info.version),
|
||||
"min_version": str(MIN_REQUIRED_PROTECT_V),
|
||||
},
|
||||
)
|
||||
return False
|
||||
|
||||
if entry.unique_id is None:
|
||||
hass.config_entries.async_update_entry(entry, unique_id=nvr_info.mac)
|
||||
|
@ -23,6 +23,7 @@ from homeassistant.config_entries import (
|
||||
OptionsFlowWithReload,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_ID,
|
||||
CONF_PASSWORD,
|
||||
@ -214,6 +215,7 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_USERNAME, default=user_input.get(CONF_USERNAME)
|
||||
): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
@ -247,6 +249,7 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
session = async_create_clientsession(
|
||||
self.hass, cookie_jar=CookieJar(unsafe=True)
|
||||
)
|
||||
public_api_session = async_get_clientsession(self.hass)
|
||||
|
||||
host = user_input[CONF_HOST]
|
||||
port = user_input.get(CONF_PORT, DEFAULT_PORT)
|
||||
@ -254,10 +257,12 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
protect = ProtectApiClient(
|
||||
session=session,
|
||||
public_api_session=public_api_session,
|
||||
host=host,
|
||||
port=port,
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
api_key=user_input[CONF_API_KEY],
|
||||
verify_ssl=verify_ssl,
|
||||
cache_dir=Path(self.hass.config.path(STORAGE_DIR, "unifiprotect")),
|
||||
config_dir=Path(self.hass.config.path(STORAGE_DIR, "unifiprotect")),
|
||||
@ -286,6 +291,14 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
auth_user = bootstrap.users.get(bootstrap.auth_user_id)
|
||||
if auth_user and auth_user.cloud_account:
|
||||
errors["base"] = "cloud_user"
|
||||
try:
|
||||
await protect.get_meta_info()
|
||||
except NotAuthorized as ex:
|
||||
_LOGGER.debug(ex)
|
||||
errors[CONF_API_KEY] = "invalid_auth"
|
||||
except ClientError as ex:
|
||||
_LOGGER.error(ex)
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
return nvr_data, errors
|
||||
|
||||
@ -318,12 +331,18 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
description_placeholders={
|
||||
"local_user_documentation_url": await async_local_user_documentation_url(
|
||||
self.hass
|
||||
),
|
||||
},
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_USERNAME, default=form_data.get(CONF_USERNAME)
|
||||
): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
@ -366,6 +385,7 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_USERNAME, default=user_input.get(CONF_USERNAME)
|
||||
): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
|
@ -52,7 +52,7 @@ DEVICES_THAT_ADOPT = {
|
||||
DEVICES_WITH_ENTITIES = DEVICES_THAT_ADOPT | {ModelType.NVR}
|
||||
DEVICES_FOR_SUBSCRIBE = DEVICES_WITH_ENTITIES | {ModelType.EVENT}
|
||||
|
||||
MIN_REQUIRED_PROTECT_V = Version("1.20.0")
|
||||
MIN_REQUIRED_PROTECT_V = Version("6.0.0")
|
||||
OUTDATED_LOG_MESSAGE = (
|
||||
"You are running v%s of UniFi Protect. Minimum required version is v%s. Please"
|
||||
" upgrade UniFi Protect and then retry"
|
||||
|
@ -10,19 +10,27 @@
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Hostname or IP address of your UniFi Protect device."
|
||||
"host": "Hostname or IP address of your UniFi Protect device.",
|
||||
"api_key": "API key for your local user account."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "UniFi Protect reauth",
|
||||
"description": "Your credentials or API key seem to be missing or invalid. For instructions on how to create a local user or generate a new API key, please refer to the documentation: {local_user_documentation_url}",
|
||||
"data": {
|
||||
"host": "IP/Host of UniFi Protect server",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "API key for your local user account.",
|
||||
"username": "Username for your local (not cloud) user account."
|
||||
}
|
||||
},
|
||||
"discovery_confirm": {
|
||||
@ -30,14 +38,18 @@
|
||||
"description": "Do you want to set up {name} ({ip_address})? You will need a local user created in your UniFi OS Console to log in with. Ubiquiti Cloud users will not work. For more information: {local_user_documentation_url}",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "API key for your local user account."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"protect_version": "Minimum required version is v1.20.0. Please upgrade UniFi Protect and then retry.",
|
||||
"protect_version": "Minimum required version is v6.0.0. Please upgrade UniFi Protect and then retry.",
|
||||
"cloud_user": "Ubiquiti Cloud users are not supported. Please use a local user instead."
|
||||
},
|
||||
"abort": {
|
||||
@ -669,5 +681,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"api_key_required": {
|
||||
"message": "API key is required. Please reauthenticate this integration to provide an API key."
|
||||
},
|
||||
"protect_version": {
|
||||
"message": "Your UniFi Protect version ({current_version}) is too old. Minimum required: {min_version}."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -110,13 +110,16 @@ def async_create_api_client(
|
||||
"""Create ProtectApiClient from config entry."""
|
||||
|
||||
session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True))
|
||||
public_api_session = async_create_clientsession(hass)
|
||||
return ProtectApiClient(
|
||||
host=entry.data[CONF_HOST],
|
||||
port=entry.data[CONF_PORT],
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
api_key=entry.data.get("api_key"),
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
session=session,
|
||||
public_api_session=public_api_session,
|
||||
subscribed_models=DEVICES_FOR_SUBSCRIBE,
|
||||
override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False),
|
||||
ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False),
|
||||
|
@ -32,6 +32,7 @@ from uiprotect.data import (
|
||||
from uiprotect.websocket import WebsocketState
|
||||
|
||||
from homeassistant.components.unifiprotect.const import DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@ -68,6 +69,7 @@ def mock_ufp_config_entry():
|
||||
"host": "1.1.1.1",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
CONF_API_KEY: "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": False,
|
||||
|
@ -5,7 +5,7 @@
|
||||
"canAutoUpdate": true,
|
||||
"isStatsGatheringEnabled": true,
|
||||
"timezone": "America/New_York",
|
||||
"version": "2.2.6",
|
||||
"version": "6.0.0",
|
||||
"ucoreVersion": "2.3.26",
|
||||
"firmwareVersion": "2.3.10",
|
||||
"uiVersion": null,
|
||||
|
@ -74,6 +74,10 @@ async def test_form(hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR) -> None
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
return_value=bootstrap,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_meta_info",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.async_setup_entry",
|
||||
return_value=True,
|
||||
@ -89,6 +93,7 @@ async def test_form(hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR) -> None
|
||||
"host": "1.1.1.1",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@ -99,6 +104,7 @@ async def test_form(hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR) -> None
|
||||
"host": "1.1.1.1",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": False,
|
||||
@ -116,9 +122,15 @@ async def test_form_version_too_old(
|
||||
)
|
||||
|
||||
bootstrap.nvr = old_nvr
|
||||
with patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
return_value=bootstrap,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
return_value=bootstrap,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_meta_info",
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@ -126,6 +138,7 @@ async def test_form_version_too_old(
|
||||
"host": "1.1.1.1",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
@ -133,15 +146,21 @@ async def test_form_version_too_old(
|
||||
assert result2["errors"] == {"base": "protect_version"}
|
||||
|
||||
|
||||
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
async def test_form_invalid_auth_password(hass: HomeAssistant) -> None:
|
||||
"""Test we handle invalid auth password."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
side_effect=NotAuthorized,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
side_effect=NotAuthorized,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_meta_info",
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@ -149,6 +168,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
"host": "1.1.1.1",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
@ -156,6 +176,38 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
assert result2["errors"] == {"password": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_invalid_auth_api_key(
|
||||
hass: HomeAssistant, bootstrap: Bootstrap
|
||||
) -> None:
|
||||
"""Test we handle invalid auth api key."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
return_value=bootstrap,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_meta_info",
|
||||
side_effect=NotAuthorized,
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"host": "1.1.1.1",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"api_key": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_cloud_user(
|
||||
hass: HomeAssistant, bootstrap: Bootstrap, cloud_account: CloudAccount
|
||||
) -> None:
|
||||
@ -167,9 +219,15 @@ async def test_form_cloud_user(
|
||||
user = bootstrap.users[bootstrap.auth_user_id]
|
||||
user.cloud_account = cloud_account
|
||||
bootstrap.users[bootstrap.auth_user_id] = user
|
||||
with patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
return_value=bootstrap,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
return_value=bootstrap,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_meta_info",
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@ -177,6 +235,7 @@ async def test_form_cloud_user(
|
||||
"host": "1.1.1.1",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
@ -190,9 +249,15 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
side_effect=NvrError,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
side_effect=NvrError,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_meta_info",
|
||||
side_effect=NvrError,
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@ -200,6 +265,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"host": "1.1.1.1",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
@ -217,6 +283,7 @@ async def test_form_reauth_auth(
|
||||
"host": "1.1.1.1",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": False,
|
||||
@ -234,15 +301,22 @@ async def test_form_reauth_auth(
|
||||
"name": "Mock Title",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
side_effect=NotAuthorized,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
side_effect=NotAuthorized,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_meta_info",
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
@ -260,12 +334,17 @@ async def test_form_reauth_auth(
|
||||
"homeassistant.components.unifiprotect.async_setup",
|
||||
return_value=True,
|
||||
) as mock_setup,
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_meta_info",
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
"username": "test-username",
|
||||
"password": "new-password",
|
||||
"api_key": "test-api-key",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@ -283,6 +362,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) -
|
||||
"host": "1.1.1.1",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": False,
|
||||
@ -383,6 +463,10 @@ async def test_discovered_by_unifi_discovery_direct_connect(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
return_value=bootstrap,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_meta_info",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.async_setup_entry",
|
||||
return_value=True,
|
||||
@ -397,6 +481,7 @@ async def test_discovered_by_unifi_discovery_direct_connect(
|
||||
{
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@ -407,6 +492,7 @@ async def test_discovered_by_unifi_discovery_direct_connect(
|
||||
"host": DIRECT_CONNECT_DOMAIN,
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": True,
|
||||
@ -425,6 +511,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated(
|
||||
"host": "y.ui.direct",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": True,
|
||||
@ -583,6 +670,10 @@ async def test_discovered_by_unifi_discovery(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
side_effect=[NotAuthorized, bootstrap],
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_meta_info",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.async_setup_entry",
|
||||
return_value=True,
|
||||
@ -597,6 +688,7 @@ async def test_discovered_by_unifi_discovery(
|
||||
{
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@ -607,6 +699,7 @@ async def test_discovered_by_unifi_discovery(
|
||||
"host": DEVICE_IP_ADDRESS,
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": False,
|
||||
@ -644,6 +737,10 @@ async def test_discovered_by_unifi_discovery_partial(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
return_value=bootstrap,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_meta_info",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.async_setup_entry",
|
||||
return_value=True,
|
||||
@ -658,6 +755,7 @@ async def test_discovered_by_unifi_discovery_partial(
|
||||
{
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@ -668,6 +766,7 @@ async def test_discovered_by_unifi_discovery_partial(
|
||||
"host": DEVICE_IP_ADDRESS,
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": False,
|
||||
@ -686,6 +785,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
|
||||
"host": DIRECT_CONNECT_DOMAIN,
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": True,
|
||||
@ -716,6 +816,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
|
||||
"host": "127.0.0.1",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": True,
|
||||
@ -746,6 +847,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
|
||||
"host": "y.ui.direct",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": True,
|
||||
@ -787,6 +889,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
|
||||
"host": "y.ui.direct",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": True,
|
||||
@ -827,6 +930,10 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
||||
return_value=bootstrap,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_meta_info",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.async_setup_entry",
|
||||
return_value=True,
|
||||
@ -841,6 +948,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
|
||||
{
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@ -851,6 +959,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
|
||||
"host": "nomatchsameip.ui.direct",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": True,
|
||||
@ -869,6 +978,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
|
||||
"host": "y.ui.direct",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": True,
|
||||
|
@ -18,6 +18,7 @@ from homeassistant.components.unifiprotect.data import (
|
||||
async_ufp_instance_for_config_entry_ids,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
@ -29,6 +30,19 @@ from tests.common import MockConfigEntry
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_user_can_write_nvr(request: pytest.FixtureRequest, ufp: MockUFPFixture):
|
||||
"""Fixture to mock can_write method on NVR objects with indirect parametrization."""
|
||||
can_write_result = getattr(request, "param", True)
|
||||
original_can_write = ufp.api.bootstrap.nvr.can_write
|
||||
mock_can_write = Mock(return_value=can_write_result)
|
||||
object.__setattr__(ufp.api.bootstrap.nvr, "can_write", mock_can_write)
|
||||
try:
|
||||
yield mock_can_write
|
||||
finally:
|
||||
object.__setattr__(ufp.api.bootstrap.nvr, "can_write", original_can_write)
|
||||
|
||||
|
||||
async def test_setup(hass: HomeAssistant, ufp: MockUFPFixture) -> None:
|
||||
"""Test working setup of unifiprotect entry."""
|
||||
|
||||
@ -68,6 +82,7 @@ async def test_setup_multiple(
|
||||
"host": "1.1.1.1",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
CONF_API_KEY: "test-api-key",
|
||||
"id": "UnifiProtect",
|
||||
"port": 443,
|
||||
"verify_ssl": False,
|
||||
@ -331,6 +346,112 @@ async def test_async_ufp_instance_for_config_entry_ids(
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_user_can_write_nvr", [True], indirect=True)
|
||||
async def test_setup_creates_api_key_when_missing(
|
||||
hass: HomeAssistant, ufp: MockUFPFixture, mock_user_can_write_nvr: Mock
|
||||
) -> None:
|
||||
"""Test that API key is created when missing and user has write permissions."""
|
||||
# Setup: API key is not set initially, user has write permissions
|
||||
ufp.api.is_api_key_set.return_value = False
|
||||
ufp.api.create_api_key = AsyncMock(return_value="new-api-key-123")
|
||||
|
||||
# Mock set_api_key to update is_api_key_set return value when called
|
||||
def set_api_key_side_effect(key):
|
||||
ufp.api.is_api_key_set.return_value = True
|
||||
|
||||
ufp.api.set_api_key.side_effect = set_api_key_side_effect
|
||||
|
||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify API key was created and set
|
||||
ufp.api.create_api_key.assert_called_once_with(name="Home Assistant (test home)")
|
||||
ufp.api.set_api_key.assert_called_once_with("new-api-key-123")
|
||||
|
||||
# Verify config entry was updated with new API key
|
||||
assert ufp.entry.data[CONF_API_KEY] == "new-api-key-123"
|
||||
assert ufp.entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_user_can_write_nvr", [False], indirect=True)
|
||||
async def test_setup_skips_api_key_creation_when_no_write_permission(
|
||||
hass: HomeAssistant, ufp: MockUFPFixture, mock_user_can_write_nvr: Mock
|
||||
) -> None:
|
||||
"""Test that API key creation is skipped when user has no write permissions."""
|
||||
# Setup: API key is not set, user has no write permissions
|
||||
ufp.api.is_api_key_set.return_value = False
|
||||
|
||||
# Should fail with auth error since no API key and can't create one
|
||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert ufp.entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
# Verify API key creation was not attempted
|
||||
ufp.api.create_api_key.assert_not_called()
|
||||
ufp.api.set_api_key.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_user_can_write_nvr", [True], indirect=True)
|
||||
async def test_setup_handles_api_key_creation_failure(
|
||||
hass: HomeAssistant, ufp: MockUFPFixture, mock_user_can_write_nvr: Mock
|
||||
) -> None:
|
||||
"""Test handling of API key creation failure."""
|
||||
# Setup: API key is not set, user has write permissions, but creation fails
|
||||
ufp.api.is_api_key_set.return_value = False
|
||||
ufp.api.create_api_key = AsyncMock(
|
||||
side_effect=NotAuthorized("Failed to create API key")
|
||||
)
|
||||
|
||||
# Should fail with auth error due to API key creation failure
|
||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert ufp.entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
# Verify API key creation was attempted but set_api_key was not called
|
||||
ufp.api.create_api_key.assert_called_once_with(name="Home Assistant (test home)")
|
||||
ufp.api.set_api_key.assert_not_called()
|
||||
|
||||
|
||||
async def test_setup_with_existing_api_key(
|
||||
hass: HomeAssistant, ufp: MockUFPFixture
|
||||
) -> None:
|
||||
"""Test setup when API key is already set."""
|
||||
# Setup: API key is already set
|
||||
ufp.api.is_api_key_set.return_value = True
|
||||
|
||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert ufp.entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Verify API key creation was not attempted
|
||||
ufp.api.create_api_key.assert_not_called()
|
||||
ufp.api.set_api_key.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_user_can_write_nvr", [True], indirect=True)
|
||||
async def test_setup_api_key_creation_returns_none(
|
||||
hass: HomeAssistant, ufp: MockUFPFixture, mock_user_can_write_nvr: Mock
|
||||
) -> None:
|
||||
"""Test handling when API key creation returns None."""
|
||||
# Setup: API key is not set, creation returns None (empty response)
|
||||
# set_api_key will be called with None but is_api_key_set will still be False
|
||||
ufp.api.is_api_key_set.return_value = False
|
||||
ufp.api.create_api_key = AsyncMock(return_value=None)
|
||||
|
||||
# Should fail with auth error since API key creation returned None
|
||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert ufp.entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
# Verify API key creation was attempted and set_api_key was called with None
|
||||
ufp.api.create_api_key.assert_called_once_with(name="Home Assistant (test home)")
|
||||
ufp.api.set_api_key.assert_called_once_with(None)
|
||||
|
||||
|
||||
async def test_migrate_entry_version_2(hass: HomeAssistant) -> None:
|
||||
"""Test remove CONF_ALLOW_EA from options while migrating a 1 config entry to 2."""
|
||||
with (
|
||||
@ -350,3 +471,47 @@ async def test_migrate_entry_version_2(hass: HomeAssistant) -> None:
|
||||
assert entry.version == 2
|
||||
assert entry.options.get(CONF_ALLOW_EA) is None
|
||||
assert entry.unique_id == "123456"
|
||||
|
||||
|
||||
async def test_setup_skips_api_key_creation_when_no_auth_user(
|
||||
hass: HomeAssistant, ufp: MockUFPFixture
|
||||
) -> None:
|
||||
"""Test that API key creation is skipped when auth_user is None."""
|
||||
# Setup: API key is not set, auth_user is None
|
||||
ufp.api.is_api_key_set.return_value = False
|
||||
|
||||
# Mock the users dictionary to return None for any user ID
|
||||
with patch.dict(ufp.api.bootstrap.users, {}, clear=True):
|
||||
# Should fail with auth error since no API key and no auth user to create one
|
||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert ufp.entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
# Verify API key creation was not attempted
|
||||
ufp.api.create_api_key.assert_not_called()
|
||||
ufp.api.set_api_key.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_user_can_write_nvr", [True], indirect=True)
|
||||
async def test_setup_fails_when_api_key_still_missing_after_creation(
|
||||
hass: HomeAssistant, ufp: MockUFPFixture, mock_user_can_write_nvr: Mock
|
||||
) -> None:
|
||||
"""Test that setup fails when API key is still missing after creation attempts."""
|
||||
# Setup: API key is not set and remains not set even after attempts
|
||||
ufp.api.is_api_key_set.return_value = False # type: ignore[attr-defined]
|
||||
ufp.api.create_api_key = AsyncMock(return_value="new-api-key-123") # type: ignore[method-assign]
|
||||
ufp.api.set_api_key = Mock() # type: ignore[method-assign] # Mock this but API key still won't be "set"
|
||||
|
||||
# Setup should fail since API key is still not set after creation
|
||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify entry is in setup error state (which will trigger reauth automatically)
|
||||
assert ufp.entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
# Verify API key creation was attempted
|
||||
ufp.api.create_api_key.assert_called_once_with( # type: ignore[attr-defined]
|
||||
name="Home Assistant (test home)"
|
||||
)
|
||||
ufp.api.set_api_key.assert_called_once_with("new-api-key-123") # type: ignore[attr-defined]
|
||||
|
@ -234,6 +234,7 @@ async def test_browse_media_root_multiple_consoles(
|
||||
"host": "1.1.1.2",
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"api_key": "test-api-key",
|
||||
"id": "UnifiProtect2",
|
||||
"port": 443,
|
||||
"verify_ssl": False,
|
||||
|
Loading…
x
Reference in New Issue
Block a user