diff --git a/homeassistant/components/firefly_iii/config_flow.py b/homeassistant/components/firefly_iii/config_flow.py index 3d7ade86842..279d56c408f 100644 --- a/homeassistant/components/firefly_iii/config_flow.py +++ b/homeassistant/components/firefly_iii/config_flow.py @@ -129,6 +129,51 @@ class FireflyConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the integration.""" + errors: dict[str, str] = {} + reconf_entry = self._get_reconfigure_entry() + + if user_input: + try: + await _validate_input( + self.hass, + data={ + **reconf_entry.data, + **user_input, + }, + ) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except FireflyClientTimeout: + errors["base"] = "timeout_connect" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]}) + return self.async_update_reload_and_abort( + reconf_entry, + data_updates={ + CONF_URL: user_input[CONF_URL], + CONF_API_KEY: user_input[CONF_API_KEY], + CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], + }, + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_USER_DATA_SCHEMA, + suggested_values=user_input or reconf_entry.data.copy(), + ), + errors=errors, + ) + class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/firefly_iii/strings.json b/homeassistant/components/firefly_iii/strings.json index 4e097810335..163c9f315c0 100644 --- a/homeassistant/components/firefly_iii/strings.json +++ b/homeassistant/components/firefly_iii/strings.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -20,6 +21,20 @@ }, "description": "The access token for your Firefly III instance is invalid and needs to be updated. Go to **Options > Profile** and select the **OAuth** tab. Create a new personal access token and copy it (it will only display once)." }, + "reconfigure": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "url": "[%key:common::config_flow::data::url%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + }, + "data_description": { + "api_key": "[%key:component::firefly_iii::config::step::user::data_description::api_key%]", + "url": "[%key:common::config_flow::data::url%]", + "verify_ssl": "[%key:component::firefly_iii::config::step::user::data_description::verify_ssl%]" + }, + "description": "Use the following form to reconfigure your Firefly III instance.", + "title": "Reconfigure Firefly III Integration" + }, "user": { "data": { "api_key": "[%key:common::config_flow::data::api_key%]", diff --git a/tests/components/firefly_iii/conftest.py b/tests/components/firefly_iii/conftest.py index 7c9aa4c2bbd..5deffc97415 100644 --- a/tests/components/firefly_iii/conftest.py +++ b/tests/components/firefly_iii/conftest.py @@ -92,5 +92,5 @@ def mock_config_entry() -> MockConfigEntry: title="Firefly III test", data=MOCK_TEST_CONFIG, entry_id="firefly_iii_test_entry_123", - unique_id="firefly_iii_test_unique_id_123", + unique_id=MOCK_TEST_CONFIG[CONF_API_KEY], ) diff --git a/tests/components/firefly_iii/snapshots/test_diagnostics.ambr b/tests/components/firefly_iii/snapshots/test_diagnostics.ambr index c0316c446e7..5822af17c69 100644 --- a/tests/components/firefly_iii/snapshots/test_diagnostics.ambr +++ b/tests/components/firefly_iii/snapshots/test_diagnostics.ambr @@ -21,7 +21,7 @@ 'subentries': list([ ]), 'title': 'Firefly III test', - 'unique_id': 'firefly_iii_test_unique_id_123', + 'unique_id': 'test_api_key', 'version': 1, }), 'data': dict({ diff --git a/tests/components/firefly_iii/snapshots/test_sensor.ambr b/tests/components/firefly_iii/snapshots/test_sensor.ambr index ce35465a16f..efcbec9fdc4 100644 --- a/tests/components/firefly_iii/snapshots/test_sensor.ambr +++ b/tests/components/firefly_iii/snapshots/test_sensor.ambr @@ -32,7 +32,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'budget', - 'unique_id': 'firefly_iii_test_unique_id_123_budget_2_budget', + 'unique_id': 'test_api_key_budget_2_budget', 'unit_of_measurement': 'AMS', }) # --- @@ -85,7 +85,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'account_balance', - 'unique_id': 'firefly_iii_test_unique_id_123_account_4_account_balance', + 'unique_id': 'test_api_key_account_4_account_balance', 'unit_of_measurement': 'AMS', }) # --- @@ -136,7 +136,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'account_role', - 'unique_id': 'firefly_iii_test_unique_id_123_account_4_account_role', + 'unique_id': 'test_api_key_account_4_account_role', 'unit_of_measurement': None, }) # --- @@ -184,7 +184,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'account_type', - 'unique_id': 'firefly_iii_test_unique_id_123_account_4_account_type', + 'unique_id': 'test_api_key_account_4_account_type', 'unit_of_measurement': None, }) # --- @@ -235,7 +235,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'category', - 'unique_id': 'firefly_iii_test_unique_id_123_category_2_category', + 'unique_id': 'test_api_key_category_2_category', 'unit_of_measurement': 'AMS', }) # --- @@ -288,7 +288,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'account_balance', - 'unique_id': 'firefly_iii_test_unique_id_123_account_2_account_balance', + 'unique_id': 'test_api_key_account_2_account_balance', 'unit_of_measurement': 'AMS', }) # --- @@ -339,7 +339,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'account_role', - 'unique_id': 'firefly_iii_test_unique_id_123_account_2_account_role', + 'unique_id': 'test_api_key_account_2_account_role', 'unit_of_measurement': None, }) # --- @@ -387,7 +387,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'account_type', - 'unique_id': 'firefly_iii_test_unique_id_123_account_2_account_type', + 'unique_id': 'test_api_key_account_2_account_type', 'unit_of_measurement': None, }) # --- @@ -438,7 +438,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'account_balance', - 'unique_id': 'firefly_iii_test_unique_id_123_account_3_account_balance', + 'unique_id': 'test_api_key_account_3_account_balance', 'unit_of_measurement': 'AMS', }) # --- @@ -489,7 +489,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'account_role', - 'unique_id': 'firefly_iii_test_unique_id_123_account_3_account_role', + 'unique_id': 'test_api_key_account_3_account_role', 'unit_of_measurement': None, }) # --- @@ -537,7 +537,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'account_type', - 'unique_id': 'firefly_iii_test_unique_id_123_account_3_account_type', + 'unique_id': 'test_api_key_account_3_account_type', 'unit_of_measurement': None, }) # --- diff --git a/tests/components/firefly_iii/test_config_flow.py b/tests/components/firefly_iii/test_config_flow.py index afe0a95831e..3da91be316f 100644 --- a/tests/components/firefly_iii/test_config_flow.py +++ b/tests/components/firefly_iii/test_config_flow.py @@ -25,6 +25,12 @@ MOCK_USER_SETUP = { CONF_VERIFY_SSL: True, } +USER_INPUT_RECONFIGURE = { + CONF_URL: "https://new_domain:9000/", + CONF_API_KEY: "new_api_key", + CONF_VERIFY_SSL: True, +} + async def test_form_and_flow( hass: HomeAssistant, @@ -225,3 +231,123 @@ async def test_reauth_flow_exceptions( assert result["reason"] == "reauth_successful" assert mock_config_entry.data[CONF_API_KEY] == "new_api_key" assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_flow_reconfigure( + hass: HomeAssistant, + mock_firefly_client: AsyncMock, + mock_setup_entry: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the full flow of the config flow.""" + 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" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=USER_INPUT_RECONFIGURE, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data[CONF_API_KEY] == "new_api_key" + assert mock_config_entry.data[CONF_URL] == "https://new_domain:9000/" + assert mock_config_entry.data[CONF_VERIFY_SSL] is True + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_flow_reconfigure_unique_id( + hass: HomeAssistant, + mock_firefly_client: AsyncMock, + mock_setup_entry: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the full flow of the config flow, this time with a known unique ID.""" + mock_config_entry.add_to_hass(hass) + duplicate_entry = MockConfigEntry( + domain="firefly_iii", + data={ + CONF_URL: "https://duplicate-url/", + CONF_API_KEY: "other_key", + CONF_VERIFY_SSL: True, + }, + unique_id="very-annoying-duplicate", + ) + duplicate_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" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_URL: "https://duplicate-url/", + CONF_API_KEY: "new_key", + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("exception", "reason"), + [ + ( + FireflyAuthenticationError, + "invalid_auth", + ), + ( + FireflyConnectionError, + "cannot_connect", + ), + ( + FireflyTimeoutError, + "timeout_connect", + ), + ( + Exception("Some other error"), + "unknown", + ), + ], +) +async def test_full_flow_reconfigure_exceptions( + hass: HomeAssistant, + mock_firefly_client: AsyncMock, + mock_setup_entry: MagicMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + reason: str, +) -> None: + """Test the full flow of the config flow, this time with exceptions.""" + 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_firefly_client.get_about.side_effect = exception + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=USER_INPUT_RECONFIGURE, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": reason} + + mock_firefly_client.get_about.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=USER_INPUT_RECONFIGURE, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data[CONF_API_KEY] == "new_api_key" + assert mock_config_entry.data[CONF_URL] == "https://new_domain:9000/" + assert mock_config_entry.data[CONF_VERIFY_SSL] is True + assert len(mock_setup_entry.mock_calls) == 1