mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Add airOS (insecure ssl) support for legacy v6 devices (#172954)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientSession, TCPConnector
|
||||
from airos.airos6 import AirOS6
|
||||
from airos.airos8 import AirOS8
|
||||
from airos.exceptions import (
|
||||
@@ -10,6 +11,7 @@ from airos.exceptions import (
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
AirOSKeyDataMissingError,
|
||||
AirOSTLSCompatibilityError,
|
||||
)
|
||||
from airos.helpers import DetectDeviceData, async_get_firmware_data
|
||||
|
||||
@@ -30,13 +32,20 @@ from homeassistant.exceptions import (
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADDITIONAL_SETTINGS
|
||||
from .const import (
|
||||
CONF_LEGACY_SSL,
|
||||
DEFAULT_SSL,
|
||||
DEFAULT_VERIFY_SSL,
|
||||
DOMAIN,
|
||||
SECTION_ADDITIONAL_SETTINGS,
|
||||
)
|
||||
from .coordinator import (
|
||||
AirOSConfigEntry,
|
||||
AirOSDataUpdateCoordinator,
|
||||
AirOSFirmwareUpdateCoordinator,
|
||||
AirOSRuntimeData,
|
||||
)
|
||||
from .helpers import build_legacy_context
|
||||
|
||||
_PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -51,41 +60,60 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||
"""Set up Ubiquiti airOS from a config entry."""
|
||||
owns_session = False
|
||||
verify_ssl = entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_VERIFY_SSL]
|
||||
|
||||
# By default airOS 8 comes with self-signed SSL certificates,
|
||||
# with no option in the web UI to change or upload a custom certificate.
|
||||
session = async_get_clientsession(
|
||||
hass, verify_ssl=entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_VERIFY_SSL]
|
||||
)
|
||||
session = async_get_clientsession(hass, verify_ssl=verify_ssl)
|
||||
|
||||
if entry.data.get(CONF_LEGACY_SSL, False):
|
||||
session = ClientSession(
|
||||
connector=TCPConnector(ssl=build_legacy_context(verify_ssl=verify_ssl))
|
||||
)
|
||||
owns_session = True
|
||||
|
||||
conn_data = {
|
||||
CONF_HOST: entry.data[CONF_HOST],
|
||||
CONF_USERNAME: entry.data[CONF_USERNAME],
|
||||
CONF_PASSWORD: entry.data[CONF_PASSWORD],
|
||||
"use_ssl": entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_SSL],
|
||||
"session": session,
|
||||
"use_ssl": entry.data[SECTION_ADDITIONAL_SETTINGS][CONF_SSL],
|
||||
}
|
||||
|
||||
async def close_session() -> None:
|
||||
"""Close legacy session before raising if needed."""
|
||||
if owns_session:
|
||||
await session.close()
|
||||
|
||||
# Determine firmware version before creating the device instance
|
||||
try:
|
||||
device_data: DetectDeviceData = await async_get_firmware_data(**conn_data)
|
||||
|
||||
except (
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
AirOSTLSCompatibilityError,
|
||||
TimeoutError,
|
||||
) as err:
|
||||
await close_session()
|
||||
raise ConfigEntryNotReady from err
|
||||
except (
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSDataMissingError,
|
||||
) as err:
|
||||
await close_session()
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except AirOSKeyDataMissingError as err:
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise ConfigEntryError("key_data_missing") from err
|
||||
await close_session()
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="key_data_missing"
|
||||
) from err
|
||||
except Exception as err:
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise ConfigEntryError("unknown") from err
|
||||
await close_session()
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="unknown"
|
||||
) from err
|
||||
|
||||
airos_class: type[AirOS8 | AirOS6] = (
|
||||
AirOS8 if device_data["fw_major"] == 8 else AirOS6
|
||||
@@ -96,16 +124,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
|
||||
data_coordinator = AirOSDataUpdateCoordinator(
|
||||
hass, entry, device_data, airos_device
|
||||
)
|
||||
await data_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
firmware_coordinator: AirOSFirmwareUpdateCoordinator | None = None
|
||||
if device_data["fw_major"] >= 8:
|
||||
firmware_coordinator = AirOSFirmwareUpdateCoordinator(hass, entry, airos_device)
|
||||
await firmware_coordinator.async_config_entry_first_refresh()
|
||||
try:
|
||||
await data_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
firmware_coordinator: AirOSFirmwareUpdateCoordinator | None = None
|
||||
if device_data["fw_major"] >= 8:
|
||||
firmware_coordinator = AirOSFirmwareUpdateCoordinator(
|
||||
hass, entry, airos_device
|
||||
)
|
||||
await firmware_coordinator.async_config_entry_first_refresh()
|
||||
except ConfigEntryNotReady, ConfigEntryAuthFailed:
|
||||
await close_session()
|
||||
raise
|
||||
except Exception as err:
|
||||
await close_session()
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="unknown"
|
||||
) from err
|
||||
|
||||
entry.runtime_data = AirOSRuntimeData(
|
||||
status=data_coordinator,
|
||||
firmware=firmware_coordinator,
|
||||
owns_session=owns_session,
|
||||
session=session,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
|
||||
@@ -182,4 +224,9 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> b
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||
unload_state = await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||
# Clean up legacy session if needed
|
||||
if unload_state and entry.runtime_data.owns_session:
|
||||
await entry.runtime_data.session.close()
|
||||
|
||||
return unload_state
|
||||
|
||||
@@ -5,6 +5,7 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any, override
|
||||
|
||||
from aiohttp import ClientSession, TCPConnector
|
||||
from airos.airos6 import AirOS6
|
||||
from airos.airos8 import AirOS8
|
||||
from airos.discovery import airos_discover_devices
|
||||
@@ -16,6 +17,7 @@ from airos.exceptions import (
|
||||
AirOSEndpointError,
|
||||
AirOSKeyDataMissingError,
|
||||
AirOSListenerError,
|
||||
AirOSTLSCompatibilityError,
|
||||
)
|
||||
from airos.helpers import DetectDeviceData, async_get_firmware_data
|
||||
import voluptuous as vol
|
||||
@@ -44,6 +46,7 @@ from homeassistant.helpers.selector import (
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
|
||||
from .const import (
|
||||
CONF_LEGACY_SSL,
|
||||
DEFAULT_SSL,
|
||||
DEFAULT_USERNAME,
|
||||
DEFAULT_VERIFY_SSL,
|
||||
@@ -54,6 +57,7 @@ from .const import (
|
||||
MAC_ADDRESS,
|
||||
SECTION_ADDITIONAL_SETTINGS,
|
||||
)
|
||||
from .helpers import build_legacy_context
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -128,15 +132,24 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
async def _validate_and_get_device_info(
|
||||
self, config_data: dict[str, Any]
|
||||
self,
|
||||
config_data: dict[str, Any],
|
||||
legacy: bool = False,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Validate user input with the device API."""
|
||||
# By default airOS 8 comes with self-signed SSL certificates,
|
||||
# with no option in the web UI to change or upload a custom certificate.
|
||||
session = async_get_clientsession(
|
||||
self.hass,
|
||||
verify_ssl=config_data[SECTION_ADDITIONAL_SETTINGS][CONF_VERIFY_SSL],
|
||||
)
|
||||
# Older airOS 6 devices may still lack proper levels
|
||||
|
||||
close_session = False
|
||||
verify_ssl = config_data[SECTION_ADDITIONAL_SETTINGS][CONF_VERIFY_SSL]
|
||||
|
||||
session = async_get_clientsession(self.hass, verify_ssl=verify_ssl)
|
||||
if legacy:
|
||||
session = ClientSession(
|
||||
connector=TCPConnector(ssl=build_legacy_context(verify_ssl=verify_ssl))
|
||||
)
|
||||
close_session = True
|
||||
|
||||
try:
|
||||
device_data: DetectDeviceData = await async_get_firmware_data(
|
||||
@@ -147,6 +160,17 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
use_ssl=config_data[SECTION_ADDITIONAL_SETTINGS][CONF_SSL],
|
||||
)
|
||||
|
||||
except AirOSTLSCompatibilityError:
|
||||
# If already in legacy, stop iteration
|
||||
if legacy:
|
||||
self.errors["base"] = "cannot_connect"
|
||||
else:
|
||||
retry_config = dict(config_data)
|
||||
retry_config[CONF_LEGACY_SSL] = True
|
||||
return await self._validate_and_get_device_info(
|
||||
config_data=retry_config, legacy=True
|
||||
)
|
||||
|
||||
except (
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
@@ -169,6 +193,10 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return {"title": device_data["hostname"], "data": config_data}
|
||||
|
||||
finally:
|
||||
if close_session:
|
||||
await session.close()
|
||||
|
||||
return None
|
||||
|
||||
async def async_step_reauth(
|
||||
|
||||
@@ -20,3 +20,5 @@ HOSTNAME = "hostname"
|
||||
IP_ADDRESS = "ip_address"
|
||||
MAC_ADDRESS = "mac_address"
|
||||
DEVICE_NAME = "airOS device"
|
||||
|
||||
CONF_LEGACY_SSL = "legacy_ssl"
|
||||
|
||||
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, TypeVar, override
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from airos.airos6 import AirOS6, AirOS6Data
|
||||
from airos.airos8 import AirOS8, AirOS8Data
|
||||
from airos.exceptions import (
|
||||
@@ -39,6 +40,8 @@ class AirOSRuntimeData:
|
||||
|
||||
status: AirOSDataUpdateCoordinator
|
||||
firmware: AirOSFirmwareUpdateCoordinator | None
|
||||
session: ClientSession
|
||||
owns_session: bool = False
|
||||
|
||||
|
||||
async def async_fetch_airos_data(
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Helpers for airOS."""
|
||||
|
||||
import ssl
|
||||
|
||||
|
||||
def build_legacy_context(*, verify_ssl: bool) -> ssl.SSLContext:
|
||||
"""Build an SSL context compatible with legacy airOS 6 devices."""
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
ctx.set_ciphers("DEFAULT:@SECLEVEL=0")
|
||||
ctx.minimum_version = ssl.TLSVersion.TLSv1
|
||||
|
||||
if not verify_ssl:
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
return ctx
|
||||
@@ -207,6 +207,9 @@
|
||||
"reboot_failed": {
|
||||
"message": "The device did not accept the reboot request. Try again, or check your device web interface for errors."
|
||||
},
|
||||
"unknown": {
|
||||
"message": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"update_connection_authentication_error": {
|
||||
"message": "Authentication or connection failed during firmware update"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Test the Ubiquiti airOS config flow."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from airos.exceptions import (
|
||||
AirOSConnectionAuthenticationError,
|
||||
@@ -10,12 +9,14 @@ from airos.exceptions import (
|
||||
AirOSEndpointError,
|
||||
AirOSKeyDataMissingError,
|
||||
AirOSListenerError,
|
||||
AirOSTLSCompatibilityError,
|
||||
)
|
||||
from airos.helpers import DetectDeviceData
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.airos.const import (
|
||||
CONF_LEGACY_SSL,
|
||||
DEFAULT_USERNAME,
|
||||
DOMAIN,
|
||||
HOSTNAME,
|
||||
@@ -84,7 +85,7 @@ MOCK_DISC_EXISTS = {
|
||||
|
||||
async def test_manual_flow_creates_entry(
|
||||
hass: HomeAssistant,
|
||||
ap_status_fixture: dict[str, Any],
|
||||
ap_status_fixture: AirOSData,
|
||||
mock_airos_client: AsyncMock,
|
||||
mock_async_get_firmware_data: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
@@ -159,7 +160,7 @@ async def test_form_duplicate_entry(
|
||||
async def test_form_exception_handling(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
ap_status_fixture: dict[str, Any],
|
||||
ap_status_fixture: AirOSData,
|
||||
mock_airos_client: AsyncMock,
|
||||
mock_async_get_firmware_data: AsyncMock,
|
||||
exception: Exception,
|
||||
@@ -876,3 +877,99 @@ async def test_dhcp_ip_unchanged(
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_manual_flow_retries_with_legacy_tls(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_async_get_firmware_data: AsyncMock,
|
||||
ap_status_fixture: AirOSData,
|
||||
) -> None:
|
||||
"""Test manual flow retries with legacy TLS and creates an entry."""
|
||||
legacy_session = MagicMock()
|
||||
legacy_session.close = AsyncMock()
|
||||
|
||||
mock_async_get_firmware_data.side_effect = [
|
||||
AirOSTLSCompatibilityError(),
|
||||
{
|
||||
"mac": ap_status_fixture.derived.mac,
|
||||
"hostname": ap_status_fixture.host.hostname,
|
||||
},
|
||||
]
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.airos.config_flow.TCPConnector",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.airos.config_flow.ClientSession",
|
||||
return_value=legacy_session,
|
||||
) as mock_client_session,
|
||||
patch(
|
||||
"homeassistant.components.airos.config_flow.build_legacy_context",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"next_step_id": "manual"}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], MOCK_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"][CONF_LEGACY_SSL] is True
|
||||
assert mock_async_get_firmware_data.await_count == 2
|
||||
mock_client_session.assert_called_once()
|
||||
legacy_session.close.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_validate_raise_on_attempted_legacy(
|
||||
hass: HomeAssistant,
|
||||
mock_async_get_firmware_data: AsyncMock,
|
||||
) -> None:
|
||||
"""Test legacy mode re-raises TLS compatibility errors."""
|
||||
legacy_session = MagicMock()
|
||||
legacy_session.close = AsyncMock()
|
||||
|
||||
mock_async_get_firmware_data.side_effect = AirOSTLSCompatibilityError()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.airos.config_flow.TCPConnector",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.airos.config_flow.ClientSession",
|
||||
return_value=legacy_session,
|
||||
) as mock_client_session,
|
||||
patch(
|
||||
"homeassistant.components.airos.config_flow.build_legacy_context",
|
||||
return_value=MagicMock(),
|
||||
) as mock_build_legacy_context,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"next_step_id": "manual"}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], MOCK_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "manual"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
assert mock_async_get_firmware_data.await_count == 2
|
||||
mock_client_session.assert_called_once()
|
||||
mock_build_legacy_context.assert_called_once_with(
|
||||
verify_ssl=MOCK_CONFIG[SECTION_ADDITIONAL_SETTINGS][CONF_VERIFY_SSL]
|
||||
)
|
||||
legacy_session.close.assert_awaited_once()
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Tests for airOS helpers."""
|
||||
|
||||
import ssl
|
||||
|
||||
from homeassistant.components.airos.helpers import build_legacy_context
|
||||
|
||||
|
||||
def test_build_legacy_context() -> None:
|
||||
"""Test building a legacy SSL context."""
|
||||
context = build_legacy_context(verify_ssl=False)
|
||||
|
||||
assert isinstance(context, ssl.SSLContext)
|
||||
assert context.minimum_version == ssl.TLSVersion.TLSv1
|
||||
assert context.check_hostname is False
|
||||
assert context.verify_mode == ssl.CERT_NONE
|
||||
|
||||
|
||||
def test_build_legacy_context_verify_ssl() -> None:
|
||||
"""Test building a legacy SSL context with verification enabled."""
|
||||
context = build_legacy_context(verify_ssl=True)
|
||||
|
||||
assert isinstance(context, ssl.SSLContext)
|
||||
assert context.minimum_version == ssl.TLSVersion.TLSv1
|
||||
assert context.check_hostname is True
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Test for airOS integration setup."""
|
||||
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, patch
|
||||
|
||||
from airos.exceptions import (
|
||||
AirOSConnectionAuthenticationError,
|
||||
@@ -11,6 +11,7 @@ from airos.exceptions import (
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.airos.const import (
|
||||
CONF_LEGACY_SSL,
|
||||
DEFAULT_SSL,
|
||||
DEFAULT_VERIFY_SSL,
|
||||
DOMAIN,
|
||||
@@ -22,6 +23,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_USER,
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
ConfigEntryState,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
@@ -293,3 +295,169 @@ async def test_fetch_airos_data_auth_error(mock_airos_client: MagicMock) -> None
|
||||
|
||||
with pytest.raises(ConfigEntryAuthFailed):
|
||||
await async_fetch_airos_data(mock_airos_client, mock_airos_client.status)
|
||||
|
||||
|
||||
async def test_setup_entry_with_legacy_ssl(
|
||||
hass: HomeAssistant,
|
||||
mock_airos_class: MagicMock,
|
||||
mock_airos_client: MagicMock,
|
||||
mock_async_get_firmware_data: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setting up a config entry with legacy SSL session ownership."""
|
||||
legacy_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="NanoStation",
|
||||
unique_id="01:23:45:67:89:AB",
|
||||
data={**MOCK_CONFIG_V1_2, CONF_LEGACY_SSL: True},
|
||||
)
|
||||
legacy_entry.add_to_hass(hass)
|
||||
|
||||
legacy_session = MagicMock()
|
||||
legacy_session.close = AsyncMock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.airos.ClientSession",
|
||||
return_value=legacy_session,
|
||||
) as mock_client_session,
|
||||
patch(
|
||||
"homeassistant.components.airos.TCPConnector",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.airos.build_legacy_context",
|
||||
return_value=MagicMock(),
|
||||
) as mock_build_legacy_context,
|
||||
):
|
||||
await hass.config_entries.async_setup(legacy_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert legacy_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_client_session.assert_called_once()
|
||||
mock_build_legacy_context.assert_called_once_with(verify_ssl=DEFAULT_VERIFY_SSL)
|
||||
|
||||
mock_airos_class.assert_called_once_with(
|
||||
host=MOCK_CONFIG_V1_2[CONF_HOST],
|
||||
username=MOCK_CONFIG_V1_2[CONF_USERNAME],
|
||||
password=MOCK_CONFIG_V1_2[CONF_PASSWORD],
|
||||
session=legacy_session,
|
||||
use_ssl=DEFAULT_SSL,
|
||||
)
|
||||
|
||||
assert await hass.config_entries.async_unload(legacy_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
legacy_session.close.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "state"),
|
||||
[
|
||||
(AirOSDeviceConnectionError, ConfigEntryState.SETUP_RETRY),
|
||||
],
|
||||
)
|
||||
async def test_setup_entry_with_legacy_ssl_fails_firmware_detect(
|
||||
hass: HomeAssistant,
|
||||
mock_airos_class: MagicMock,
|
||||
mock_airos_client: MagicMock,
|
||||
mock_async_get_firmware_data: AsyncMock,
|
||||
exception: Exception,
|
||||
state: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Test handling legacy SSL connection failure."""
|
||||
legacy_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="NanoStation",
|
||||
unique_id="01:23:45:67:89:AB",
|
||||
data={**MOCK_CONFIG_V1_2, CONF_LEGACY_SSL: True},
|
||||
)
|
||||
legacy_entry.add_to_hass(hass)
|
||||
|
||||
legacy_session = MagicMock()
|
||||
legacy_session.close = AsyncMock()
|
||||
|
||||
mock_async_get_firmware_data.side_effect = exception
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.airos.ClientSession",
|
||||
return_value=legacy_session,
|
||||
) as mock_client_session,
|
||||
patch(
|
||||
"homeassistant.components.airos.TCPConnector",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.airos.build_legacy_context",
|
||||
return_value=MagicMock(),
|
||||
) as mock_build_legacy_context,
|
||||
):
|
||||
result = await hass.config_entries.async_setup(legacy_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result is False
|
||||
assert legacy_entry.state is state
|
||||
mock_client_session.assert_called_once()
|
||||
mock_build_legacy_context.assert_called_once_with(verify_ssl=DEFAULT_VERIFY_SSL)
|
||||
legacy_session.close.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "state"),
|
||||
[
|
||||
(ConfigEntryNotReady, ConfigEntryState.SETUP_RETRY),
|
||||
(Exception, ConfigEntryState.SETUP_ERROR),
|
||||
],
|
||||
)
|
||||
async def test_setup_entry_with_legacy_ssl_fails_coordinator(
|
||||
hass: HomeAssistant,
|
||||
mock_airos_class: MagicMock,
|
||||
mock_airos_client: MagicMock,
|
||||
mock_async_get_firmware_data: AsyncMock,
|
||||
exception: Exception,
|
||||
state: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Test legacy session is closed when status coordinator first refresh fails."""
|
||||
legacy_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="NanoStation",
|
||||
unique_id="01:23:45:67:89:AB",
|
||||
data={**MOCK_CONFIG_V1_2, CONF_LEGACY_SSL: True},
|
||||
)
|
||||
legacy_entry.add_to_hass(hass)
|
||||
|
||||
legacy_session = MagicMock()
|
||||
legacy_session.close = AsyncMock()
|
||||
|
||||
mock_status_coordinator = MagicMock()
|
||||
mock_status_coordinator.async_config_entry_first_refresh = AsyncMock(
|
||||
side_effect=exception
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.airos.ClientSession",
|
||||
return_value=legacy_session,
|
||||
) as mock_client_session,
|
||||
patch(
|
||||
"homeassistant.components.airos.TCPConnector",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.airos.build_legacy_context",
|
||||
return_value=MagicMock(),
|
||||
) as mock_build_legacy_context,
|
||||
patch(
|
||||
"homeassistant.components.airos.AirOSDataUpdateCoordinator",
|
||||
return_value=mock_status_coordinator,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.async_setup(legacy_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result is False
|
||||
assert legacy_entry.state is state
|
||||
mock_client_session.assert_called_once()
|
||||
mock_build_legacy_context.assert_called_once_with(verify_ssl=DEFAULT_VERIFY_SSL)
|
||||
legacy_session.close.assert_awaited_once()
|
||||
|
||||
Reference in New Issue
Block a user