core/tests/components/unifi/test_hub.py

481 lines
16 KiB
Python
Raw Normal View History

"""Test UniFi Network."""
from copy import deepcopy
from datetime import timedelta
from http import HTTPStatus
from unittest.mock import Mock, patch
import aiounifi
import pytest
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.unifi.const import (
2019-07-31 19:25:30 +00:00
CONF_SITE_ID,
CONF_TRACK_CLIENTS,
CONF_TRACK_DEVICES,
DEFAULT_ALLOW_BANDWIDTH_SENSORS,
Add UniFi Uptime sensor (#40058) * Added UniFi Uptime sensor Added the UniFi uptime data as a sensor. Untested. * Update sensor.py Updated code as a result of the tests. * Changed timestamp format and device class Converted state to iso timestamp and changed device class to DEVICE_CLASS_TIMESTAMP. * Updated unit of measurement to None * Added import * Update homeassistant/components/unifi/sensor.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Removed whitespace * Added the uptime sensors option to the config flow * All the unit tests should be there now * Whoops * Fixed translation * Properly formatted the code * Flake8 really has angel eyes * Black should also be satisfied now * Should have satisfied all static code analysis tools * Fixed add uptime sensor function * Fixed overintendation * Fixed unit tests * Made a spelling mistake during editing of unit tests * Test verifies if utc time is correct * Converted to iso format * Converted unit test to iso format * Unit test sensor json had the wrong uptime name * Added options_updated handler * Fixed remove sensors unit test * Update homeassistant/components/unifi/sensor.py Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com> * Update homeassistant/components/unifi/sensor.py Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com> * Update test_device_tracker.py Removed uptime from the devices * Fixed black formatting issue * I think the code coverage should be good now * Trying to add the sensors again * Using signals to hopefully trigger the controller to add them again * Forgot import * Sorted components * fixed isort comments * Removed CLASS and DEVICE_CLASS * Added TYPE again * Removed double underscores Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>
2020-09-18 17:33:37 +00:00
DEFAULT_ALLOW_UPTIME_SENSORS,
DEFAULT_DETECTION_TIME,
DEFAULT_TRACK_CLIENTS,
DEFAULT_TRACK_DEVICES,
DEFAULT_TRACK_WIRED_CLIENTS,
DOMAIN as UNIFI_DOMAIN,
PLATFORMS,
UNIFI_WIRELESS_CLIENTS,
)
from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect
from homeassistant.components.unifi.hub import get_unifi_api
from homeassistant.const import (
2019-07-31 19:25:30 +00:00
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
CONF_VERIFY_SSL,
2021-02-05 15:31:47 +00:00
CONTENT_TYPE_JSON,
2019-07-31 19:25:30 +00:00
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
2021-11-18 23:56:22 +00:00
DEFAULT_CONFIG_ENTRY_ID = "1"
2021-02-05 15:31:47 +00:00
DEFAULT_HOST = "1.2.3.4"
DEFAULT_SITE = "site_id"
CONTROLLER_HOST = {
"hostname": "controller_host",
2021-02-05 15:31:47 +00:00
"ip": DEFAULT_HOST,
"is_wired": True,
"last_seen": 1562600145,
"mac": "10:00:00:00:00:01",
"name": "Controller host",
"oui": "Producer",
"sw_mac": "00:00:00:00:01:01",
"sw_port": 1,
"wired-rx_bytes": 1234000000,
"wired-tx_bytes": 5678000000,
Add UniFi Uptime sensor (#40058) * Added UniFi Uptime sensor Added the UniFi uptime data as a sensor. Untested. * Update sensor.py Updated code as a result of the tests. * Changed timestamp format and device class Converted state to iso timestamp and changed device class to DEVICE_CLASS_TIMESTAMP. * Updated unit of measurement to None * Added import * Update homeassistant/components/unifi/sensor.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Removed whitespace * Added the uptime sensors option to the config flow * All the unit tests should be there now * Whoops * Fixed translation * Properly formatted the code * Flake8 really has angel eyes * Black should also be satisfied now * Should have satisfied all static code analysis tools * Fixed add uptime sensor function * Fixed overintendation * Fixed unit tests * Made a spelling mistake during editing of unit tests * Test verifies if utc time is correct * Converted to iso format * Converted unit test to iso format * Unit test sensor json had the wrong uptime name * Added options_updated handler * Fixed remove sensors unit test * Update homeassistant/components/unifi/sensor.py Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com> * Update homeassistant/components/unifi/sensor.py Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com> * Update test_device_tracker.py Removed uptime from the devices * Fixed black formatting issue * I think the code coverage should be good now * Trying to add the sensors again * Using signals to hopefully trigger the controller to add them again * Forgot import * Sorted components * fixed isort comments * Removed CLASS and DEVICE_CLASS * Added TYPE again * Removed double underscores Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>
2020-09-18 17:33:37 +00:00
"uptime": 1562600160,
}
ENTRY_CONFIG = {
2021-02-05 15:31:47 +00:00
CONF_HOST: DEFAULT_HOST,
2019-07-31 19:25:30 +00:00
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
CONF_PORT: 1234,
2021-02-05 15:31:47 +00:00
CONF_SITE_ID: DEFAULT_SITE,
CONF_VERIFY_SSL: False,
}
ENTRY_OPTIONS = {}
CONFIGURATION = []
SITE = [{"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}]
SYSTEM_INFORMATION = [
{
"anonymous_controller_id": "24f81231-a456-4c32-abcd-f5612345385f",
"build": "atag_7.4.162_21057",
"console_display_version": "3.1.15",
"hostname": "UDMP",
"name": "UDMP",
"previous_version": "7.4.156",
"timezone": "Europe/Stockholm",
"ubnt_device_type": "UDMPRO",
"udm_version": "3.0.20.9281",
"update_available": False,
"update_downloaded": False,
"uptime": 1196290,
"version": "7.4.162",
}
]
2021-02-05 15:31:47 +00:00
def mock_default_unifi_requests(
aioclient_mock,
host,
site_id,
sites=None,
clients_response=None,
clients_all_response=None,
devices_response=None,
dpiapp_response=None,
dpigroup_response=None,
port_forward_response=None,
system_information_response=None,
2021-02-05 15:31:47 +00:00
wlans_response=None,
):
"""Mock default UniFi requests responses."""
aioclient_mock.get(f"https://{host}:1234", status=302) # Check UniFi OS
aioclient_mock.post(
f"https://{host}:1234/api/login",
json={"data": "login successful", "meta": {"rc": "ok"}},
headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"https://{host}:1234/api/self/sites",
json={"data": sites or [], "meta": {"rc": "ok"}},
headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"https://{host}:1234/api/s/{site_id}/stat/sta",
json={"data": clients_response or [], "meta": {"rc": "ok"}},
headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"https://{host}:1234/api/s/{site_id}/rest/user",
json={"data": clients_all_response or [], "meta": {"rc": "ok"}},
headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"https://{host}:1234/api/s/{site_id}/stat/device",
json={"data": devices_response or [], "meta": {"rc": "ok"}},
headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"https://{host}:1234/api/s/{site_id}/rest/dpiapp",
json={"data": dpiapp_response or [], "meta": {"rc": "ok"}},
headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"https://{host}:1234/api/s/{site_id}/rest/dpigroup",
json={"data": dpigroup_response or [], "meta": {"rc": "ok"}},
headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"https://{host}:1234/api/s/{site_id}/rest/portforward",
json={"data": port_forward_response or [], "meta": {"rc": "ok"}},
headers={"content-type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
f"https://{host}:1234/api/s/{site_id}/stat/sysinfo",
json={"data": system_information_response or [], "meta": {"rc": "ok"}},
headers={"content-type": CONTENT_TYPE_JSON},
)
2021-02-05 15:31:47 +00:00
aioclient_mock.get(
f"https://{host}:1234/api/s/{site_id}/rest/wlanconf",
json={"data": wlans_response or [], "meta": {"rc": "ok"}},
headers={"content-type": CONTENT_TYPE_JSON},
)
2023-11-22 06:02:13 +00:00
aioclient_mock.get(
f"https://{host}:1234/v2/api/site/{site_id}/trafficroutes",
json=[{}],
headers={"content-type": CONTENT_TYPE_JSON},
)
2023-10-25 01:27:42 +00:00
aioclient_mock.get(
f"https://{host}:1234/v2/api/site/{site_id}/trafficrules",
json=[{}],
headers={"content-type": CONTENT_TYPE_JSON},
)
2021-02-05 15:31:47 +00:00
async def setup_unifi_integration(
hass,
2021-02-05 15:31:47 +00:00
aioclient_mock=None,
*,
config=ENTRY_CONFIG,
options=ENTRY_OPTIONS,
2021-02-05 15:31:47 +00:00
sites=SITE,
clients_response=None,
clients_all_response=None,
2021-02-05 15:31:47 +00:00
devices_response=None,
dpiapp_response=None,
2021-02-05 15:31:47 +00:00
dpigroup_response=None,
port_forward_response=None,
system_information_response=None,
2021-02-05 15:31:47 +00:00
wlans_response=None,
2019-12-30 18:40:52 +00:00
known_wireless_clients=None,
unique_id="1",
config_entry_id=DEFAULT_CONFIG_ENTRY_ID,
):
"""Create the UniFi Network instance."""
assert await async_setup_component(hass, UNIFI_DOMAIN, {})
config_entry = MockConfigEntry(
domain=UNIFI_DOMAIN,
data=deepcopy(config),
options=deepcopy(options),
unique_id=unique_id,
entry_id=config_entry_id,
version=1,
2019-07-31 19:25:30 +00:00
)
config_entry.add_to_hass(hass)
2019-12-30 18:40:52 +00:00
if known_wireless_clients:
hass.data[UNIFI_WIRELESS_CLIENTS].wireless_clients.update(
known_wireless_clients
2019-12-30 18:40:52 +00:00
)
2021-02-05 15:31:47 +00:00
if aioclient_mock:
mock_default_unifi_requests(
aioclient_mock,
host=config_entry.data[CONF_HOST],
site_id=config_entry.data[CONF_SITE_ID],
2021-02-05 15:31:47 +00:00
sites=sites,
clients_response=clients_response,
clients_all_response=clients_all_response,
devices_response=devices_response,
dpiapp_response=dpiapp_response,
dpigroup_response=dpigroup_response,
port_forward_response=port_forward_response,
system_information_response=system_information_response,
2021-02-05 15:31:47 +00:00
wlans_response=wlans_response,
)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
if config_entry.entry_id not in hass.data[UNIFI_DOMAIN]:
return None
2021-02-05 15:31:47 +00:00
return config_entry
async def test_hub_setup(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Successful setup."""
with patch(
"homeassistant.config_entries.ConfigEntries.async_forward_entry_setup",
return_value=True,
) as forward_entry_setup:
config_entry = await setup_unifi_integration(
hass, aioclient_mock, system_information_response=SYSTEM_INFORMATION
)
hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
entry = hub.config_entry
assert len(forward_entry_setup.mock_calls) == len(PLATFORMS)
assert forward_entry_setup.mock_calls[0][1] == (entry, BUTTON_DOMAIN)
assert forward_entry_setup.mock_calls[1][1] == (entry, TRACKER_DOMAIN)
assert forward_entry_setup.mock_calls[2][1] == (entry, IMAGE_DOMAIN)
assert forward_entry_setup.mock_calls[3][1] == (entry, SENSOR_DOMAIN)
assert forward_entry_setup.mock_calls[4][1] == (entry, SWITCH_DOMAIN)
assert hub.host == ENTRY_CONFIG[CONF_HOST]
assert hub.is_admin == (SITE[0]["role"] == "admin")
assert hub.option_allow_bandwidth_sensors == DEFAULT_ALLOW_BANDWIDTH_SENSORS
assert hub.option_allow_uptime_sensors == DEFAULT_ALLOW_UPTIME_SENSORS
assert isinstance(hub.option_block_clients, list)
assert hub.option_track_clients == DEFAULT_TRACK_CLIENTS
assert hub.option_track_devices == DEFAULT_TRACK_DEVICES
assert hub.option_track_wired_clients == DEFAULT_TRACK_WIRED_CLIENTS
assert hub.option_detection_time == timedelta(seconds=DEFAULT_DETECTION_TIME)
assert isinstance(hub.option_ssid_filter, set)
assert hub.signal_reachable == "unifi-reachable-1"
assert hub.signal_options_update == "unifi-options-1"
assert hub.signal_heartbeat_missed == "unifi-heartbeat-missed"
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(UNIFI_DOMAIN, config_entry.unique_id)},
)
assert device_entry.sw_version == "7.4.162"
async def test_hub_not_accessible(hass: HomeAssistant) -> None:
"""Retry to login gets scheduled when connection fails."""
with patch(
"homeassistant.components.unifi.hub.get_unifi_api",
side_effect=CannotConnect,
):
await setup_unifi_integration(hass)
assert hass.data[UNIFI_DOMAIN] == {}
async def test_hub_trigger_reauth_flow(hass: HomeAssistant) -> None:
"""Failed authentication trigger a reauthentication flow."""
with patch(
"homeassistant.components.unifi.get_unifi_api",
side_effect=AuthenticationRequired,
), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init:
await setup_unifi_integration(hass)
mock_flow_init.assert_called_once()
assert hass.data[UNIFI_DOMAIN] == {}
async def test_hub_unknown_error(hass: HomeAssistant) -> None:
"""Unknown errors are handled."""
with patch(
"homeassistant.components.unifi.hub.get_unifi_api",
side_effect=Exception,
):
await setup_unifi_integration(hass)
assert hass.data[UNIFI_DOMAIN] == {}
async def test_config_entry_updated(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Calling reset when the entry has been setup."""
config_entry = await setup_unifi_integration(hass, aioclient_mock)
hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
event_call = Mock()
unsub = async_dispatcher_connect(hass, hub.signal_options_update, event_call)
hass.config_entries.async_update_entry(
config_entry, options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}
)
await hass.async_block_till_done()
assert config_entry.options[CONF_TRACK_CLIENTS] is False
assert config_entry.options[CONF_TRACK_DEVICES] is False
event_call.assert_called_once()
unsub()
async def test_reset_after_successful_setup(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Calling reset when the entry has been setup."""
2021-02-05 15:31:47 +00:00
config_entry = await setup_unifi_integration(hass, aioclient_mock)
hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
result = await hub.async_reset()
await hass.async_block_till_done()
assert result is True
async def test_reset_fails(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Calling reset when the entry has been setup can return false."""
config_entry = await setup_unifi_integration(hass, aioclient_mock)
hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
with patch(
"homeassistant.config_entries.ConfigEntries.async_forward_entry_unload",
return_value=False,
):
result = await hub.async_reset()
await hass.async_block_till_done()
assert result is False
async def test_connection_state_signalling(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
mock_device_registry,
websocket_mock,
) -> None:
"""Verify connection statesignalling and connection state are working."""
client = {
"hostname": "client",
"ip": "10.0.0.1",
"is_wired": True,
"last_seen": dt_util.as_timestamp(dt_util.utcnow()),
"mac": "00:00:00:00:00:01",
}
await setup_unifi_integration(hass, aioclient_mock, clients_response=[client])
# Controller is connected
assert hass.states.get("device_tracker.client").state == "home"
await websocket_mock.disconnect()
# Controller is disconnected
assert hass.states.get("device_tracker.client").state == "unavailable"
await websocket_mock.reconnect()
# Controller is once again connected
assert hass.states.get("device_tracker.client").state == "home"
async def test_reconnect_mechanism(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, websocket_mock
) -> None:
"""Verify reconnect prints only on first reconnection try."""
await setup_unifi_integration(hass, aioclient_mock)
aioclient_mock.clear_requests()
2023-08-26 23:27:45 +00:00
aioclient_mock.get(f"https://{DEFAULT_HOST}:1234/", status=HTTPStatus.BAD_GATEWAY)
await websocket_mock.disconnect()
assert aioclient_mock.call_count == 0
await websocket_mock.reconnect(fail=True)
assert aioclient_mock.call_count == 1
await websocket_mock.reconnect(fail=True)
assert aioclient_mock.call_count == 2
@pytest.mark.parametrize(
"exception",
[
TimeoutError,
aiounifi.BadGateway,
aiounifi.ServiceUnavailable,
aiounifi.AiounifiException,
],
)
async def test_reconnect_mechanism_exceptions(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, websocket_mock, exception
) -> None:
"""Verify async_reconnect calls expected methods."""
await setup_unifi_integration(hass, aioclient_mock)
with patch("aiounifi.Controller.login", side_effect=exception), patch(
"homeassistant.components.unifi.hub.UnifiHub.reconnect"
) as mock_reconnect:
await websocket_mock.disconnect()
await websocket_mock.reconnect()
mock_reconnect.assert_called_once()
async def test_get_unifi_api(hass: HomeAssistant) -> None:
"""Successful call."""
2023-08-26 23:27:45 +00:00
with patch("aiounifi.Controller.login", return_value=True):
assert await get_unifi_api(hass, ENTRY_CONFIG)
async def test_get_unifi_api_verify_ssl_false(hass: HomeAssistant) -> None:
"""Successful call with verify ssl set to false."""
hub_data = dict(ENTRY_CONFIG)
hub_data[CONF_VERIFY_SSL] = False
2023-08-26 23:27:45 +00:00
with patch("aiounifi.Controller.login", return_value=True):
assert await get_unifi_api(hass, hub_data)
@pytest.mark.parametrize(
("side_effect", "raised_exception"),
[
(TimeoutError, CannotConnect),
(aiounifi.BadGateway, CannotConnect),
(aiounifi.Forbidden, CannotConnect),
(aiounifi.ServiceUnavailable, CannotConnect),
(aiounifi.RequestError, CannotConnect),
(aiounifi.ResponseError, CannotConnect),
(aiounifi.Unauthorized, AuthenticationRequired),
(aiounifi.LoginRequired, AuthenticationRequired),
(aiounifi.AiounifiException, AuthenticationRequired),
],
)
async def test_get_unifi_api_fails_to_connect(
hass: HomeAssistant, side_effect, raised_exception
) -> None:
"""Check that get_unifi_api can handle UniFi Network being unavailable."""
2023-08-26 23:27:45 +00:00
with patch("aiounifi.Controller.login", side_effect=side_effect), pytest.raises(
raised_exception
):
await get_unifi_api(hass, ENTRY_CONFIG)