diff --git a/homeassistant/components/unifi_access/config_flow.py b/homeassistant/components/unifi_access/config_flow.py index 81f99f4473ec..e88510f1bbdc 100644 --- a/homeassistant/components/unifi_access/config_flow.py +++ b/homeassistant/components/unifi_access/config_flow.py @@ -44,7 +44,11 @@ class UnifiAccessConfigFlow(ConfigFlow, domain=DOMAIN): try: await client.authenticate() except ApiAuthError: - errors["base"] = "invalid_auth" + try: + is_protect = await client.is_protect_api_key() + except Exception: # noqa: BLE001 + is_protect = False + errors["base"] = "protect_api_key" if is_protect else "invalid_auth" except ApiConnectionError: errors["base"] = "cannot_connect" except Exception: diff --git a/homeassistant/components/unifi_access/strings.json b/homeassistant/components/unifi_access/strings.json index 44cf6dd921b7..592a2fe58464 100644 --- a/homeassistant/components/unifi_access/strings.json +++ b/homeassistant/components/unifi_access/strings.json @@ -8,6 +8,7 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "protect_api_key": "This API key is associated with UniFi Protect, not UniFi Access. Please generate a new API key from the UniFi Access application settings.", "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { diff --git a/tests/components/unifi_access/conftest.py b/tests/components/unifi_access/conftest.py index c28e628ab4d1..40917a279ae4 100644 --- a/tests/components/unifi_access/conftest.py +++ b/tests/components/unifi_access/conftest.py @@ -105,6 +105,7 @@ def mock_client() -> Generator[MagicMock]: ): client = client_mock.return_value client.authenticate = AsyncMock() + client.is_protect_api_key = AsyncMock(return_value=False) client.get_doors = AsyncMock(return_value=MOCK_DOORS) client.get_emergency_status = AsyncMock( return_value=EmergencyStatus(evacuation=False, lockdown=False) diff --git a/tests/components/unifi_access/test_config_flow.py b/tests/components/unifi_access/test_config_flow.py index d42e70d6a453..abfe671b0821 100644 --- a/tests/components/unifi_access/test_config_flow.py +++ b/tests/components/unifi_access/test_config_flow.py @@ -398,3 +398,177 @@ async def test_user_flow_ssl_context( _, call_kwargs = patched_client.call_args assert isinstance(call_kwargs["ssl_context"], expected_ssl_context_type) + + +async def test_user_flow_protect_api_key( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_client: MagicMock, +) -> None: + """Test user config flow shows specific error when a Protect API key is used.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + mock_client.authenticate.side_effect = ApiAuthError() + mock_client.is_protect_api_key.return_value = True + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: MOCK_HOST, + CONF_API_TOKEN: MOCK_API_TOKEN, + CONF_VERIFY_SSL: False, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "protect_api_key"} + + # Test recovery + mock_client.authenticate.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: MOCK_HOST, + CONF_API_TOKEN: "correct-access-api-key", + CONF_VERIFY_SSL: False, + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_user_flow_protect_api_key_unreachable( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_client: MagicMock, +) -> None: + """Test user config flow falls back to invalid_auth when Protect is unreachable.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + mock_client.authenticate.side_effect = ApiAuthError() + mock_client.is_protect_api_key.return_value = False + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: MOCK_HOST, + CONF_API_TOKEN: MOCK_API_TOKEN, + CONF_VERIFY_SSL: False, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_user_flow_protect_api_key_check_raises( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_client: MagicMock, +) -> None: + """Test user config flow falls back to invalid_auth when protect check raises.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + mock_client.authenticate.side_effect = ApiAuthError() + mock_client.is_protect_api_key.side_effect = Exception("unexpected") + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: MOCK_HOST, + CONF_API_TOKEN: MOCK_API_TOKEN, + CONF_VERIFY_SSL: False, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_reauth_flow_protect_api_key( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow shows specific error when a Protect API key is used.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_client.authenticate.side_effect = ApiAuthError() + mock_client.is_protect_api_key.return_value = True + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_API_TOKEN: "protect-api-key"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "protect_api_key"} + + # Test recovery + mock_client.authenticate.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_API_TOKEN: "correct-access-api-key"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +async def test_reconfigure_flow_protect_api_key( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow shows specific error when a Protect API key is used.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + mock_client.authenticate.side_effect = ApiAuthError() + mock_client.is_protect_api_key.return_value = True + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "10.0.0.1", + CONF_API_TOKEN: "protect-api-key", + CONF_VERIFY_SSL: False, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "protect_api_key"} + + # Test recovery + mock_client.authenticate.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "10.0.0.1", + CONF_API_TOKEN: "correct-access-api-key", + CONF_VERIFY_SSL: False, + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful"