Fix authentication error when adding new devices to SMLIGHT (#138373)

* Fix authentication issue

Fixes #138216

* Fix incorrect mocks in unsupported device tests

* set _device_name in auth flow also

* Update get_info Mock to handle authentication

* Update tests
This commit is contained in:
TimL 2025-02-12 22:22:58 +11:00 committed by GitHub
parent 6ef1178a35
commit a3cde3d8ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 74 additions and 15 deletions

View File

@ -77,12 +77,14 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
try: try:
if not await self._async_check_auth_required(user_input):
info = await self.client.get_info() info = await self.client.get_info()
self._host = str(info.device_ip)
self._device_name = str(info.hostname)
if info.model not in Devices: if info.model not in Devices:
return self.async_abort(reason="unsupported_device") return self.async_abort(reason="unsupported_device")
if not await self._async_check_auth_required(user_input):
return await self._async_complete_entry(user_input) return await self._async_complete_entry(user_input)
except SmlightConnectionError: except SmlightConnectionError:
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")

View File

@ -3,6 +3,7 @@
from collections.abc import AsyncGenerator, Generator from collections.abc import AsyncGenerator, Generator
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from pysmlight.exceptions import SmlightAuthError
from pysmlight.sse import sseClient from pysmlight.sse import sseClient
from pysmlight.web import CmdWrapper, Firmware, Info, Sensors from pysmlight.web import CmdWrapper, Firmware, Info, Sensors
import pytest import pytest
@ -81,9 +82,16 @@ def mock_smlight_client(request: pytest.FixtureRequest) -> Generator[MagicMock]:
): ):
api = smlight_mock.return_value api = smlight_mock.return_value
api.host = MOCK_HOST api.host = MOCK_HOST
api.get_info.return_value = Info.from_dict(
load_json_object_fixture("info.json", DOMAIN) def get_info_side_effect(*args, **kwargs) -> Info:
) """Return the info."""
if api.check_auth_needed.return_value and not api.authenticate.called:
raise SmlightAuthError
return Info.from_dict(load_json_object_fixture("info.json", DOMAIN))
api.get_info.side_effect = get_info_side_effect
api.get_sensors.return_value = Sensors.from_dict( api.get_sensors.return_value = Sensors.from_dict(
load_json_object_fixture("sensors.json", DOMAIN) load_json_object_fixture("sensors.json", DOMAIN)
) )

View File

@ -45,6 +45,7 @@ async def test_buttons(
mock_smlight_client: MagicMock, mock_smlight_client: MagicMock,
) -> None: ) -> None:
"""Test creation of button entities.""" """Test creation of button entities."""
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = MOCK_ROUTER mock_smlight_client.get_info.return_value = MOCK_ROUTER
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
@ -78,6 +79,7 @@ async def test_disabled_by_default_buttons(
mock_smlight_client: MagicMock, mock_smlight_client: MagicMock,
) -> None: ) -> None:
"""Test the disabled by default buttons.""" """Test the disabled by default buttons."""
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = MOCK_ROUTER mock_smlight_client.get_info.return_value = MOCK_ROUTER
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
@ -96,7 +98,8 @@ async def test_remove_router_reconnect(
mock_smlight_client: MagicMock, mock_smlight_client: MagicMock,
) -> None: ) -> None:
"""Test removal of orphaned router reconnect button.""" """Test removal of orphaned router reconnect button."""
save_mock = mock_smlight_client.get_info.return_value save_mock = mock_smlight_client.get_info.side_effect
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = MOCK_ROUTER mock_smlight_client.get_info.return_value = MOCK_ROUTER
mock_config_entry = await setup_integration(hass, mock_config_entry) mock_config_entry = await setup_integration(hass, mock_config_entry)
@ -106,7 +109,7 @@ async def test_remove_router_reconnect(
assert len(entities) == 4 assert len(entities) == 4
assert entities[3].unique_id == "aa:bb:cc:dd:ee:ff-reconnect_zigbee_router" assert entities[3].unique_id == "aa:bb:cc:dd:ee:ff-reconnect_zigbee_router"
mock_smlight_client.get_info.return_value = save_mock mock_smlight_client.get_info.side_effect = save_mock
freezer.tick(SCAN_INTERVAL) freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass) async_fire_time_changed(hass)

View File

