"""Test the config flow for Proxmox VE.""" from __future__ import annotations from unittest.mock import MagicMock from proxmoxer import AuthenticationError from proxmoxer.core import ResourceException import pytest from requests.exceptions import ConnectTimeout, SSLError from homeassistant.components.proxmoxve import CONF_HOST, CONF_REALM from homeassistant.components.proxmoxve.const import CONF_NODES, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from .conftest import MOCK_TEST_CONFIG from tests.common import MockConfigEntry MOCK_USER_STEP = { CONF_HOST: "127.0.0.1", CONF_USERNAME: "test_user@pam", CONF_PASSWORD: "test_password", CONF_VERIFY_SSL: True, CONF_PORT: 8006, CONF_REALM: "pam", } MOCK_USER_SETUP = {CONF_NODES: ["pve1"]} MOCK_USER_FINAL = { **MOCK_USER_STEP, **MOCK_USER_SETUP, } async def test_form( hass: HomeAssistant, mock_proxmox_client: MagicMock, ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_STEP ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "127.0.0.1" assert result["data"] == MOCK_TEST_CONFIG @pytest.mark.parametrize( ("exception", "reason"), [ ( AuthenticationError("Invalid credentials"), "invalid_auth", ), ( SSLError("SSL handshake failed"), "ssl_error", ), ( ConnectTimeout("Connection timed out"), "connect_timeout", ), ], ) async def test_form_exceptions( hass: HomeAssistant, mock_proxmox_client: MagicMock, exception: Exception, reason: str, ) -> None: """Test we handle all exceptions.""" mock_proxmox_client.nodes.get.side_effect = exception result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_STEP, ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": reason} mock_proxmox_client.nodes.get.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_STEP ) assert result["type"] is FlowResultType.CREATE_ENTRY async def test_form_no_nodes_exception( hass: HomeAssistant, mock_proxmox_client: MagicMock, ) -> None: """Test we handle no nodes found exception.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_proxmox_client.nodes.get.side_effect = ResourceException( "404", "status_message", "content" ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_STEP ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_nodes_found"} mock_proxmox_client.nodes.get.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_STEP ) assert result["type"] is FlowResultType.CREATE_ENTRY async def test_duplicate_entry( hass: HomeAssistant, mock_proxmox_client: MagicMock, mock_setup_entry: MagicMock, mock_config_entry: MockConfigEntry, ) -> None: """Test we handle duplicate entries.""" mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_STEP ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" async def test_import_flow( hass: HomeAssistant, mock_setup_entry: MagicMock, mock_proxmox_client: MagicMock, ) -> None: """Test importing from YAML creates a config entry and sets it up.""" MOCK_IMPORT_CONFIG = { DOMAIN: { **MOCK_USER_STEP, **MOCK_USER_SETUP, } } result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_CONFIG[DOMAIN] ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "127.0.0.1" assert result["data"][CONF_HOST] == "127.0.0.1" assert len(mock_setup_entry.mock_calls) == 1 assert result["result"].state is ConfigEntryState.LOADED @pytest.mark.parametrize( ("exception", "reason"), [ ( AuthenticationError("Invalid credentials"), "invalid_auth", ), ( SSLError("SSL handshake failed"), "ssl_error", ), ( ConnectTimeout("Connection timed out"), "connect_timeout", ), ( ResourceException("404", "status_message", "content"), "no_nodes_found", ), ], ) async def test_import_flow_exceptions( hass: HomeAssistant, mock_setup_entry: MagicMock, mock_proxmox_client: MagicMock, exception: Exception, reason: str, ) -> None: """Test importing from YAML creates a config entry and sets it up.""" MOCK_IMPORT_CONFIG = { DOMAIN: { **MOCK_USER_STEP, **MOCK_USER_SETUP, } } mock_proxmox_client.nodes.get.side_effect = exception result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_CONFIG[DOMAIN] ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason assert len(mock_setup_entry.mock_calls) == 0 assert len(hass.config_entries.async_entries(DOMAIN)) == 0 async def test_full_flow_reconfigure( hass: HomeAssistant, mock_proxmox_client: MagicMock, 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=MOCK_USER_STEP, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert mock_config_entry.data == MOCK_TEST_CONFIG async def test_full_flow_reconfigure_match_entries( hass: HomeAssistant, mock_proxmox_client: MagicMock, mock_setup_entry: MagicMock, mock_config_entry: MockConfigEntry, ) -> None: """Test the full flow of the config flow, this time matching existing entries.""" mock_config_entry.add_to_hass(hass) # Adding a second entry with a different host, since configuring the same host should work second_entry = MockConfigEntry( domain=DOMAIN, title="Second ProxmoxVE", data={ **MOCK_TEST_CONFIG, CONF_HOST: "192.168.1.1", }, ) second_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={ **MOCK_USER_STEP, CONF_HOST: "192.168.1.1", }, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data == MOCK_TEST_CONFIG assert len(mock_setup_entry.mock_calls) == 0 @pytest.mark.parametrize( ("exception", "reason"), [ ( AuthenticationError("Invalid credentials"), "invalid_auth", ), ( SSLError("SSL handshake failed"), "ssl_error", ), ( ConnectTimeout("Connection timed out"), "connect_timeout", ), ( ResourceException("404", "status_message", "content"), "no_nodes_found", ), ], ) async def test_full_flow_reconfigure_exceptions( hass: HomeAssistant, mock_proxmox_client: MagicMock, mock_setup_entry: MagicMock, mock_config_entry: MockConfigEntry, exception: Exception, reason: str, ) -> 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" mock_proxmox_client.nodes.get.side_effect = exception result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_STEP, ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": reason} mock_proxmox_client.nodes.get.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_STEP, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert mock_config_entry.data == MOCK_TEST_CONFIG async def test_full_flow_reauth( hass: HomeAssistant, mock_proxmox_client: MagicMock, 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 hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) result = await mock_config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # There is no user input result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "new_password"}, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_config_entry.data[CONF_PASSWORD] == "new_password" assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.parametrize( ("exception", "reason"), [ ( AuthenticationError("Invalid credentials"), "invalid_auth", ), ( SSLError("SSL handshake failed"), "ssl_error", ), ( ConnectTimeout("Connection timed out"), "connect_timeout", ), ( ResourceException("404", "status_message", "content"), "no_nodes_found", ), ], ) async def test_full_flow_reauth_exceptions( hass: HomeAssistant, mock_proxmox_client: MagicMock, mock_setup_entry: MagicMock, mock_config_entry: MockConfigEntry, exception: Exception, reason: str, ) -> None: """Test we handle all exceptions in the reauth flow.""" mock_config_entry.add_to_hass(hass) mock_proxmox_client.nodes.get.side_effect = exception result = await mock_config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "new_password"}, ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": reason} # Now test that we can recover from the error mock_proxmox_client.nodes.get.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "new_password"}, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_config_entry.data[CONF_PASSWORD] == "new_password" assert len(mock_setup_entry.mock_calls) == 1