diff --git a/homeassistant/components/openai_conversation/config_flow.py b/homeassistant/components/openai_conversation/config_flow.py index c36bf437b012..fcef2c2a8f2b 100644 --- a/homeassistant/components/openai_conversation/config_flow.py +++ b/homeassistant/components/openai_conversation/config_flow.py @@ -11,6 +11,7 @@ import probatio from homeassistant.components.zone import ENTITY_ID_HOME from homeassistant.config_entries import ( SOURCE_REAUTH, + SOURCE_RECONFIGURE, ConfigEntry, ConfigEntryState, ConfigFlow, @@ -134,7 +135,12 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" + return await self._async_step_api_key(user_input, "user") + async def _async_step_api_key( + self, user_input: dict[str, Any] | None, step_id: str + ) -> ConfigFlowResult: + """Handle an API key form.""" errors: dict[str, str] = {} if user_input is not None: @@ -149,9 +155,18 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - if self.source == SOURCE_REAUTH: + if self.source in (SOURCE_REAUTH, SOURCE_RECONFIGURE): + entry = ( + self._get_reauth_entry() + if self.source == SOURCE_REAUTH + else self._get_reconfigure_entry() + ) + if entry.update_listeners: + return self.async_update_and_abort( + entry, data_updates=user_input + ) return self.async_update_reload_and_abort( - self._get_reauth_entry(), data_updates=user_input + entry, data_updates=user_input ) return self.async_create_entry( title="ChatGPT", @@ -185,7 +200,7 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="user", + step_id=step_id, data_schema=self.add_suggested_values_to_schema( STEP_USER_DATA_SCHEMA, user_input ), @@ -205,12 +220,13 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" - if not user_input: - return self.async_show_form( - step_id="reauth_confirm", data_schema=STEP_USER_DATA_SCHEMA - ) + return await self._async_step_api_key(user_input, "reauth_confirm") - return await self.async_step_user(user_input) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the API key.""" + return await self._async_step_api_key(user_input, "reconfigure") @classmethod @callback diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json index 3ffc6bfb2d35..6a46ad36217b 100644 --- a/homeassistant/components/openai_conversation/strings.json +++ b/homeassistant/components/openai_conversation/strings.json @@ -18,6 +18,15 @@ }, "description": "Reauthentication required. Please enter your updated API key." }, + "reconfigure": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "api_key": "[%key:component::openai_conversation::config::step::user::data_description::api_key%]" + }, + "description": "Enter your updated OpenAI API key." + }, "user": { "data": { "api_key": "[%key:common::config_flow::data::api_key%]" diff --git a/tests/components/openai_conversation/test_config_flow.py b/tests/components/openai_conversation/test_config_flow.py index 58913f8141d7..e47a93fada71 100644 --- a/tests/components/openai_conversation/test_config_flow.py +++ b/tests/components/openai_conversation/test_config_flow.py @@ -1646,6 +1646,61 @@ async def test_reauth(hass: HomeAssistant) -> None: assert mock_config_entry.data[CONF_API_KEY] == "new_api_key" +@pytest.mark.usefixtures("mock_init_component") +async def test_reconfigure( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test the API key can be reconfigured.""" + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + with ( + patch( + "homeassistant.components.openai_conversation.config_flow.openai.resources.models.AsyncModels.list", + new_callable=AsyncMock, + ), + patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as mock_async_reload, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_API_KEY: "new_api_key"} + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data[CONF_API_KEY] == "new_api_key" + assert mock_async_reload.call_count == 1 + + +async def test_reconfigure_invalid_auth( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test an invalid API key is rejected during reconfiguration.""" + result = await mock_config_entry.start_reconfigure_flow(hass) + + with patch( + "homeassistant.components.openai_conversation.config_flow.openai.resources.models.AsyncModels.list", + new_callable=AsyncMock, + side_effect=AuthenticationError( + response=httpx.Response(status_code=None, request=""), + body=None, + message=None, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_API_KEY: "invalid_api_key"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert result["errors"] == {"base": "invalid_auth"} + assert mock_config_entry.data[CONF_API_KEY] == "bla" + + @pytest.mark.parametrize( ("current_llm_apis", "suggested_llm_apis", "expected_options"), [