@ -66,6 +66,46 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_user_flow_auth(
hass: HomeAssistant, mock_smlight_client: MagicMock, mock_setup_entry: AsyncMock
) -> None:
"""Test the full manual user flow with authentication."""
mock_smlight_client.check_auth_needed.return_value = True
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "slzb-06p7.local",
},
)
assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "auth"
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: MOCK_USERNAME,
CONF_PASSWORD: MOCK_PASSWORD,
},
)
assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == "SLZB-06p7"
assert result3["data"] == {
CONF_USERNAME: MOCK_USERNAME,
CONF_PASSWORD: MOCK_PASSWORD,
CONF_HOST: MOCK_HOST,
}
assert result3["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff"
assert len(mock_setup_entry.mock_calls) == 1
async def test_zeroconf_flow( async def test_zeroconf_flow(
hass: HomeAssistant, hass: HomeAssistant,
mock_smlight_client: MagicMock, mock_smlight_client: MagicMock,
@ -145,7 +185,7 @@ async def test_zeroconf_flow_auth(
assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["context"]["source"] == "zeroconf" assert result3["context"]["source"] == "zeroconf"
assert result3["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert result3["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff"
assert result3["title"] == "slzb-06" assert result3["title"] == "SLZB-06p7"
assert result3["data"] == { assert result3["data"] == {
CONF_USERNAME: MOCK_USERNAME, CONF_USERNAME: MOCK_USERNAME,
CONF_PASSWORD: MOCK_PASSWORD, CONF_PASSWORD: MOCK_PASSWORD,
@ -162,6 +202,7 @@ async def test_zeroconf_unsupported_abort(
mock_smlight_client: MagicMock, mock_smlight_client: MagicMock,
) -> None: ) -> None:
"""Test we abort zeroconf flow if device unsupported.""" """Test we abort zeroconf flow if device unsupported."""
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = Info(model="SLZB-X") mock_smlight_client.get_info.return_value = Info(model="SLZB-X")
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -186,6 +227,7 @@ async def test_user_unsupported_abort(
mock_smlight_client: MagicMock, mock_smlight_client: MagicMock,
) -> None: ) -> None:
"""Test we abort user flow if unsupported device.""" """Test we abort user flow if unsupported device."""
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = Info(model="SLZB-X") mock_smlight_client.get_info.return_value = Info(model="SLZB-X")
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -206,15 +248,13 @@ async def test_user_unsupported_abort(
assert result2["reason"] == "unsupported_device" assert result2["reason"] == "unsupported_device"
async def test_user_unsupported_abort_auth( async def test_user_unsupported_device_abort_auth(
hass: HomeAssistant, hass: HomeAssistant,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
mock_smlight_client: MagicMock, mock_smlight_client: MagicMock,
) -> None: ) -> None:
"""Test we abort user flow if unsupported device (with auth).""" """Test we abort user flow if unsupported device (with auth)."""
mock_smlight_client.check_auth_needed.return_value = True mock_smlight_client.check_auth_needed.return_value = True
mock_smlight_client.authenticate.side_effect = SmlightAuthError
mock_smlight_client.get_info.side_effect = SmlightAuthError
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -366,7 +406,7 @@ async def test_user_invalid_auth(
} }
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_smlight_client.get_info.mock_calls) == 4 assert len(mock_smlight_client.get_info.mock_calls) == 3
async def test_user_cannot_connect( async def test_user_cannot_connect(

View File

@ -165,6 +165,7 @@ async def test_device_legacy_firmware(
"""Test device setup for old firmware version that dont support required API.""" """Test device setup for old firmware version that dont support required API."""
LEGACY_VERSION = "v0.9.9" LEGACY_VERSION = "v0.9.9"
mock_smlight_client.get_sensors.side_effect = SmlightError mock_smlight_client.get_sensors.side_effect = SmlightError
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = Info( mock_smlight_client.get_info.return_value = Info(
legacy_api=2, sw_version=LEGACY_VERSION, MAC="AA:BB:CC:DD:EE:FF" legacy_api=2, sw_version=LEGACY_VERSION, MAC="AA:BB:CC:DD:EE:FF"
) )

View File

@ -132,6 +132,7 @@ async def test_update_firmware(
event_function(MOCK_FIRMWARE_DONE) event_function(MOCK_FIRMWARE_DONE)
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = Info( mock_smlight_client.get_info.return_value = Info(
sw_version="v2.7.5", sw_version="v2.7.5",
) )
@ -153,6 +154,7 @@ async def test_update_zigbee2_firmware(
mock_smlight_client: MagicMock, mock_smlight_client: MagicMock,
) -> None: ) -> None:
"""Test update of zigbee2 firmware where available.""" """Test update of zigbee2 firmware where available."""
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = Info.from_dict( mock_smlight_client.get_info.return_value = Info.from_dict(
load_json_object_fixture("info-MR1.json", DOMAIN) load_json_object_fixture("info-MR1.json", DOMAIN)
) )
@ -195,6 +197,7 @@ async def test_update_legacy_firmware_v2(
mock_smlight_client: MagicMock, mock_smlight_client: MagicMock,
) -> None: ) -> None:
"""Test firmware update for legacy v2 firmware.""" """Test firmware update for legacy v2 firmware."""
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = Info( mock_smlight_client.get_info.return_value = Info(
sw_version="v2.0.18", sw_version="v2.0.18",
legacy_api=1, legacy_api=1,
@ -220,6 +223,7 @@ async def test_update_legacy_firmware_v2(
event_function(MOCK_FIRMWARE_DONE) event_function(MOCK_FIRMWARE_DONE)
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = Info( mock_smlight_client.get_info.return_value = Info(
sw_version="v2.7.5", sw_version="v2.7.5",
) )
@ -333,6 +337,7 @@ async def test_update_release_notes(
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,
) -> None: ) -> None:
"""Test firmware release notes.""" """Test firmware release notes."""
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = Info.from_dict( mock_smlight_client.get_info.return_value = Info.from_dict(
load_json_object_fixture("info-MR1.json", DOMAIN) load_json_object_fixture("info-MR1.json", DOMAIN)
) )