diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py index 7199a5f3ed6..08e9b52a2ca 100644 --- a/tests/components/unifi/test_button.py +++ b/tests/components/unifi/test_button.py @@ -1,12 +1,14 @@ """UniFi Network button platform tests.""" from datetime import timedelta +from typing import Any +from unittest.mock import patch import pytest from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass from homeassistant.components.unifi.const import CONF_SITE_ID -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_HOST, @@ -22,266 +24,298 @@ import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker -WLAN_ID = "_id" -WLAN = { - WLAN_ID: "012345678910111213141516", - "bc_filter_enabled": False, - "bc_filter_list": [], - "dtim_mode": "default", - "dtim_na": 1, - "dtim_ng": 1, - "enabled": True, - "group_rekey": 3600, - "mac_filter_enabled": False, - "mac_filter_list": [], - "mac_filter_policy": "allow", - "minrate_na_advertising_rates": False, - "minrate_na_beacon_rate_kbps": 6000, - "minrate_na_data_rate_kbps": 6000, - "minrate_na_enabled": False, - "minrate_na_mgmt_rate_kbps": 6000, - "minrate_ng_advertising_rates": False, - "minrate_ng_beacon_rate_kbps": 1000, - "minrate_ng_data_rate_kbps": 1000, - "minrate_ng_enabled": False, - "minrate_ng_mgmt_rate_kbps": 1000, - "name": "SSID 1", - "no2ghz_oui": False, - "schedule": [], - "security": "wpapsk", - "site_id": "5a32aa4ee4b0412345678910", - "usergroup_id": "012345678910111213141518", - "wep_idx": 1, - "wlangroup_id": "012345678910111213141519", - "wpa_enc": "ccmp", - "wpa_mode": "wpa2", - "x_iapp_key": "01234567891011121314151617181920", - "x_passphrase": "password", -} +RANDOM_TOKEN = "random_token" -@pytest.mark.parametrize( - "device_payload", - [ - [ +@pytest.fixture(autouse=True) +def mock_secret(): + """Mock secret.""" + with patch("secrets.token_urlsafe", return_value=RANDOM_TOKEN): + yield + + +DEVICE_RESTART = [ + { + "board_rev": 3, + "device_id": "mock-id", + "ip": "10.0.0.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "switch", + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + } +] + +DEVICE_POWER_CYCLE_POE = [ + { + "board_rev": 3, + "device_id": "mock-id", + "ip": "10.0.0.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "switch", + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + "port_table": [ { - "board_rev": 3, - "device_id": "mock-id", - "ip": "10.0.0.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "switch", - "state": 1, - "type": "usw", - "version": "4.0.42.10433", - } - ] - ], -) -async def test_restart_device_button( + "media": "GE", + "name": "Port 1", + "port_idx": 1, + "poe_caps": 7, + "poe_class": "Class 4", + "poe_enable": True, + "poe_mode": "auto", + "poe_power": "2.56", + "poe_voltage": "53.40", + "portconf_id": "1a1", + "port_poe": True, + "up": True, + }, + ], + } +] + +WLAN_REGENERATE_PASSWORD = [ + { + "_id": "012345678910111213141516", + "bc_filter_enabled": False, + "bc_filter_list": [], + "dtim_mode": "default", + "dtim_na": 1, + "dtim_ng": 1, + "enabled": True, + "group_rekey": 3600, + "mac_filter_enabled": False, + "mac_filter_list": [], + "mac_filter_policy": "allow", + "minrate_na_advertising_rates": False, + "minrate_na_beacon_rate_kbps": 6000, + "minrate_na_data_rate_kbps": 6000, + "minrate_na_enabled": False, + "minrate_na_mgmt_rate_kbps": 6000, + "minrate_ng_advertising_rates": False, + "minrate_ng_beacon_rate_kbps": 1000, + "minrate_ng_data_rate_kbps": 1000, + "minrate_ng_enabled": False, + "minrate_ng_mgmt_rate_kbps": 1000, + "name": "SSID 1", + "no2ghz_oui": False, + "schedule": [], + "security": "wpapsk", + "site_id": "5a32aa4ee4b0412345678910", + "usergroup_id": "012345678910111213141518", + "wep_idx": 1, + "wlangroup_id": "012345678910111213141519", + "wpa_enc": "ccmp", + "wpa_mode": "wpa2", + "x_iapp_key": "01234567891011121314151617181920", + "x_passphrase": "password", + } +] + + +async def _test_button_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup, websocket_mock, + config_entry: ConfigEntry, + entity_count: int, + entity_id: str, + unique_id: str, + device_class: ButtonDeviceClass, + request_method: str, + request_path: str, + request_data: dict[str, Any], + call: dict[str, str], ) -> None: - """Test restarting device button.""" - config_entry = config_entry_setup - assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 1 + """Test button entity.""" + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == entity_count - ent_reg_entry = entity_registry.async_get("button.switch_restart") - assert ent_reg_entry.unique_id == "device_restart-00:00:00:00:01:01" + ent_reg_entry = entity_registry.async_get(entity_id) + assert ent_reg_entry.unique_id == unique_id assert ent_reg_entry.entity_category is EntityCategory.CONFIG # Validate state object - button = hass.states.get("button.switch_restart") + button = hass.states.get(entity_id) assert button is not None - assert button.attributes.get(ATTR_DEVICE_CLASS) == ButtonDeviceClass.RESTART + assert button.attributes.get(ATTR_DEVICE_CLASS) == device_class - # Send restart device command + # Send and validate device command aioclient_mock.clear_requests() - aioclient_mock.post( + aioclient_mock.request( + request_method, f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/devmgr", + f"/api/s/{config_entry.data[CONF_SITE_ID]}{request_path}", + **request_data, ) await hass.services.async_call( - BUTTON_DOMAIN, - "press", - {"entity_id": "button.switch_restart"}, - blocking=True, + BUTTON_DOMAIN, "press", {"entity_id": entity_id}, blocking=True ) assert aioclient_mock.call_count == 1 - assert aioclient_mock.mock_calls[0][2] == { - "cmd": "restart", - "mac": "00:00:00:00:01:01", - "reboot_type": "soft", - } + assert aioclient_mock.mock_calls[0][2] == call # Availability signalling # Controller disconnects await websocket_mock.disconnect() - assert hass.states.get("button.switch_restart").state == STATE_UNAVAILABLE + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # Controller reconnects await websocket_mock.reconnect() - assert hass.states.get("button.switch_restart").state != STATE_UNAVAILABLE + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE @pytest.mark.parametrize( - "device_payload", + ( + "device_payload", + "entity_count", + "entity_id", + "unique_id", + "device_class", + "request_method", + "request_path", + "call", + ), [ - [ + ( + DEVICE_RESTART, + 1, + "button.switch_restart", + "device_restart-00:00:00:00:01:01", + ButtonDeviceClass.RESTART, + "post", + "/cmd/devmgr", { - "board_rev": 3, - "device_id": "mock-id", - "ip": "10.0.0.1", - "last_seen": 1562600145, + "cmd": "restart", "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "switch", - "state": 1, - "type": "usw", - "version": "4.0.42.10433", - "port_table": [ - { - "media": "GE", - "name": "Port 1", - "port_idx": 1, - "poe_caps": 7, - "poe_class": "Class 4", - "poe_enable": True, - "poe_mode": "auto", - "poe_power": "2.56", - "poe_voltage": "53.40", - "portconf_id": "1a1", - "port_poe": True, - "up": True, - }, - ], - } - ] + "reboot_type": "soft", + }, + ), + ( + DEVICE_POWER_CYCLE_POE, + 2, + "button.switch_port_1_power_cycle", + "power_cycle-00:00:00:00:01:01_1", + ButtonDeviceClass.RESTART, + "post", + "/cmd/devmgr", + { + "cmd": "power-cycle", + "mac": "00:00:00:00:01:01", + "port_idx": 1, + }, + ), ], ) -async def test_power_cycle_poe( +async def test_device_button_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, config_entry_setup, websocket_mock, + entity_count: int, + entity_id: str, + unique_id: str, + device_class: ButtonDeviceClass, + request_method: str, + request_path: str, + call: dict[str, str], ) -> None: - """Test restarting device button.""" - config_entry = config_entry_setup - assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 2 - - ent_reg_entry = entity_registry.async_get("button.switch_port_1_power_cycle") - assert ent_reg_entry.unique_id == "power_cycle-00:00:00:00:01:01_1" - assert ent_reg_entry.entity_category is EntityCategory.CONFIG - - # Validate state object - button = hass.states.get("button.switch_port_1_power_cycle") - assert button is not None - assert button.attributes.get(ATTR_DEVICE_CLASS) == ButtonDeviceClass.RESTART - - # Send restart device command - aioclient_mock.clear_requests() - aioclient_mock.post( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/devmgr", - ) - - await hass.services.async_call( - BUTTON_DOMAIN, - "press", - {"entity_id": "button.switch_port_1_power_cycle"}, - blocking=True, - ) - assert aioclient_mock.call_count == 1 - assert aioclient_mock.mock_calls[0][2] == { - "cmd": "power-cycle", - "mac": "00:00:00:00:01:01", - "port_idx": 1, - } - - # Availability signalling - - # Controller disconnects - await websocket_mock.disconnect() - assert ( - hass.states.get("button.switch_port_1_power_cycle").state == STATE_UNAVAILABLE - ) - - # Controller reconnects - await websocket_mock.reconnect() - assert ( - hass.states.get("button.switch_port_1_power_cycle").state != STATE_UNAVAILABLE + """Test button entities based on device sources.""" + await _test_button_entity( + hass, + entity_registry, + aioclient_mock, + websocket_mock, + config_entry_setup, + entity_count, + entity_id, + unique_id, + device_class, + request_method, + request_path, + {}, + call, ) -@pytest.mark.parametrize("wlan_payload", [[WLAN]]) -async def test_wlan_regenerate_password( +@pytest.mark.parametrize( + ( + "wlan_payload", + "entity_count", + "entity_id", + "unique_id", + "device_class", + "request_method", + "request_path", + "request_data", + "call", + ), + [ + ( + WLAN_REGENERATE_PASSWORD, + 1, + "button.ssid_1_regenerate_password", + "regenerate_password-012345678910111213141516", + ButtonDeviceClass.UPDATE, + "put", + f"/rest/wlanconf/{WLAN_REGENERATE_PASSWORD[0]["_id"]}", + { + "json": {"data": "password changed successfully", "meta": {"rc": "ok"}}, + "headers": {"content-type": CONTENT_TYPE_JSON}, + }, + {"x_passphrase": RANDOM_TOKEN}, + ), + ], +) +async def test_wlan_button_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, config_entry_setup, websocket_mock, + entity_count: int, + entity_id: str, + unique_id: str, + device_class: ButtonDeviceClass, + request_method: str, + request_path: str, + request_data: dict[str, Any], + call: dict[str, str], ) -> None: - """Test WLAN regenerate password button.""" - config_entry = config_entry_setup + """Test button entities based on WLAN sources.""" assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 0 - button_regenerate_password = "button.ssid_1_regenerate_password" - - ent_reg_entry = entity_registry.async_get(button_regenerate_password) - assert ent_reg_entry.unique_id == "regenerate_password-012345678910111213141516" + ent_reg_entry = entity_registry.async_get(entity_id) assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION - assert ent_reg_entry.entity_category is EntityCategory.CONFIG # Enable entity - entity_registry.async_update_entity( - entity_id=button_regenerate_password, disabled_by=None - ) - await hass.async_block_till_done() - + entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 1 - - # Validate state object - button = hass.states.get(button_regenerate_password) - assert button is not None - assert button.attributes.get(ATTR_DEVICE_CLASS) == ButtonDeviceClass.UPDATE - - aioclient_mock.clear_requests() - aioclient_mock.put( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/wlanconf/{WLAN[WLAN_ID]}", - json={"data": "password changed successfully", "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, + await _test_button_entity( + hass, + entity_registry, + aioclient_mock, + websocket_mock, + config_entry_setup, + entity_count, + entity_id, + unique_id, + device_class, + request_method, + request_path, + request_data, + call, ) - - # Send WLAN regenerate password command - await hass.services.async_call( - BUTTON_DOMAIN, - "press", - {"entity_id": button_regenerate_password}, - blocking=True, - ) - assert aioclient_mock.call_count == 1 - assert next(iter(aioclient_mock.mock_calls[0][2])) == "x_passphrase" - - # Availability signalling - - # Controller disconnects - await websocket_mock.disconnect() - assert hass.states.get(button_regenerate_password).state == STATE_UNAVAILABLE - - # Controller reconnects - await websocket_mock.reconnect() - assert hass.states.get(button_regenerate_password).state != STATE_UNAVAILABLE